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]:
|
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,
|
ObjectType.AREA,
|
||||||
lambda r: (r.index, r.name) if isinstance(r, AreaProperties) else None,
|
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(
|
async def subscribe(
|
||||||
self, callback: Callable[[Message], Awaitable[None]]
|
self, callback: Callable[[Message], Awaitable[None]]
|
||||||
|
|||||||
@ -147,6 +147,14 @@ class MockAreaState:
|
|||||||
entry_delay: int = 30 # seconds; configured grace period after a door opens
|
entry_delay: int = 30 # seconds; configured grace period after a door opens
|
||||||
exit_delay: int = 60 # seconds; configured grace period after arming
|
exit_delay: int = 60 # seconds; configured grace period after arming
|
||||||
enabled: bool = True # whether this area is part of NumAreasUsed
|
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
|
@dataclass
|
||||||
@ -346,6 +354,11 @@ class MockState:
|
|||||||
entry_delay=acct.area_entry_delays.get(i, 30),
|
entry_delay=acct.area_entry_delays.get(i, 30),
|
||||||
exit_delay=acct.area_exit_delays.get(i, 60),
|
exit_delay=acct.area_exit_delays.get(i, 60),
|
||||||
enabled=i <= acct.num_areas_used,
|
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)
|
for i in sorted(in_use_areas)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -249,6 +249,25 @@ class PcaAccount:
|
|||||||
area_entry_delays: 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_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)
|
# Panel-wide TempFormat (enuTempFormat: 1=Fahrenheit, 2=Celsius)
|
||||||
# and NumAreasUsed (count of armable security areas — 1 for a
|
# and NumAreasUsed (count of armable security areas — 1 for a
|
||||||
# typical single-area home install, up to numAreas=8 on Omni Pro II).
|
# 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,
|
"entryDelayOffset": 1771,
|
||||||
"exitDelayOffset": 1779,
|
"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).
|
# Installer section begins at byte 2560 (clsCapOMNI_PRO_II.instSetupStart).
|
||||||
# Layout for OMNI_PRO_II observed empirically against the live fixture
|
# 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)
|
zone_areas: dict[int, int] = field(default_factory=dict)
|
||||||
area_entry_delays: 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_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
|
temp_format: int = 0
|
||||||
num_areas_used: 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.
|
# Per-area entry/exit delays from the user section.
|
||||||
num_areas = cap.get("max_areas", 0)
|
num_areas = cap.get("max_areas", 0)
|
||||||
entry_off = cap.get("entryDelayOffset")
|
def _read_area_byte_array(offset_key: str) -> dict[int, int]:
|
||||||
area_entry_delays: dict[int, int] = {}
|
off = cap.get(offset_key)
|
||||||
if entry_off is not None and entry_off + num_areas <= len(setup_data):
|
if off is None or off + num_areas > len(setup_data):
|
||||||
for slot in range(1, num_areas + 1):
|
return {}
|
||||||
area_entry_delays[slot] = setup_data[entry_off + slot - 1]
|
return {i: setup_data[off + i - 1] for i in range(1, num_areas + 1)}
|
||||||
|
|
||||||
exit_off = cap.get("exitDelayOffset")
|
def _read_area_bool_array(offset_key: str) -> dict[int, bool]:
|
||||||
area_exit_delays: dict[int, int] = {}
|
return {i: bool(b) for i, b in _read_area_byte_array(offset_key).items()}
|
||||||
if exit_off is not None and exit_off + num_areas <= len(setup_data):
|
|
||||||
for slot in range(1, num_areas + 1):
|
area_entry_delays = _read_area_byte_array("entryDelayOffset")
|
||||||
area_exit_delays[slot] = setup_data[exit_off + slot - 1]
|
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.
|
# Scalars from the installer section.
|
||||||
tf_off = cap.get("tempFormatOffset")
|
tf_off = cap.get("tempFormatOffset")
|
||||||
@ -584,6 +622,11 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk:
|
|||||||
zone_areas=zone_areas,
|
zone_areas=zone_areas,
|
||||||
area_entry_delays=area_entry_delays,
|
area_entry_delays=area_entry_delays,
|
||||||
area_exit_delays=area_exit_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,
|
temp_format=temp_format,
|
||||||
num_areas_used=num_areas_used,
|
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.zone_areas = walk.zone_areas
|
||||||
account.area_entry_delays = walk.area_entry_delays
|
account.area_entry_delays = walk.area_entry_delays
|
||||||
account.area_exit_delays = walk.area_exit_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.temp_format = walk.temp_format
|
||||||
account.num_areas_used = walk.num_areas_used
|
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.
|
# zone surfaces as area=1 through the wire Properties reply.
|
||||||
for idx, z in coordinator.data.zones.items():
|
for idx, z in coordinator.data.zones.items():
|
||||||
assert z.area == 1, f"zone {idx} expected area=1 got {z.area}"
|
assert z.area == 1, f"zone {idx} expected area=1 got {z.area}"
|
||||||
# Note: coordinator.data.areas is empty here because this
|
# Areas: the live fixture has no user-assigned area names
|
||||||
# fixture has no user-assigned area names and the v2 client's
|
# but the v2 client's list_area_names now falls back to
|
||||||
# list_area_names() only returns named areas — that's a
|
# "Area 1".."Area 8". HA's _discover_areas then enumerates
|
||||||
# separate HA discovery concern. The mock *does* serve the
|
# each, walks the Properties reply, and lands the configured
|
||||||
# delays in Properties replies; verify directly:
|
# entry/exit delays from SetupData.
|
||||||
# (see test_e2e_program_echo.py for the MockState side, which
|
assert 1 in coordinator.data.areas
|
||||||
# has Area 1 with the correct entry_delay=60 / exit_delay=90).
|
assert coordinator.data.areas[1].entry_delay == 60
|
||||||
|
assert coordinator.data.areas[1].exit_delay == 90
|
||||||
finally:
|
finally:
|
||||||
await hass.config_entries.async_unload(entry.entry_id)
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
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.num_areas_used == 1
|
||||||
assert acct.area_entry_delays[1] == 60
|
assert acct.area_entry_delays[1] == 60
|
||||||
assert acct.area_exit_delays[1] == 90
|
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.zones[1].name == "GARAGE ENTRY"
|
||||||
assert state.units[1].name == "ROOM ONE"
|
assert state.units[1].name == "ROOM ONE"
|
||||||
assert state.thermostats[1].name == "DOWNSTAIRS"
|
assert state.thermostats[1].name == "DOWNSTAIRS"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user