Skip to content

Quality, fail-closed guards, debug hex, and TDD (no protocol change) - #2

Open
matthewhand wants to merge 15 commits into
pr1/bx2-hose-byte1from
pr2/quality-guards-tdd
Open

Quality, fail-closed guards, debug hex, and TDD (no protocol change)#2
matthewhand wants to merge 15 commits into
pr1/bx2-hose-byte1from
pr2/quality-guards-tdd

Conversation

@matthewhand

@matthewhand matthewhand commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Stacked on #1. No protocol change. Same f006 map as PR1:

CLI Name Tap Start Hex
--zone 1 Sprinkler 0 [0x01, 0x00, 0x00, minutes] 010000NN
--zone 2 Hose 1 [0x01, 0x01, 0x00, minutes] 010100NN
Stop [0x00, 0x00, 0x00, 0x00] 00000000

Quality, guards, debug, and TDD around that map. It does not change the start bytes.

Discovery (from #1, extracted here for tests): exact allowlist Tap Timer and BX2. if alias not in self.accepted_aliases: return None. Empty alias does not match. HOLMAN_ACCEPTED_ALIASES adds another exact name. Unknown names fail closed.

Vendor service UUIDs (from #1, extracted here for tests): defaults CO3015 / CO3012 / CO3011 (BX2 on-air is CO3011 / c521f000-…). HOLMAN_SERVICE_UUIDS is a comma-separated full replace (unset = defaults; set = exactly those UUIDs). Discovery and connect-time use the same resolved list. Constructor accepted_aliases / service_uuids wins over env (resolve_aliases / resolve_service_uuids).

Why

PR1 put the byte-1 tap map inside TapTimer.start. The builder still lived next to BLE, so it could not be tested without gatt / BlueZ, invalid zones were silently clamped to 1..2, and there was no hex log of what actually went on the wire.

This PR extracts a pure payload.py plus unittest, and extracts exact-alias defaults and the service-UUID filter so they are testable without gatt. Constructor-vs-env resolution lives in aliases.py for the same reason: holman.py needs gatt.

Quality

  • New holman/payload.py: manual_payload, tap_for_zone, tap_name, clamp_runtime. No BLE imports. TAP_SPRINKLER / TAP_HOSE, ZONE_NAMES Sprinkler / Hose.
  • New holman/aliases.py: get_default_aliases() / alias_accepted() so exact Tap Timer + BX2 defaults and the env add are testable without gatt. get_default_service_uuids() so the CO3015 / CO3012 / CO3011 defaults and HOLMAN_SERVICE_UUIDS full replace are testable without gatt. resolve_aliases() / resolve_service_uuids() so constructor-wins-over-env is testable without importing holman.holman.
  • TapTimerManager / TapTimer call those resolvers instead of inlined if None.
  • TapTimer.start / stop call the shared builder. One place defines the bytes.

Guards

  • Unknown zone (0, 3, …) raises ValueError and fails closed. No more max(1, min(zone, 2)) clamp that could turn a bad call into Sprinkler.
  • Runtime still clamped to 1..255.
  • Missing f006 characteristic raises RuntimeError instead of a silent no-op.
  • holmanctl --start catches ValueError, prints start refused: …, exits 2.
  • Unknown advertised names (Tap, BX, Holman BX2, BTX2, empty) fail closed.

Debug

  • logging.getLogger(holman.holman) debug line on every f006 write: hex, zone, name.
  • holmanctl --debug turns on logging.DEBUG.
  • Even without --debug, --start prints payload=01010005 (Hose, 5 minutes) so a dry run is diagnosable from the CLI line.

TDD

tests/test_payload.py loads the shipped modules by path. It does not copy the builder.

python3 -m unittest discover -s tests -v

Tests, no Bluetooth:

  • test_start_sprinkler 01 00 00 03
  • test_start_hose 01 01 00 01
  • test_hose_payload_bytes exact [0x01, 0x01, 0x00, 5]
  • Stop all-zero
  • Runtime clamp 0→1, 999→255
  • Invalid zone fails closed
  • tap_name Sprinkler / Hose
  • Alias default exact ('Tap Timer', 'BX2')
  • Env adds another exact name
  • Unknown names fail closed
  • Default service UUID list includes CO3011
  • HOLMAN_SERVICE_UUIDS replaces the list
  • Unknown UUID is not in the default unless the env is set
  • Unset env via resolve_*Tap Timer + BX2; defaults include CO3011
  • HOLMAN_ACCEPTED_ALIASES appends via resolve_aliases
  • HOLMAN_SERVICE_UUIDS full-replaces via resolve_service_uuids (CO3015 / CO3012 gone)
  • resolve_aliases(('BX2',)) wins when env is Nope
  • resolve_service_uuids((CO3011,)) wins when env is an unknown UUID
  • Device data: alias_accepted('BX2'); CO3011 is c521f000-0d70-4d4f-8e43-40d84c50ab38

Out of scope

Test plan

  • python3 -m unittest discover -s tests -v
  • python3 -m py_compile holman/*.py holmanctl.py tests/test_payload.py
  • holmanctl --help shows --debug and --zone 1 Sprinkler / --zone 2 Hose
  • holmanctl --start … with an unknown --zone prints start refused and does not write
  • holmanctl --debug --start … --zone 2 logs 010100NN

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR successfully refactors the payload building logic into testable modules with fail-closed validation for invalid zones. The implementation is sound with proper error handling, debug logging, and comprehensive test coverage. The changes maintain backward compatibility while improving code quality and safety.

Key improvements:

  • Extracted payload building into pure functions with no BLE dependencies
  • Replaced silent zone clamping with fail-closed ValueError for invalid zones
  • Added RuntimeError when f006 characteristic is missing (previously silent no-op)
  • Added debug hex logging for f006 writes
  • Comprehensive unit tests (9 tests) that don't require Bluetooth

No blocking defects found. The code is ready to merge.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

@matthewhand
matthewhand force-pushed the pr1/bx2-hose-byte1 branch 2 times, most recently from a688baf to 6c4086f Compare August 26, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant