check_bins (dashboard) · check_tomorrow (notification trigger) · check_schedule_expiry (bell curve) · check_address.
Past confirmed_until, rotation continues forward, prefixed (PROJECTED).
Files on iaccn01, Ansible playbook copies to HA container mount.
A bin reminder shouldn't be this interesting
It also shouldn't false-positive six days a week, or silently lapse the day the council issues a new calendar.
North Lanarkshire’s calendar is a PDF — three-week green / blue / black rotation, brown bin every other week, Wednesday collection. Standard council format. The naive solution is a weekly time-triggered automation; the right solution is a small data model with sensors derived from it, so adding new automations (notifications, dashboard cards, holiday adjustments) doesn’t require touching the bin logic.
The shape that landed was a schedule.json with metadata and a date array, four command-line sensors driving different consumers, an Ansible playbook to deploy it, and a documented “paste this prompt into Claude” workflow for regenerating the JSON when the council issues a new calendar.
schedule.json, the single source of truth
{
"metadata": {
"address": "<redacted>",
"confirmed_from": "2026-04-15",
"confirmed_until": "2027-03-31",
"collection_day": "Wednesday",
"rotation": ["green", "blue", "black"],
"brown_alternates": true,
"notes": "Council holidays (Christmas/New Year) may shift dates — verify against printed calendar."
},
"schedule": [
{"date": "2026-04-15", "bins": ["blue"]},
{"date": "2026-04-22", "bins": ["green"]},
{"date": "2026-04-29", "bins": ["general"]},
{"date": "2026-05-06", "bins": ["blue", "brown"]},
{"date": "2026-05-13", "bins": ["green"]}
]
} One JSON, four consumers
Each sensor has one job. The split is what eliminates the false-positive problem.
always shows next collection + countdown
returns "none" 6 days/wk; only fires on collection eve
days remaining until confirmed_until lapses
address tag for dashboard card
The bugs that turned a one-day build into a five-session arc
Notification fired every single day
Set up a time trigger at 18:00 with a condition sensor.bin_collection_tomorrow != 'none'. Got reminded “Bins out tonight” every evening — including Mondays, Thursdays, Fridays. Useless.
The dashboard sensor always has a value — it’s the next collection with a countdown (“Next: Green in 4 days”), which is what makes the dashboard card useful. The condition != 'none' is therefore always true.
Split the dashboard sensor from the notification sensor. sensor.bin_collection_alert (new) returns 'none' six days a week and only returns bin names when tomorrow is collection day. Automation triggers off the alert sensor; dashboard keeps using the always-populated one. The pattern generalises: any sensor used both for display and notification will produce false positives in one of the two contexts.
Schedule silently expires when council issues new calendar
Initial design: hardcoded confirmed dates only. Past the last date, every sensor returns “none” — the dashboard says nothing, no notifications fire, no warning. The system silently degrades to non-functional.
No notion of schedule freshness, no projection past the last known date, no expiry alerts.
Added metadata.confirmed_from / confirmed_until, a projection layer in check_bins.sh that continues the rotation rules forward up to 90 days (prefixed (PROJECTED) so the source-of-truth distinction is preserved), and a check_schedule_expiry.sh sensor that returns integer days remaining. Bell-curve automation fires reminders at ±28, ±14, ±7, ±3, and 0 days relative to expiry so the schedule never silently lapses.
Schedule JSON regeneration was a chore
Hand-typing 52 dates plus bin types from a council PDF, every year, is the kind of toil that means it doesn’t get done.
No tooling around the data refresh.
Documented a “paste this prompt into Claude / ChatGPT” workflow with the exact JSON schema and the rotation rules pre-stated. Operator pastes the council’s plain-text dates in any format; the model returns a single-line JSON ready to paste into the cat > schedule.json line on iaccn01. Refresh time: ~2 minutes.
Two patterns worth keeping
- pattern·Single data file, multiple sensors. Adding a new consumer (e.g. a Lovelace card on a different dashboard, a daily morning summary, a holiday-week override) is a new
.shscript and an HA sensor entry — no changes to the underlying data. - pattern·Confirmed data + projection layer with clear labelling. The system never fails closed when input data goes stale; it degrades to projected output that’s clearly marked, while a separate sensor measures and alerts on the staleness itself.
- pattern·Display sensor ≠ notification sensor. Whenever a single sensor is used for both, false positives happen in one or the other. Splitting them early is cheaper than debugging the symptom later.