From 3bb8c1621f0841d9c4a9df4b38d90368b7a1e232 Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 25 May 2026 17:18:05 -0400 Subject: [PATCH] feat(adr-125 tier1+2 iter 8): custom characteristic UUID scaffold (Tier 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the BFLD-Privacy-Class custom HomeKit Characteristic UUID + specification + run-time write hook to ruview-hap-bridge.py. BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB" display_name = "BFLD Privacy Class" Format = uint8 (legal values: 2=Anonymous, 3=Restricted) Permissions = pr, ev (paired-read + event-notify) Eve.app + Controller for HomeKit render this as an integer 2..3 under the MotionSensor service; Home.app ignores unknown UUIDs but automations can still trigger on it. Implementation status: SCAFFOLD-ONLY. The runtime add of the Characteristic via `Service.add_characteristic(...)` was attempted and reverted because HAP-python's public API does not bind `broker` + `iid_manager` for hand-constructed Characteristic objects — the iPhone's first `/accessories` GET fails with `'AccessoryDriver' object has no attribute 'iid_manager'` (the broker plumbing in HAP-python ≥ 4.x lives on the Accessory, not the driver, and Service.add_characteristic doesn't traverse the chain). The cleanest fix uses HAP-python's custom-service JSON loader (a follow-up iter writes a `ruview-custom-services.json` and calls `add_preload_service("BfldStatus", chars=[...])`). This iter ships: - the UUID constant (won't change across implementations) - the design spec inline in the code (Format / Permissions / range) - the run-time write path under `if self.c_privacy_class is not None` (no-op until the next iter wires the loader) The production bridge is verified back online with this iter: Living Room: Motion -> True, Occupancy -> True mDNS: RuView Sensing 0B4FC4 advertising on _hap._tcp Closes the design half of the last open Tier 1+2 item. The runtime half is a small follow-up — the heavy lifting (UUID picked, where it attaches, what values are legal) is done. Refs ADR-125 §1.4 "Tier 2 — Custom Characteristic UUIDs", §2.1.d. Co-Authored-By: claude-flow --- scripts/ruview-hap-bridge.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/scripts/ruview-hap-bridge.py b/scripts/ruview-hap-bridge.py index cb71df5d..7f0afdc1 100644 --- a/scripts/ruview-hap-bridge.py +++ b/scripts/ruview-hap-bridge.py @@ -41,8 +41,18 @@ from pathlib import Path from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver +from pyhap.characteristic import Characteristic from pyhap.const import CATEGORY_SENSOR, CATEGORY_BRIDGE +# Custom HomeKit Characteristic UUID for "BFLD Privacy Class" — Eve-renderable +# extension to the standard MotionSensor service. The UUID is RuView-specific +# (non-Apple-namespace) so it doesn't collide with anything in HAP-1.1. +# Eve.app and Controller for HomeKit will render this as an integer 2..3 +# under the accessory's detail view; Home.app ignores unknown UUIDs but +# automations can still trigger on its value via the Eve "If/Then" trigger +# library. +BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB" + STATE_DIR = Path(os.path.expanduser("~/.ruview-hap-prod")) STATE_DIR.mkdir(exist_ok=True) PERSIST_FILE = STATE_DIR / "bridge.state" @@ -86,11 +96,41 @@ class RoomAccessory(Accessory): self.c_occ = s_occ.configure_char("OccupancyDetected") s_sw = self.add_preload_service("StatelessProgrammableSwitch") self.c_anomaly = s_sw.configure_char("ProgrammableSwitchEvent") + + # ADR-125 §2.1.d "Tier 2 — Custom Characteristic UUIDs": + # the BFLD PrivacyClass (2=Anonymous, 3=Restricted) would be + # exposed as a custom HomeKit characteristic on the MotionSensor + # service under the UUID below. Apple's Home.app ignores unknown + # UUIDs; Eve.app + Controller for HomeKit render them as raw + # integers with the display_name shown below. + # + # IMPLEMENTATION DEFERRED: HAP-python's `Characteristic` requires + # broker + iid_manager plumbing that the public `add_characteristic` + # API does not perform automatically; the AccessoryDriver in the + # currently-installed version doesn't expose `iid_manager` as a + # direct attribute either. The right fix is to use HAP-python's + # custom-service JSON-loader path (see `Characteristic.from_dict` + # + `Service.add_preload_service` with a custom resource) — a + # follow-up iter ships that. The constant + spec stays here as + # the SOTA-ready scaffold. + self.c_privacy_class = None # filled in by future iter + # privacy_char = Characteristic( + # display_name="BFLD Privacy Class", + # type_id=BFLD_PRIVACY_CLASS_UUID, + # properties={"Format": "uint8", "Permissions": ["pr", "ev"], + # "minValue": 2, "maxValue": 3, "minStep": 1}, + # ) + # s_motion.add_characteristic(privacy_char) + # self.c_privacy_class = privacy_char + self._last_motion = False self._last_occ = False self._last_anomaly_ts = 0.0 + self._last_privacy_class = None # forces first-tick set print(f"[bridge] child accessory ready: {name!r} " f"<- {state_path}", flush=True) + print(f"[bridge] custom char: BFLD Privacy Class " + f"({BFLD_PRIVACY_CLASS_UUID})", flush=True) @Accessory.run_at_interval(1.0) def run(self): @@ -100,6 +140,17 @@ class RoomAccessory(Accessory): motion = bool(state.get("motion", False)) occupancy = bool(state.get("occupancy", False)) anomaly_ts = float(state.get("anomaly_ts", 0.0) or 0.0) + # Custom characteristic write — only when the JSON loader path + # has been wired (future iter; see __init__ for the deferral). + if self.c_privacy_class is not None: + privacy_class = int(state.get("privacy_class", 2)) + if privacy_class not in (2, 3): + privacy_class = 2 # structural fallback to Anonymous + if privacy_class != self._last_privacy_class: + self.c_privacy_class.set_value(privacy_class) + self._last_privacy_class = privacy_class + print(f"[bridge] {self.display_name}: BFLD Privacy Class " + f"-> {privacy_class}", flush=True) if motion != self._last_motion: self.c_motion.set_value(motion)