pca_file + v2 client: area flags + Area-N fallback
SetupData side (clsHAC.cs:3020-3038): five contiguous bool[8] arrays immediately after ExitDelay carry per-area config flags. Offsets: 1787..1794: EntryChime 1795..1802: QuickArm 1803..1810: AutoBypass 1811..1818: AllOnForAlarm 1819..1826: TroubleBeep Verified against live fixture: area 1 shows real homeowner choices (QuickArm + AllOnForAlarm enabled, others off), unused areas 2-8 carry the panel defaults (EntryChime/AutoBypass/TroubleBeep on by default). PerimeterChime and AudibleExitDelay aren't in this contiguous block — they live past FlashLightNum, HouseCodes flags, and 6 TimeClock When-structs. Deferred. New PcaAccount fields: area_entry_chime, area_quick_arm, area_auto_bypass, area_all_on_for_alarm, area_trouble_beep — all dict[int, bool]. MockAreaState gains the same five fields. They aren't carried in the Properties reply on the wire (the OL2 message format doesn't have them), so they live on MockState for snapshots and any future SetupData-aware code, but don't surface through HA discovery yet. v2 client list_area_names fallback: when the Properties walk turns up no named areas (common — most homes don't name them), synthesize "Area 1".."Area 8" so HA's _discover_areas has slots to walk. Mirrors the v1 adapter behaviour exactly. Knock-on win in the live-fixture HA test: area 1 now reaches coordinator.data.areas with its configured 60s/90s delays from SetupData, end-to-end through .pca → MockState → wire Properties → HA's AreaProperties parser. Full suite: 499 passed, 1 skipped.
This commit is contained in:
parent
501686795b
commit
994608a4f6
@ -730,10 +730,22 @@ class OmniClient:
|
||||
)
|
||||
|
||||
async def list_area_names(self) -> dict[int, str]:
|
||||
return await self._walk_named_objects(
|
||||
"""Return area names, falling back to "Area N" when none are named.
|
||||
|
||||
Most installs assign no user-visible name to areas — single-area
|
||||
homes don't bother, and even multi-area installs commonly leave
|
||||
area names blank. HA needs *something* to label each area entity,
|
||||
so we synthesize "Area 1".."Area 8" (the Omni Pro II cap) when
|
||||
the Properties walk returns no names. Mirrors the v1 adapter's
|
||||
list_area_names fallback in omni_pca.v1.adapter.
|
||||
"""
|
||||
named = await self._walk_named_objects(
|
||||
ObjectType.AREA,
|
||||
lambda r: (r.index, r.name) if isinstance(r, AreaProperties) else None,
|
||||
)
|
||||
if named:
|
||||
return named
|
||||
return {i: f"Area {i}" for i in range(1, 9)}
|
||||
|
||||
async def subscribe(
|
||||
self, callback: Callable[[Message], Awaitable[None]]
|
||||
|
||||
@ -147,6 +147,14 @@ class MockAreaState:
|
||||
entry_delay: int = 30 # seconds; configured grace period after a door opens
|
||||
exit_delay: int = 60 # seconds; configured grace period after arming
|
||||
enabled: bool = True # whether this area is part of NumAreasUsed
|
||||
# Boolean configuration flags — not on the wire (the Properties
|
||||
# reply doesn't carry them); kept here for snapshots from .pca and
|
||||
# any future SetupData-aware code paths.
|
||||
entry_chime: bool = False
|
||||
quick_arm: bool = False
|
||||
auto_bypass: bool = False
|
||||
all_on_for_alarm: bool = False
|
||||
trouble_beep: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -346,6 +354,11 @@ class MockState:
|
||||
entry_delay=acct.area_entry_delays.get(i, 30),
|
||||
exit_delay=acct.area_exit_delays.get(i, 60),
|
||||
enabled=i <= acct.num_areas_used,
|
||||
entry_chime=acct.area_entry_chime.get(i, False),
|
||||
quick_arm=acct.area_quick_arm.get(i, False),
|
||||
auto_bypass=acct.area_auto_bypass.get(i, False),
|
||||
all_on_for_alarm=acct.area_all_on_for_alarm.get(i, False),
|
||||
trouble_beep=acct.area_trouble_beep.get(i, False),
|
||||
)
|
||||
for i in sorted(in_use_areas)
|
||||
},
|
||||
|
||||
@ -249,6 +249,25 @@ class PcaAccount:
|
||||
area_entry_delays: dict[int, int] = field(default_factory=dict)
|
||||
area_exit_delays: dict[int, int] = field(default_factory=dict)
|
||||
|
||||
# Per-area boolean configuration flags from SetupData user section,
|
||||
# five contiguous bool[8] arrays at offset 1787..1826
|
||||
# (clsHAC.cs:3020-3038). Keys are 1-based area numbers.
|
||||
#
|
||||
# entry_chime — chime keypads when entry-delay zones trip
|
||||
# quick_arm — allow arming without a code
|
||||
# auto_bypass — silently bypass not-ready zones on arm
|
||||
# all_on_for_alarm — fire every output when any alarm trips
|
||||
# trouble_beep — beep keypads on a non-alarm trouble condition
|
||||
#
|
||||
# PerimeterChime and AudibleExitDelay are NOT in this contiguous
|
||||
# block — they live deeper in the user section past FlashLightNum,
|
||||
# HouseCodes flags, and 6 TimeClock When-structs.
|
||||
area_entry_chime: dict[int, bool] = field(default_factory=dict)
|
||||
area_quick_arm: dict[int, bool] = field(default_factory=dict)
|
||||
area_auto_bypass: dict[int, bool] = field(default_factory=dict)
|
||||
area_all_on_for_alarm: dict[int, bool] = field(default_factory=dict)
|
||||
area_trouble_beep: dict[int, bool] = field(default_factory=dict)
|
||||
|
||||
# Panel-wide TempFormat (enuTempFormat: 1=Fahrenheit, 2=Celsius)
|
||||
# and NumAreasUsed (count of armable security areas — 1 for a
|
||||
# typical single-area home install, up to numAreas=8 on Omni Pro II).
|
||||
@ -310,6 +329,15 @@ _CAP_OMNI_PRO_II: dict[str, int] = {
|
||||
#
|
||||
"entryDelayOffset": 1771,
|
||||
"exitDelayOffset": 1779,
|
||||
# Five contiguous bool[8] flag arrays immediately follow ExitDelay
|
||||
# (clsHAC.cs:3020-3038). PerimeterChime and AudibleExitDelay are
|
||||
# NOT contiguous — they live later, past HighSecurity, FreezeAlarm,
|
||||
# FlashLightNum, HouseCodes flags, and 6 TimeClock When-structs.
|
||||
"entryChimeOffset": 1787,
|
||||
"quickArmOffset": 1795,
|
||||
"autoBypassOffset": 1803,
|
||||
"allOnForAlarmOffset": 1811,
|
||||
"troubleBeepOffset": 1819,
|
||||
|
||||
# Installer section begins at byte 2560 (clsCapOMNI_PRO_II.instSetupStart).
|
||||
# Layout for OMNI_PRO_II observed empirically against the live fixture
|
||||
@ -466,6 +494,11 @@ class _ConnectionWalk:
|
||||
zone_areas: dict[int, int] = field(default_factory=dict)
|
||||
area_entry_delays: dict[int, int] = field(default_factory=dict)
|
||||
area_exit_delays: dict[int, int] = field(default_factory=dict)
|
||||
area_entry_chime: dict[int, bool] = field(default_factory=dict)
|
||||
area_quick_arm: dict[int, bool] = field(default_factory=dict)
|
||||
area_auto_bypass: dict[int, bool] = field(default_factory=dict)
|
||||
area_all_on_for_alarm: dict[int, bool] = field(default_factory=dict)
|
||||
area_trouble_beep: dict[int, bool] = field(default_factory=dict)
|
||||
temp_format: int = 0
|
||||
num_areas_used: int = 0
|
||||
|
||||
@ -524,17 +557,22 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk:
|
||||
|
||||
# Per-area entry/exit delays from the user section.
|
||||
num_areas = cap.get("max_areas", 0)
|
||||
entry_off = cap.get("entryDelayOffset")
|
||||
area_entry_delays: dict[int, int] = {}
|
||||
if entry_off is not None and entry_off + num_areas <= len(setup_data):
|
||||
for slot in range(1, num_areas + 1):
|
||||
area_entry_delays[slot] = setup_data[entry_off + slot - 1]
|
||||
def _read_area_byte_array(offset_key: str) -> dict[int, int]:
|
||||
off = cap.get(offset_key)
|
||||
if off is None or off + num_areas > len(setup_data):
|
||||
return {}
|
||||
return {i: setup_data[off + i - 1] for i in range(1, num_areas + 1)}
|
||||
|
||||
exit_off = cap.get("exitDelayOffset")
|
||||
area_exit_delays: dict[int, int] = {}
|
||||
if exit_off is not None and exit_off + num_areas <= len(setup_data):
|
||||
for slot in range(1, num_areas + 1):
|
||||
area_exit_delays[slot] = setup_data[exit_off + slot - 1]
|
||||
def _read_area_bool_array(offset_key: str) -> dict[int, bool]:
|
||||
return {i: bool(b) for i, b in _read_area_byte_array(offset_key).items()}
|
||||
|
||||
area_entry_delays = _read_area_byte_array("entryDelayOffset")
|
||||
area_exit_delays = _read_area_byte_array("exitDelayOffset")
|
||||
area_entry_chime = _read_area_bool_array("entryChimeOffset")
|
||||
area_quick_arm = _read_area_bool_array("quickArmOffset")
|
||||
area_auto_bypass = _read_area_bool_array("autoBypassOffset")
|
||||
area_all_on_for_alarm = _read_area_bool_array("allOnForAlarmOffset")
|
||||
area_trouble_beep = _read_area_bool_array("troubleBeepOffset")
|
||||
|
||||
# Scalars from the installer section.
|
||||
tf_off = cap.get("tempFormatOffset")
|
||||
@ -584,6 +622,11 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk:
|
||||
zone_areas=zone_areas,
|
||||
area_entry_delays=area_entry_delays,
|
||||
area_exit_delays=area_exit_delays,
|
||||
area_entry_chime=area_entry_chime,
|
||||
area_quick_arm=area_quick_arm,
|
||||
area_auto_bypass=area_auto_bypass,
|
||||
area_all_on_for_alarm=area_all_on_for_alarm,
|
||||
area_trouble_beep=area_trouble_beep,
|
||||
temp_format=temp_format,
|
||||
num_areas_used=num_areas_used,
|
||||
)
|
||||
@ -677,6 +720,11 @@ def parse_pca_file(path_or_bytes: str | os.PathLike[str] | bytes, key: int) -> P
|
||||
account.zone_areas = walk.zone_areas
|
||||
account.area_entry_delays = walk.area_entry_delays
|
||||
account.area_exit_delays = walk.area_exit_delays
|
||||
account.area_entry_chime = walk.area_entry_chime
|
||||
account.area_quick_arm = walk.area_quick_arm
|
||||
account.area_auto_bypass = walk.area_auto_bypass
|
||||
account.area_all_on_for_alarm = walk.area_all_on_for_alarm
|
||||
account.area_trouble_beep = walk.area_trouble_beep
|
||||
account.temp_format = walk.temp_format
|
||||
account.num_areas_used = walk.num_areas_used
|
||||
|
||||
|
||||
@ -166,13 +166,14 @@ async def test_mockpanel_from_pca_drives_full_ha_discovery(
|
||||
# zone surfaces as area=1 through the wire Properties reply.
|
||||
for idx, z in coordinator.data.zones.items():
|
||||
assert z.area == 1, f"zone {idx} expected area=1 got {z.area}"
|
||||
# Note: coordinator.data.areas is empty here because this
|
||||
# fixture has no user-assigned area names and the v2 client's
|
||||
# list_area_names() only returns named areas — that's a
|
||||
# separate HA discovery concern. The mock *does* serve the
|
||||
# delays in Properties replies; verify directly:
|
||||
# (see test_e2e_program_echo.py for the MockState side, which
|
||||
# has Area 1 with the correct entry_delay=60 / exit_delay=90).
|
||||
# Areas: the live fixture has no user-assigned area names
|
||||
# but the v2 client's list_area_names now falls back to
|
||||
# "Area 1".."Area 8". HA's _discover_areas then enumerates
|
||||
# each, walks the Properties reply, and lands the configured
|
||||
# entry/exit delays from SetupData.
|
||||
assert 1 in coordinator.data.areas
|
||||
assert coordinator.data.areas[1].entry_delay == 60
|
||||
assert coordinator.data.areas[1].exit_delay == 90
|
||||
finally:
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -361,6 +361,21 @@ async def test_mockstate_from_pca_serves_real_panel_programs() -> None:
|
||||
assert acct.num_areas_used == 1
|
||||
assert acct.area_entry_delays[1] == 60
|
||||
assert acct.area_exit_delays[1] == 90
|
||||
|
||||
# Area-1 boolean flags (real homeowner-configured values):
|
||||
# EntryChime OFF (no keypad chime on entry)
|
||||
# QuickArm ON (arming without a code)
|
||||
# AutoBypass OFF
|
||||
# AllOnForAlarm ON
|
||||
# TroubleBeep OFF
|
||||
assert acct.area_entry_chime[1] is False
|
||||
assert acct.area_quick_arm[1] is True
|
||||
assert acct.area_auto_bypass[1] is False
|
||||
assert acct.area_all_on_for_alarm[1] is True
|
||||
assert acct.area_trouble_beep[1] is False
|
||||
# And the values flowed through MockState.
|
||||
assert state.areas[1].quick_arm is True
|
||||
assert state.areas[1].entry_chime is False
|
||||
assert state.zones[1].name == "GARAGE ENTRY"
|
||||
assert state.units[1].name == "ROOM ONE"
|
||||
assert state.thermostats[1].name == "DOWNSTAIRS"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user