Quality, fail-closed guards, debug hex, and TDD (no protocol change) - #2
Open
matthewhand wants to merge 15 commits into
Open
Quality, fail-closed guards, debug hex, and TDD (no protocol change)#2matthewhand wants to merge 15 commits into
matthewhand wants to merge 15 commits into
Conversation
Accept BX-prefixed aliases, unlock c001 when present, and let start() select outlet 1 or 2. Document the BX2 payload and pitfalls in docs/BX2.md with no device addresses or site-specific names.
There was a problem hiding this comment.
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
force-pushed
the
pr1/bx2-hose-byte1
branch
2 times, most recently
from
August 26, 2026 03:57
a688baf to
6c4086f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #1. No protocol change. Same f006 map as PR1:
--zone 1[0x01, 0x00, 0x00, minutes]010000NN--zone 2[0x01, 0x01, 0x00, minutes]010100NN[0x00, 0x00, 0x00, 0x00]00000000Quality, guards, debug, and TDD around that map. It does not change the start bytes.
Discovery (from #1, extracted here for tests): exact allowlist
Tap TimerandBX2.if alias not in self.accepted_aliases: return None. Empty alias does not match.HOLMAN_ACCEPTED_ALIASESadds 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_UUIDSis a comma-separated full replace (unset = defaults; set = exactly those UUIDs). Discovery and connect-time use the same resolved list. Constructoraccepted_aliases/service_uuidswins 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 withoutgatt/ 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.pyplus unittest, and extracts exact-alias defaults and the service-UUID filter so they are testable withoutgatt. Constructor-vs-env resolution lives inaliases.pyfor the same reason:holman.pyneedsgatt.Quality
holman/payload.py:manual_payload,tap_for_zone,tap_name,clamp_runtime. No BLE imports.TAP_SPRINKLER/TAP_HOSE,ZONE_NAMESSprinkler / Hose.holman/aliases.py:get_default_aliases()/alias_accepted()so exactTap Timer+BX2defaults and the env add are testable withoutgatt.get_default_service_uuids()so the CO3015 / CO3012 / CO3011 defaults andHOLMAN_SERVICE_UUIDSfull replace are testable withoutgatt.resolve_aliases()/resolve_service_uuids()so constructor-wins-over-env is testable without importingholman.holman.TapTimerManager/TapTimercall those resolvers instead of inlinedif None.TapTimer.start/stopcall the shared builder. One place defines the bytes.Guards
ValueErrorand fails closed. No moremax(1, min(zone, 2))clamp that could turn a bad call into Sprinkler.RuntimeErrorinstead of a silent no-op.holmanctl --startcatchesValueError, printsstart refused: …, exits 2.Tap,BX,Holman BX2,BTX2, empty) fail closed.Debug
logging.getLogger(holman.holman)debug line on every f006 write: hex, zone, name.holmanctl --debugturns onlogging.DEBUG.--debug,--startprintspayload=01010005(Hose, 5 minutes) so a dry run is diagnosable from the CLI line.TDD
tests/test_payload.pyloads the shipped modules by path. It does not copy the builder.Tests, no Bluetooth:
test_start_sprinkler01 00 00 03test_start_hose01 01 00 01test_hose_payload_bytesexact[0x01, 0x01, 0x00, 5]tap_nameSprinkler / Hose('Tap Timer', 'BX2')HOLMAN_SERVICE_UUIDSreplaces the listresolve_*→Tap Timer+BX2; defaults include CO3011HOLMAN_ACCEPTED_ALIASESappends viaresolve_aliasesHOLMAN_SERVICE_UUIDSfull-replaces viaresolve_service_uuids(CO3015 / CO3012 gone)resolve_aliases(('BX2',))wins when env isNoperesolve_service_uuids((CO3011,))wins when env is an unknown UUIDalias_accepted('BX2'); CO3011 isc521f000-0d70-4d4f-8e43-40d84c50ab38Out of scope
gattis still required to importholman.holman; tests deliberately do not import it.Test plan
python3 -m unittest discover -s tests -vpython3 -m py_compile holman/*.py holmanctl.py tests/test_payload.pyholmanctl --helpshows--debugand--zone 1Sprinkler /--zone 2Hoseholmanctl --start …with an unknown--zoneprintsstart refusedand does not writeholmanctl --debug --start … --zone 2logs010100NN