Skip to content

Latest commit

 

History

History
341 lines (257 loc) · 9.36 KB

File metadata and controls

341 lines (257 loc) · 9.36 KB

AGENTS.md - AI Agent Instructions

Project Goal

Build and maintain a production-quality Go CLI application that fetches monthly trash pickup schedules from BDG's (Barnim Dienstleistungsgesellschaft mbH) public API, caches them locally, and sends reminders to Ulanzi AWTRIX 3 displays via MQTT. The application is designed to run via cron (not as a long-running service) and must be deterministic, reliable, and boring.

Service Provider: BDG serves the Barnim district in Brandenburg, Germany. See Kreiswerke Barnim for more information.

Non-Goals

  • ❌ Web UI or web server
  • ❌ Database or persistent storage beyond file-based cache
  • ❌ Real-time monitoring or long-running background services
  • ❌ Authentication or user management
  • ❌ Multi-tenancy or SaaS features
  • ❌ Complex retry logic or fault tolerance beyond fail-fast
  • ❌ Metrics, telemetry, or observability beyond basic logging

Architecture Overview

Package Responsibilities

bdg/
├── cmd/                    # CLI layer - Cobra commands, flag parsing
│   ├── root.go            # Root command, shared flags, validation
│   ├── status.go          # Status command - persistent display
│   └── alarm.go           # Alarm command - tomorrow-only notification
│
├── internal/api/          # HTTP client for BDG trash API
│   ├── types.go           # API response structs, domain models
│   └── client.go          # HTTP client, color normalization, parsing
│
├── internal/cache/        # File-based cache layer
│   └── cache.go           # Month-based JSON cache (YYYY-MM.json)
│
├── internal/domain/       # Pure business logic (no I/O)
│   ├── pickup.go          # Filtering, finding nearest pickups
│   └── display.go         # Date formatting, tomorrow detection
│
├── internal/mqtt/         # MQTT publisher
│   └── publisher.go       # Connect, publish, disconnect
│
└── internal/awtrix/       # AWTRIX payload construction
    ├── types.go           # AWTRIX JSON structs
    └── builder.go         # Status and alarm payload builders

Data Flow

  1. CLI Command (status or alarm) receives flags
  2. Fetch: Check cache, fallback to API if missing
  3. Cache: Write API response to ~/.cache/bdg/YYYY-MM.json
  4. Parse: Convert API response to domain Pickup objects
  5. Filter: Remove past dates and ignored trash types
  6. Find Nearest: Identify all pickups on the nearest day
  7. Build Payload: Construct AWTRIX JSON with draw commands
  8. Publish: Send to MQTT broker, disconnect

Key Design Principles

  1. Deterministic: Same inputs always produce same outputs
  2. Fail Fast: No retries, clear error messages
  3. Stateless: Each invocation is independent
  4. Testable: Domain logic is pure, I/O is isolated
  5. Minimal: Standard library + Cobra + Paho MQTT only

Commands

Build

task build
# OR
go build -o bin/bdg .

Test

task test
# OR
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...

View coverage:

go tool cover -html=coverage.txt

Lint

task lint
# OR
golangci-lint run ./...

Coding Rules

CLI & Commands

  • All flags validated before execution: Use validateFlags() in root.go
  • No interactive prompts: CLI must run unattended in cron
  • Exit codes: 0 for success, 1 for errors
  • Silence usage on error: Set SilenceUsage: true on commands
  • stderr for warnings, stdout for results

Caching Rules

  • Never refetch: If YYYY-MM.json exists in cache, use it
  • Atomic writes: Use temp file + rename pattern
  • Next month fetch: Only if now + 7 days crosses month boundary
  • Cache structure: ~/.cache/bdg/YYYY-MM.json
  • Cache miss is not an error: Return nil, nil on cache miss

Time Handling

  • Pure functions: All domain logic accepts time.Time as parameter
  • Normalize to day: Use time.Date(year, month, day, 0, 0, 0, 0, loc) for comparisons
  • UTC in tests: Always use time.UTC for deterministic tests
  • No global time.Now(): Pass time as parameter for testability

Error Handling

  • Fail fast: No retries, no fallbacks
  • Wrap errors: Use fmt.Errorf("context: %w", err) for stack context
  • Descriptive messages: Include operation and values in error messages
  • Log warnings for non-fatal issues: e.g., cache write failures

Domain Logic

  • Pure functions: No I/O, no side effects
  • Table-driven tests: Use subtests with test cases
  • 100% coverage target: Domain logic must be fully tested
  • No dependencies: Domain package imports only stdlib + api types

AWTRIX Rules

MQTT Topics

  • Status display: [PREFIX]/custom/trash
  • Alarm notification: [PREFIX]/notify
  • QoS 0: Fire-and-forget
  • Clean session: Always use clean session
  • No retained messages

Draw Command Spec

Color chips (status display):

  • Size: 4×7 pixels (width × height)
  • Command: df (filled rectangle)
  • Spacing: 5 pixels between chips (x = 0, 5, 10, 15, ...)
  • Y position: 0 (top of display)
  • Colors: Use normalized hex from API (e.g., #566322)

Text display:

  • Command: dt (draw text)
  • X position: 15 (fixed)
  • Y position: 1
  • Max length: 4 characters, uppercase
  • Color: Red (#FF0000) for tomorrow, white (#FFFFFF) otherwise

Example draw payload:

{
  "draw": [
    {"df": [0, 0, 4, 7, "#FFFF00"]},
    {"df": [5, 0, 4, 7, "#999999"]},
    {"dt": [15, 1, "TOM", "#FF0000"]}
  ],
  "noScroll": true,
  "duration": 5
}

Alarm Spec

  • Trigger: Only if nearest pickup is tomorrow
  • Background: Red (#FF0000)
  • Text: Blinking (500ms interval)
  • Hold: true (requires button press to dismiss)
  • Text format: "TRASH: [Title1], [Title2], ..."

Example alarm payload:

{
  "text": "TRASH: Bio 14-tgl., Gelbe Tonne",
  "background": "#FF0000",
  "blinkText": 500,
  "hold": true,
  "duration": 0,
  "noScroll": false
}

Color Normalization

  • Pad to 6 characters with leading zeros (e.g., "99999""099999")
  • Add # prefix
  • Uppercase hex letters
  • Truncate if > 6 characters

Definition of Done

Before considering a task complete:

  • All tests pass (task test)
  • Linter passes with zero issues (task lint)
  • Test coverage > 95% (check coverage.html)
  • Code compiles without warnings
  • Manual smoke test (if applicable):
    • Status command updates AWTRIX display
    • Alarm command sends notification only for tomorrow
    • Cache files created correctly
  • Documentation updated (README, this file)
  • No debugging code or console logs (except intentional output)
  • Error messages are clear and actionable

Testing Strategy

Unit Tests (Domain Logic)

  • Location: internal/domain/*_test.go
  • Coverage: 100% target
  • Style: Table-driven tests with descriptive names
  • Focus: Edge cases, boundary conditions, timezone handling

Integration Tests (I/O Components)

  • API Client: Use httptest for fake server
  • Cache: Use t.TempDir() for isolated tests
  • MQTT: Mock publisher or use test broker (optional)

Test Data

Use realistic test data:

  • Dates crossing month boundaries
  • Multiple pickups on same day
  • Empty results
  • Malformed API responses (for error handling)

Common Tasks

Adding a New Trash Type Filter

  1. No code changes needed - use --ignore flag
  2. Update README examples if commonly requested

Changing Display Format

DO NOT change without discussion:

  • Chip geometry (4×7)
  • Spacing (5px)
  • Text position (x=15)
  • Color scheme (red for tomorrow, white otherwise)

These are locked by AWTRIX display constraints.

Adding New Command

  1. Create cmd/newcommand.go
  2. Register in init() with rootCmd.AddCommand()
  3. Use shared flags from root.go
  4. Follow RunE pattern for error handling
  5. Add tests
  6. Update README

Updating Dependencies

go get -u github.com/spf13/cobra
go get -u github.com/eclipse/paho.mqtt.golang
go mod tidy

Test thoroughly after updates.

Debugging

Enable Verbose Output

Add to command:

fmt.Fprintf(os.Stderr, "DEBUG: %v\n", value)

Test with Mock MQTT

Use a local MQTT broker:

docker run -it -p 1883:1883 eclipse-mosquitto

Subscribe to messages:

mosquitto_sub -h localhost -t "awtrix_abc123/#" -v

Inspect Cache

cat ~/.cache/bdg/2025-12.json | jq .

Test API Directly

curl "https://bdg.jumomind.com/mmapp/api.php?r=calendar/2025-12&city_id=XXX&area_id=YYY" | jq .

Maintenance

Monthly Smoke Test

Around the 1st of each month:

  1. Delete cache: rm -rf ~/.cache/bdg/
  2. Run status: ./bdg status <flags>
  3. Verify cache created
  4. Check AWTRIX display

Handling API Changes

If API response format changes:

  1. Update internal/api/types.go
  2. Update internal/api/client.go parsing
  3. Add tests for new format
  4. Consider backward compatibility

Performance

Current implementation is optimized for:

  • Small data sets (< 100 pickups per month)
  • Infrequent execution (hourly/daily via cron)
  • Low memory footprint (< 10MB)

If these constraints change, revisit architecture.


Remember: This is a boring, reliable tool. Resist the urge to add features. Keep it simple, deterministic, and well-tested.