Bin Vader Python Script Documentation
Purpose
bin_vader.py is the deterministic rota engine for Bin Vader.
It calculates:
- whether today is bin night
- the next collection date
- which bins need to go out
- which flat is responsible
- whether a UK bank holiday shifts collection from Friday to Saturday
- one-off swaps
- exchange swaps
- cancelled swaps
- manual overrides
- schedule listings for up to 12 weeks
The script is designed so that rota decisions and reminders do not depend on language-model judgement. The agent may explain the result, but the script should decide the result.
Recommended project layout
bin-vader/
scripts/
bin_vader.py
__init__.py
state/
bin_rota.json
bank_holidays_cache.json
tests/
__init__.py
test_bin_vader.py
docs/
bin_vader_script_documentation.md
Runtime requirements
Python 3.11 or later is recommended.
The main script uses only the Python standard library:
argparsejsonurllib.requestdataclassesdatetimepathlibtypingzoneinfo
No external package is required to run the script.
pytest is only needed for the test harness.
State file
By default, the script reads state from:
state/bin_rota.json
Minimal state file:
{
"timezone": "Europe/London",
"bank_holiday_region": "england-and-wales",
"anchor_collection_date": "2026-01-02",
"anchor_rota_index": 0,
"anchor_bin_pattern": "black_green",
"swaps": [],
"manual_overrides": []
}
timezone
The timezone used when the script needs to know today's date.
Recommended value:
"Europe/London"
bank_holiday_region
The GOV.UK bank holiday region.
For England and Wales:
"england-and-wales"
Other GOV.UK region keys are:
scotland
northern-ireland
anchor_collection_date
A known real collection date.
This must be a date where you know:
- collection actually happened
- which flat was responsible
- which bin type was collected
Example:
"anchor_collection_date": "2026-01-02"
anchor_rota_index
The index in the rota sequence for the anchor collection date.
The rota sequence is:
index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
flat: 1 2 3 4 5 6 7 8 8 7 6 5 4 3 2 1
Example:
"anchor_rota_index": 0
means Flat 1 was responsible on the anchor date.
anchor_bin_pattern
The bin type on the anchor collection date.
Allowed values:
black_green
green_only
Example:
"anchor_bin_pattern": "black_green"
Core rota rules
Normal collection:
Friday morning
Normal bin night:
Thursday night
Bank-holiday-shifted collection:
Saturday morning
Bank-holiday-shifted bin night:
Friday night
The current bank holiday rule is:
If there is a UK bank holiday from Monday to Friday in the same collection week,
the usual Friday collection shifts to Saturday.
This rule is implemented in:
affected_by_bank_holiday()
If the council behaves differently, change that function.
Bin pattern
Collections alternate between:
black and green bins
green bins only
If the anchor collection is black_green, then:
collection 0: black and green bins
collection 1: green bins only
collection 2: black and green bins
collection 3: green bins only
If the anchor collection is green_only, the pattern is reversed.
Flat rota
The rota sequence is:
1,2,3,4,5,6,7,8,8,7,6,5,4,3,2,1
The sequence then repeats.
The rota advances once per actual collection. It does not advance by calendar week independently of collection dates.
Bank holiday data
The script fetches bank holidays from GOV.UK:
https://www.gov.uk/bank-holidays.json
The result is cached in:
state/bank_holidays_cache.json
The cache is reused for up to 14 days. If the cache is missing, stale, or invalid, the script fetches fresh data.
Command reference
All examples assume the command is run from the project root.
Check whether today is bin night
scripts/bin_vader.py today
Example output on a bin night:
Tonight is bin night. Collection is tomorrow morning, 2026-06-05. Bins: green bins only. Responsible flat: 4.
Example output when it is not bin night:
No. Tonight is not bin night. Next bin night is 2026-06-11. Collection is 2026-06-12. Bins: black and green bins. Responsible flat: 5.
Test with a fixed date:
scripts/bin_vader.py today --date 2026-05-29
Emit a reminder only on bin night
scripts/bin_vader.py remind
This prints a reminder only if today is bin night. If today is not bin night, it prints nothing.
This makes it suitable for cron.
Test with a fixed date:
scripts/bin_vader.py remind --date 2026-05-29
Show the next collection
scripts/bin_vader.py next
Example output:
Collection: 2026-06-05
Bin night: 2026-06-04
Bins: green bins only
Responsible flat: 4
Base rota flat: 4
Rota index: 3
Start from a fixed date:
scripts/bin_vader.py next --date 2026-06-01
Show a specific collection date
scripts/bin_vader.py date 2026-06-05
The date must be a real collection date.
A normal week collection date will usually be Friday. A bank-holiday-shifted collection date will be Saturday.
List a schedule
scripts/bin_vader.py schedule 4
This lists the next 4 weeks.
The maximum allowed value is:
12
Requests above 12 weeks are rejected.
Example output:
| Bin night | Collection | Bins | Flat | Notes |
|---|---|---:|---:|---|
| Thursday 2026-06-04 | Friday 2026-06-05 | black and green bins | 3 | |
| Thursday 2026-06-11 | Friday 2026-06-12 | green bins only | 4 | |
Start from a fixed date:
scripts/bin_vader.py schedule 6 --date 2026-06-01
JSON output:
scripts/bin_vader.py schedule 12 --format json
Swaps
One-off swaps
A one-off swap means another flat covers a single collection.
Command:
scripts/bin_vader.py add-swap COLLECTION_DATE ORIGINAL_FLAT REPLACEMENT_FLAT
Example:
scripts/bin_vader.py add-swap 2026-06-05 3 6 --recorded-by "Nigel"
Meaning:
Flat 6 covers Flat 3 on 2026-06-05.
Example output:
Recorded swap-0001. For collection on 2026-06-05, Flat 6 is covering Flat 3.
The swap is persisted in the state file.
Exchange swaps
An exchange means two flats swap turns across two dates.
Command:
scripts/bin_vader.py add-exchange FLAT_A FLAT_B COLLECTION_DATE_A COLLECTION_DATE_B
Example:
scripts/bin_vader.py add-exchange 3 6 2026-06-05 2026-07-31 --recorded-by "Nigel"
Meaning:
Flat 6 covers Flat 3 on 2026-06-05.
Flat 3 covers Flat 6 on 2026-07-31.
Both dates are annotated in schedule output.
Example output:
Recorded swap-0001. Flat 3 and Flat 6 have exchanged 2026-06-05 and 2026-07-31.
List swaps
scripts/bin_vader.py list-swaps
Example output:
swap-0001 [active] exchange: Flat 6 covers Flat 3 on 2026-06-05; Flat 3 covers Flat 6 on 2026-07-31
Cancelled swaps remain visible:
swap-0001 [cancelled] exchange: Flat 6 covers Flat 3 on 2026-06-05; Flat 3 covers Flat 6 on 2026-07-31
Cancel swaps
Swaps are cancelled by ID.
Command:
scripts/bin_vader.py cancel-swap swap-0001 --cancelled-by "Nigel"
Optional reason:
scripts/bin_vader.py cancel-swap swap-0001 --cancelled-by "Nigel" --reason "Changed back"
Example output:
Cancelled swap-0001.
Cancellation does not delete the swap. Instead, the swap record is marked with cancellation fields:
{
"cancelled_at": "2026-06-01T18:30:00+01:00",
"cancelled_by": "Nigel",
"cancellation_reason": "Changed back"
}
Cancelled swaps do not affect:
- today checks
- reminders
- next collection
- schedule listings
- date lookups
Manual overrides
A manual override is a human-confirmed correction. Use this if the deterministic rota needs to be overridden for a specific collection.
Command:
scripts/bin_vader.py add-override COLLECTION_DATE RESPONSIBLE_FLAT BINS
Allowed bin values:
black_green
green_only
Example:
scripts/bin_vader.py add-override 2026-06-06 5 green_only --source "confirmed by council notice"
Manual overrides are applied before swaps.
This means:
- the override changes the base responsibility or bin type
- any relevant swap is then applied
Reminder cron
Use remind for scheduled reminders.
Example crontab:
TZ=Europe/London
0 18 * * * cd /opt/bin-vader && ./scripts/bin_vader.py remind | /opt/bin-vader/scripts/post_to_group.sh
Because remind prints nothing when it is not bin night, the posting script should send only when input is non-empty.
Example safe posting wrapper:
#!/usr/bin/env bash
set -euo pipefail
message="$(cat)"
if [ -z "$message" ]; then
exit 0
fi
# Replace this with the WhatsApp sending command.
echo "$message"
WhatsApp integration
The WhatsApp bridge should enforce the activation phrase before passing messages to Bin Vader.
Recommended activation phrase:
Hey Bin Vader
Example gate:
import re
ACTIVATION_RE = re.compile(r"^\s*hey\s*,?\s*bin\s+vader\b[:,]?\s*(.*)$", re.I)
def extract_bin_vader_command(message: str) -> str | None:
match = ACTIVATION_RE.match(message)
if not match:
return None
command = match.group(1).strip()
return command or "help"
Messages without the activation phrase should be ignored silently by the bridge.
Scheduled reminders do not need the activation phrase.
Testing
Install pytest:
python3 -m pip install pytest
Run tests:
python3 -m pytest -q
Expected result for the supplied harness:
15 passed
The tests use fixed bank holiday data and do not call GOV.UK.
Behaviours covered by tests
The test harness checks:
- rota sequence
- forward and backward flat order
- alternating bin pattern
- bank holiday shift from Friday to Saturday
- Friday bin night before a shifted Saturday collection
- one-off swaps
- cancelled one-off swaps
- exchange swaps
- annotations on both exchange dates
- cancelled exchanges
- manual override priority
- swap ID allocation
- cancelling swaps
- schedule maximum of 12 weeks
- table and JSON schedule output
Troubleshooting
State file not found
The script could not find:
state/bin_rota.json
Create the state file or pass a custom path:
scripts/bin_vader.py --state /path/to/bin_rota.json today
anchor_collection_date does not look like a valid generated collection date
The anchor date does not match the generated collection calendar.
Likely causes:
- the anchor date is wrong
- the anchor date was a Saturday but the script did not identify that week as bank-holiday-shifted
- the bank holiday shift rule needs changing
- the bank holiday region is wrong
Target date is not a collection date
The date command was given a date that is not an actual collection date.
Use Friday for normal weeks.
Use Saturday for bank-holiday-shifted weeks.
weeks must be no more than 12
The schedule command was asked for more than 12 weeks.
Use:
scripts/bin_vader.py schedule 12
No swap found with id
The swap ID does not exist.
List swaps:
scripts/bin_vader.py list-swaps
Design principle
The script is the source of truth.
The language model may:
- explain results
- format messages
- interpret activated requests
- pass commands to the script
The language model must not:
- invent rota state
- guess bank holiday shifts
- silently create swaps
- remember swaps only in conversation
- override the deterministic script without a recorded manual override