pca_file: PerimeterChime/AudibleExitDelay, DST, unit type+area, code PINs
Four more SetupData fields landed in one pass. The user-section walk
past the previously-mapped 5 contiguous area flags continues with 70
bytes of intervening config (HighSecurity/FreezeAlarm/FlashLightNum/
HouseCodes flags × 32 / 6 TimeClock When-structs / Latitude/Longitude/
TimeZone/AnnounceAlarms) to reach:
1897..1904: PerimeterChime[1..8] (bool[8])
1905..1912: AudibleExitDelay[1..8] (bool[8])
1913..1916: DSTStartMonth/Week, DSTEndMonth/Week (4 scalar bytes)
Live fixture DST decodes as US-standard (March / 2nd Sunday →
November / 1st Sunday). Area-1 PerimeterChime is OFF (homeowner
disabled), the panel default for unused areas 2-8 is ON.
Unit type + area assignment, derived from CAP index ranges and the
AreaGroups bitmap arrays at installer offsets 3035..3105
(clsHAC.cs:3242-3289):
X10 units (1..256) → enuOL2UnitType.Standard (1),
16 units per AreaGroups byte
ExpEnc (257..384) → Output (13), 4 units per byte
VoltOut (385..392) → Output (13), 1 byte per unit
FlagOut (393..511) → Flag (12), 8 flags per byte
The X10 sub-types (Standard/Extended/HLC/UPB/ZWave/…) collapse to
Standard since deriving them needs the HouseCodes EnableExtCode table
which we don't decode yet. Live fixture all-511-units classify
correctly: 256 X10 + 128 ExpEnc + 8 VoltOut + 119 FlagOut.
Unit areas are 8-bit membership bitmasks. The live fixture has 0xFF
everywhere ("panel default — all 8 areas"); from_pca normalises that
to 0x01 ("area 1 only") so the mock's Properties reply gives HA a
single sensible area instead of bit-set noise.
Code PINs (offset 383, 99 × 14-byte entries). Per-entry layout:
bytes 0..1: PIN (BE u16; plain 4-digit 0..9999)
byte 2: Authority (enuCodeAuthority: 0=Disabled, 1=User,
2=Manager, 3=Installer)
byte 3: Areas bitmask
bytes 4..13: WhenOn + WhenOff (2 × clsWhen)
PINs are PII — ``PcaAccount.code_pins`` is marked ``repr=False`` so
a stray ``print(acct)`` can never leak them into logs. They aren't
auto-threaded to MockState.user_codes either; tests set their own
PINs explicitly. Live-fixture decode is sane: COMPUTER=4932/User,
HOMEOWNER=1234/User, Kevin=3411/User, Debra=0000/Manager, etc.
MockAreaState gains perimeter_chime + audible_exit_delay.
MockUnitState gains unit_type + areas (and the Properties reply
serves the configured values now).
Full suite: 499 passed, 1 skipped.
This commit is contained in:
parent
994608a4f6
commit
7683557bbb
@ -132,6 +132,8 @@ class MockUnitState:
|
|||||||
name: str = ""
|
name: str = ""
|
||||||
state: int = 0 # 0=off, 1=on, 100..200=brightness percent (raw Omni)
|
state: int = 0 # 0=off, 1=on, 100..200=brightness percent (raw Omni)
|
||||||
time_remaining: int = 0
|
time_remaining: int = 0
|
||||||
|
unit_type: int = 1 # enuOL2UnitType (1=Standard); see clsUnit.cs:928 family
|
||||||
|
areas: int = 0x01 # bitmask of area membership (bit 0 = area 1, ...)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -155,6 +157,8 @@ class MockAreaState:
|
|||||||
auto_bypass: bool = False
|
auto_bypass: bool = False
|
||||||
all_on_for_alarm: bool = False
|
all_on_for_alarm: bool = False
|
||||||
trouble_beep: bool = False
|
trouble_beep: bool = False
|
||||||
|
perimeter_chime: bool = False
|
||||||
|
audible_exit_delay: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -347,7 +351,20 @@ class MockState:
|
|||||||
)
|
)
|
||||||
for i, n in acct.zone_names.items()
|
for i, n in acct.zone_names.items()
|
||||||
},
|
},
|
||||||
"units": {i: MockUnitState(name=n) for i, n in acct.unit_names.items()},
|
"units": {
|
||||||
|
i: MockUnitState(
|
||||||
|
name=n,
|
||||||
|
unit_type=acct.unit_types.get(i, 1),
|
||||||
|
# 0xFF (uninitialised → "all areas") and 0x01 are the
|
||||||
|
# two common values. If the panel doesn't have a
|
||||||
|
# specific restriction, fall back to "area 1 only"
|
||||||
|
# so HA's area-filtering produces sensible defaults.
|
||||||
|
areas=(acct.unit_areas.get(i, 0x01) or 0x01)
|
||||||
|
if acct.unit_areas.get(i, 0x01) != 0xFF
|
||||||
|
else 0x01,
|
||||||
|
)
|
||||||
|
for i, n in acct.unit_names.items()
|
||||||
|
},
|
||||||
"areas": {
|
"areas": {
|
||||||
i: MockAreaState(
|
i: MockAreaState(
|
||||||
name=acct.area_names.get(i, ""),
|
name=acct.area_names.get(i, ""),
|
||||||
@ -359,6 +376,8 @@ class MockState:
|
|||||||
auto_bypass=acct.area_auto_bypass.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),
|
all_on_for_alarm=acct.area_all_on_for_alarm.get(i, False),
|
||||||
trouble_beep=acct.area_trouble_beep.get(i, False),
|
trouble_beep=acct.area_trouble_beep.get(i, False),
|
||||||
|
perimeter_chime=acct.area_perimeter_chime.get(i, False),
|
||||||
|
audible_exit_delay=acct.area_audible_exit_delay.get(i, False),
|
||||||
)
|
)
|
||||||
for i in sorted(in_use_areas)
|
for i in sorted(in_use_areas)
|
||||||
},
|
},
|
||||||
@ -990,11 +1009,11 @@ class MockPanel:
|
|||||||
unit.state if unit else 0,
|
unit.state if unit else 0,
|
||||||
(unit.time_remaining >> 8) & 0xFF if unit else 0,
|
(unit.time_remaining >> 8) & 0xFF if unit else 0,
|
||||||
unit.time_remaining & 0xFF if unit else 0,
|
unit.time_remaining & 0xFF if unit else 0,
|
||||||
1, # UnitType: Standard
|
unit.unit_type if unit else 1,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
+ self.state.unit_name_bytes(index)
|
+ self.state.unit_name_bytes(index)
|
||||||
+ bytes([0, 1]) # reserved + UnitAreas (default area 1)
|
+ bytes([0, (unit.areas if unit else 0x01)])
|
||||||
)
|
)
|
||||||
return encode_v2(OmniLink2MessageType.Properties, body)
|
return encode_v2(OmniLink2MessageType.Properties, body)
|
||||||
|
|
||||||
|
|||||||
@ -261,12 +261,55 @@ class PcaAccount:
|
|||||||
#
|
#
|
||||||
# PerimeterChime and AudibleExitDelay are NOT in this contiguous
|
# PerimeterChime and AudibleExitDelay are NOT in this contiguous
|
||||||
# block — they live deeper in the user section past FlashLightNum,
|
# block — they live deeper in the user section past FlashLightNum,
|
||||||
# HouseCodes flags, and 6 TimeClock When-structs.
|
# HouseCodes flags, and 6 TimeClock When-structs (see
|
||||||
|
# perimeter_chime / audible_exit_delay below).
|
||||||
area_entry_chime: dict[int, bool] = 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_quick_arm: dict[int, bool] = field(default_factory=dict)
|
||||||
area_auto_bypass: 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_all_on_for_alarm: dict[int, bool] = field(default_factory=dict)
|
||||||
area_trouble_beep: dict[int, bool] = field(default_factory=dict)
|
area_trouble_beep: dict[int, bool] = field(default_factory=dict)
|
||||||
|
area_perimeter_chime: dict[int, bool] = field(default_factory=dict)
|
||||||
|
area_audible_exit_delay: dict[int, bool] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# Per-unit type + area assignment derived from CAP index ranges and
|
||||||
|
# the AreaGroups arrays in SetupData. Keys are 1-based unit slots
|
||||||
|
# (1..numUnits). Values:
|
||||||
|
# unit_types[u]: raw ``enuOL2UnitType`` byte (1=Standard for X10,
|
||||||
|
# 12=Flag for FlagOut, 13=Output for VoltOut/ExpEnc). The
|
||||||
|
# X10 sub-types are collapsed to Standard — deriving the
|
||||||
|
# specific HouseCodeFormat would require the EnableExtCode
|
||||||
|
# table which we don't decode yet.
|
||||||
|
# unit_areas[u]: 8-bit area-membership bitmask. 0xFF is the panel
|
||||||
|
# default ("all areas") when no specific restriction is set;
|
||||||
|
# 0x01 means "area 1 only".
|
||||||
|
unit_types: dict[int, int] = field(default_factory=dict)
|
||||||
|
unit_areas: dict[int, int] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# User codes (PIN database). 99 entries on OMNI_PRO_II.
|
||||||
|
#
|
||||||
|
# ``code_pins`` is the raw 16-bit value stored on disk (BE u16) —
|
||||||
|
# plain 4-digit PINs decode as decimal 0..9999, but the live fixture
|
||||||
|
# has some entries with values >9999 whose format isn't yet
|
||||||
|
# determined (possibly scrambled, possibly card-credential format,
|
||||||
|
# possibly partial-byte flags). Treat as opaque pending RE.
|
||||||
|
#
|
||||||
|
# ``code_pins`` is marked ``repr=False`` so a debug ``print(acct)``
|
||||||
|
# never leaks PIN material into logs. ``code_authority`` is the
|
||||||
|
# enuCodeAuthority byte (0=Disabled, 1=User, 2=Manager, 3=Installer)
|
||||||
|
# and ``code_areas`` is the area-membership bitmask (0xFF = all).
|
||||||
|
code_pins: dict[int, int] = field(default_factory=dict, repr=False)
|
||||||
|
code_authority: dict[int, int] = field(default_factory=dict)
|
||||||
|
code_areas: dict[int, int] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# DST configuration (when the panel switches between DST and standard
|
||||||
|
# time). Values are raw bytes from enuDSTMonth / enuDSTWeek:
|
||||||
|
# 0 = Disabled, 1..12 = month, 1..7 = week (1=First Sunday, 2=Second,
|
||||||
|
# 3=Third, 4=Fourth, 5=Last, 6=Next to Last, 7=Third from Last).
|
||||||
|
# US default after 2007: Mar/Second, Nov/First.
|
||||||
|
dst_start_month: int = 0
|
||||||
|
dst_start_week: int = 0
|
||||||
|
dst_end_month: int = 0
|
||||||
|
dst_end_week: int = 0
|
||||||
|
|
||||||
# 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
|
||||||
@ -327,6 +370,16 @@ _CAP_OMNI_PRO_II: dict[str, int] = {
|
|||||||
# 1771..1778: Areas[1..8].EntryDelay (8 bytes)
|
# 1771..1778: Areas[1..8].EntryDelay (8 bytes)
|
||||||
# 1779..1786: Areas[1..8].ExitDelay (8 bytes)
|
# 1779..1786: Areas[1..8].ExitDelay (8 bytes)
|
||||||
#
|
#
|
||||||
|
# Codes block: 99 entries × 14 bytes at offset 383.
|
||||||
|
# Per-entry layout (clsHAC.cs:3001-3009):
|
||||||
|
# bytes 0..1: PIN (BE u16, clsHardwareArray.ReadUInt16)
|
||||||
|
# byte 2: Authority (enuCodeAuthority: 0=Disabled, 1=User,
|
||||||
|
# 2=Manager, 3=Installer)
|
||||||
|
# byte 3: Areas bitmask
|
||||||
|
# bytes 4..8: WhenOn (clsWhen)
|
||||||
|
# bytes 9..13: WhenOff (clsWhen)
|
||||||
|
"codesOffset": 383,
|
||||||
|
"codeEntryBytes": 14,
|
||||||
"entryDelayOffset": 1771,
|
"entryDelayOffset": 1771,
|
||||||
"exitDelayOffset": 1779,
|
"exitDelayOffset": 1779,
|
||||||
# Five contiguous bool[8] flag arrays immediately follow ExitDelay
|
# Five contiguous bool[8] flag arrays immediately follow ExitDelay
|
||||||
@ -338,6 +391,25 @@ _CAP_OMNI_PRO_II: dict[str, int] = {
|
|||||||
"autoBypassOffset": 1803,
|
"autoBypassOffset": 1803,
|
||||||
"allOnForAlarmOffset": 1811,
|
"allOnForAlarmOffset": 1811,
|
||||||
"troubleBeepOffset": 1819,
|
"troubleBeepOffset": 1819,
|
||||||
|
# After TroubleBeep the user section continues with HighSecurity (1),
|
||||||
|
# FreezeAlarm (1), FlashLightNum_HI+LO (2; lastX10>255), HouseCodes
|
||||||
|
# EnableAllOff[16] (16), EnableAllOn[16] (16), 6×clsWhen (30),
|
||||||
|
# Latitude+Longitude+TimeZone (3), AnnounceAlarms (1) — 70 bytes —
|
||||||
|
# then the two remaining area bool[8] flags and DST scalars:
|
||||||
|
# 1897..1904: PerimeterChime[1..8]
|
||||||
|
# 1905..1912: AudibleExitDelay[1..8]
|
||||||
|
# 1913: DSTStartMonth (enuDSTMonth)
|
||||||
|
# 1914: DSTStartWeek (enuDSTWeek)
|
||||||
|
# 1915: DSTEndMonth (enuDSTMonth)
|
||||||
|
# 1916: DSTEndWeek (enuDSTWeek)
|
||||||
|
# HouseCodes.Count derives as (lastX10 - firstX10 + 1) / 16 = 16 for
|
||||||
|
# OMNI_PRO_II (clsHouseCodes.cs:35).
|
||||||
|
"perimeterChimeOffset": 1897,
|
||||||
|
"audibleExitDelayOffset": 1905,
|
||||||
|
"dstStartMonthOffset": 1913,
|
||||||
|
"dstStartWeekOffset": 1914,
|
||||||
|
"dstEndMonthOffset": 1915,
|
||||||
|
"dstEndWeekOffset": 1916,
|
||||||
|
|
||||||
# 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
|
||||||
@ -377,7 +449,18 @@ _CAP_OMNI_PRO_II: dict[str, int] = {
|
|||||||
"zoneTypeOffset": 2572,
|
"zoneTypeOffset": 2572,
|
||||||
"tempFormatOffset": 2993,
|
"tempFormatOffset": 2993,
|
||||||
"numAreasUsedOffset": 3034,
|
"numAreasUsedOffset": 3034,
|
||||||
|
# AreaGroups arrays per family — each byte is an 8-bit area-membership
|
||||||
|
# bitmask covering one or more units, sized via the CAP ranges:
|
||||||
|
"x10AreaGroupsOffset": 3035, # (lastX10-firstX10+16)/16 = 16 bytes, 1 group/16 units
|
||||||
|
"voltOutAreaGroupsOffset": 3051, # lastVoltOut-firstVoltOut+1 = 8 bytes, 1 byte/unit
|
||||||
|
"flagOutAreaGroupsOffset": 3059, # (lastFlagOut-firstFlagOut+8)/8 = 15 bytes, 1 group/8 flags
|
||||||
|
"expEncAreaGroupsOffset": 3074, # (lastExpEncOut-firstExpEncOut+4)/4 = 32 bytes, 1 group/4 outputs
|
||||||
"zoneAreaOffset": 3106,
|
"zoneAreaOffset": 3106,
|
||||||
|
# Unit index ranges → unit type derivation. Per CAP for OMNI_PRO_II:
|
||||||
|
"firstX10": 1, "lastX10": 256,
|
||||||
|
"firstExpEncOut": 257, "lastExpEncOut": 384,
|
||||||
|
"firstVoltOut": 385, "lastVoltOut": 392,
|
||||||
|
"firstFlagOut": 393, "lastFlagOut": 511,
|
||||||
"max_zones": 176, "lenZoneName": 15, "zones_count": 176,
|
"max_zones": 176, "lenZoneName": 15, "zones_count": 176,
|
||||||
"max_units": 512, "lenUnitName": 12, "units_count": 511,
|
"max_units": 512, "lenUnitName": 12, "units_count": 511,
|
||||||
"max_buttons": 128, "lenButtonName": 12, "buttons_count": 255,
|
"max_buttons": 128, "lenButtonName": 12, "buttons_count": 255,
|
||||||
@ -499,6 +582,17 @@ class _ConnectionWalk:
|
|||||||
area_auto_bypass: 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_all_on_for_alarm: dict[int, bool] = field(default_factory=dict)
|
||||||
area_trouble_beep: dict[int, bool] = field(default_factory=dict)
|
area_trouble_beep: dict[int, bool] = field(default_factory=dict)
|
||||||
|
area_perimeter_chime: dict[int, bool] = field(default_factory=dict)
|
||||||
|
area_audible_exit_delay: dict[int, bool] = field(default_factory=dict)
|
||||||
|
unit_types: dict[int, int] = field(default_factory=dict)
|
||||||
|
unit_areas: dict[int, int] = field(default_factory=dict)
|
||||||
|
code_pins: dict[int, int] = field(default_factory=dict, repr=False)
|
||||||
|
code_authority: dict[int, int] = field(default_factory=dict)
|
||||||
|
code_areas: dict[int, int] = field(default_factory=dict)
|
||||||
|
dst_start_month: int = 0
|
||||||
|
dst_start_week: int = 0
|
||||||
|
dst_end_month: int = 0
|
||||||
|
dst_end_week: int = 0
|
||||||
temp_format: int = 0
|
temp_format: int = 0
|
||||||
num_areas_used: int = 0
|
num_areas_used: int = 0
|
||||||
|
|
||||||
@ -573,6 +667,94 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk:
|
|||||||
area_auto_bypass = _read_area_bool_array("autoBypassOffset")
|
area_auto_bypass = _read_area_bool_array("autoBypassOffset")
|
||||||
area_all_on_for_alarm = _read_area_bool_array("allOnForAlarmOffset")
|
area_all_on_for_alarm = _read_area_bool_array("allOnForAlarmOffset")
|
||||||
area_trouble_beep = _read_area_bool_array("troubleBeepOffset")
|
area_trouble_beep = _read_area_bool_array("troubleBeepOffset")
|
||||||
|
area_perimeter_chime = _read_area_bool_array("perimeterChimeOffset")
|
||||||
|
area_audible_exit_delay = _read_area_bool_array("audibleExitDelayOffset")
|
||||||
|
|
||||||
|
def _read_scalar_byte(offset_key: str) -> int:
|
||||||
|
off = cap.get(offset_key)
|
||||||
|
if off is None or off >= len(setup_data):
|
||||||
|
return 0
|
||||||
|
return setup_data[off]
|
||||||
|
|
||||||
|
dst_start_month = _read_scalar_byte("dstStartMonthOffset")
|
||||||
|
dst_start_week = _read_scalar_byte("dstStartWeekOffset")
|
||||||
|
dst_end_month = _read_scalar_byte("dstEndMonthOffset")
|
||||||
|
dst_end_week = _read_scalar_byte("dstEndWeekOffset")
|
||||||
|
|
||||||
|
# Unit type + area assignment, per unit index.
|
||||||
|
#
|
||||||
|
# Unit *type* is derived from which CAP range the index falls in
|
||||||
|
# (clsUnit.CalculateUnitType + the AreaGroups read in
|
||||||
|
# clsHAC._ParseSetupData at clsHAC.cs:3242-3289). We collapse the
|
||||||
|
# X10 sub-types (Standard/Extended/HLC/UPB/ZWave/…) to
|
||||||
|
# enuOL2UnitType.Standard=1 since deriving them requires the
|
||||||
|
# HouseCodes EnableExtCode table; non-X10 families resolve to
|
||||||
|
# Output=13 (Voltage/ExpEnc) or Flag=12.
|
||||||
|
#
|
||||||
|
# Unit *area* is the bitmask byte from the AreaGroups array of the
|
||||||
|
# appropriate family, indexed by the unit's group:
|
||||||
|
# X10: group = (Number - firstX10) // 16
|
||||||
|
# VoltOut: group = (Number - firstVoltOut) (1 byte/unit)
|
||||||
|
# FlagOut: group = (Number - firstFlagOut) // 8
|
||||||
|
# ExpEnc: group = (Number - firstExpEncOut) // 4
|
||||||
|
# Byte 0xFF (panel default, uninitialised) is reported verbatim —
|
||||||
|
# consumers treat that as "all areas".
|
||||||
|
def _read_area_group(group_off_key: str, group_idx: int) -> int:
|
||||||
|
off = cap.get(group_off_key)
|
||||||
|
if off is None:
|
||||||
|
return 0xFF
|
||||||
|
pos = off + group_idx
|
||||||
|
if pos >= len(setup_data):
|
||||||
|
return 0xFF
|
||||||
|
return setup_data[pos]
|
||||||
|
|
||||||
|
# Codes block — extract PINs (raw BE u16), authority byte, areas
|
||||||
|
# bitmask. PINs are PII; we expose the raw value but don't print it
|
||||||
|
# in any repr (PcaAccount uses repr=False on these fields).
|
||||||
|
codes_off = cap.get("codesOffset")
|
||||||
|
code_bytes = cap.get("codeEntryBytes", 14)
|
||||||
|
num_codes = cap.get("max_codes", 0)
|
||||||
|
code_pins: dict[int, int] = {}
|
||||||
|
code_authority: dict[int, int] = {}
|
||||||
|
code_areas: dict[int, int] = {}
|
||||||
|
if codes_off is not None:
|
||||||
|
for k in range(1, num_codes + 1):
|
||||||
|
base = codes_off + (k - 1) * code_bytes
|
||||||
|
if base + 4 > len(setup_data):
|
||||||
|
break
|
||||||
|
# BE u16 (clsHardwareArray.ReadUInt16)
|
||||||
|
code_pins[k] = (setup_data[base] << 8) | setup_data[base + 1]
|
||||||
|
code_authority[k] = setup_data[base + 2]
|
||||||
|
code_areas[k] = setup_data[base + 3]
|
||||||
|
|
||||||
|
unit_types: dict[int, int] = {}
|
||||||
|
unit_areas: dict[int, int] = {}
|
||||||
|
f_x10, l_x10 = cap.get("firstX10", 0), cap.get("lastX10", 0)
|
||||||
|
f_vo, l_vo = cap.get("firstVoltOut", 0), cap.get("lastVoltOut", 0)
|
||||||
|
f_fo, l_fo = cap.get("firstFlagOut", 0), cap.get("lastFlagOut", 0)
|
||||||
|
f_ee, l_ee = cap.get("firstExpEncOut", 0), cap.get("lastExpEncOut", 0)
|
||||||
|
max_units = cap.get("max_units", 0)
|
||||||
|
for u in range(1, max_units + 1):
|
||||||
|
if f_x10 and f_x10 <= u <= l_x10:
|
||||||
|
unit_types[u] = 1 # enuOL2UnitType.Standard (collapsed X10 default)
|
||||||
|
unit_areas[u] = _read_area_group(
|
||||||
|
"x10AreaGroupsOffset", (u - f_x10) // 16
|
||||||
|
)
|
||||||
|
elif f_ee and f_ee <= u <= l_ee:
|
||||||
|
unit_types[u] = 13 # enuOL2UnitType.Output
|
||||||
|
unit_areas[u] = _read_area_group(
|
||||||
|
"expEncAreaGroupsOffset", (u - f_ee) // 4
|
||||||
|
)
|
||||||
|
elif f_vo and f_vo <= u <= l_vo:
|
||||||
|
unit_types[u] = 13 # enuOL2UnitType.Output
|
||||||
|
unit_areas[u] = _read_area_group(
|
||||||
|
"voltOutAreaGroupsOffset", u - f_vo
|
||||||
|
)
|
||||||
|
elif f_fo and f_fo <= u <= l_fo:
|
||||||
|
unit_types[u] = 12 # enuOL2UnitType.Flag
|
||||||
|
unit_areas[u] = _read_area_group(
|
||||||
|
"flagOutAreaGroupsOffset", (u - f_fo) // 8
|
||||||
|
)
|
||||||
|
|
||||||
# Scalars from the installer section.
|
# Scalars from the installer section.
|
||||||
tf_off = cap.get("tempFormatOffset")
|
tf_off = cap.get("tempFormatOffset")
|
||||||
@ -627,6 +809,17 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk:
|
|||||||
area_auto_bypass=area_auto_bypass,
|
area_auto_bypass=area_auto_bypass,
|
||||||
area_all_on_for_alarm=area_all_on_for_alarm,
|
area_all_on_for_alarm=area_all_on_for_alarm,
|
||||||
area_trouble_beep=area_trouble_beep,
|
area_trouble_beep=area_trouble_beep,
|
||||||
|
area_perimeter_chime=area_perimeter_chime,
|
||||||
|
area_audible_exit_delay=area_audible_exit_delay,
|
||||||
|
unit_types=unit_types,
|
||||||
|
unit_areas=unit_areas,
|
||||||
|
code_pins=code_pins,
|
||||||
|
code_authority=code_authority,
|
||||||
|
code_areas=code_areas,
|
||||||
|
dst_start_month=dst_start_month,
|
||||||
|
dst_start_week=dst_start_week,
|
||||||
|
dst_end_month=dst_end_month,
|
||||||
|
dst_end_week=dst_end_week,
|
||||||
temp_format=temp_format,
|
temp_format=temp_format,
|
||||||
num_areas_used=num_areas_used,
|
num_areas_used=num_areas_used,
|
||||||
)
|
)
|
||||||
@ -725,6 +918,17 @@ def parse_pca_file(path_or_bytes: str | os.PathLike[str] | bytes, key: int) -> P
|
|||||||
account.area_auto_bypass = walk.area_auto_bypass
|
account.area_auto_bypass = walk.area_auto_bypass
|
||||||
account.area_all_on_for_alarm = walk.area_all_on_for_alarm
|
account.area_all_on_for_alarm = walk.area_all_on_for_alarm
|
||||||
account.area_trouble_beep = walk.area_trouble_beep
|
account.area_trouble_beep = walk.area_trouble_beep
|
||||||
|
account.area_perimeter_chime = walk.area_perimeter_chime
|
||||||
|
account.area_audible_exit_delay = walk.area_audible_exit_delay
|
||||||
|
account.unit_types = walk.unit_types
|
||||||
|
account.unit_areas = walk.unit_areas
|
||||||
|
account.code_pins = walk.code_pins
|
||||||
|
account.code_authority = walk.code_authority
|
||||||
|
account.code_areas = walk.code_areas
|
||||||
|
account.dst_start_month = walk.dst_start_month
|
||||||
|
account.dst_start_week = walk.dst_start_week
|
||||||
|
account.dst_end_month = walk.dst_end_month
|
||||||
|
account.dst_end_week = walk.dst_end_week
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@ -368,14 +368,43 @@ async def test_mockstate_from_pca_serves_real_panel_programs() -> None:
|
|||||||
# AutoBypass OFF
|
# AutoBypass OFF
|
||||||
# AllOnForAlarm ON
|
# AllOnForAlarm ON
|
||||||
# TroubleBeep OFF
|
# TroubleBeep OFF
|
||||||
|
# PerimeterChime OFF (homeowner disabled)
|
||||||
|
# AudibleExitDelay ON
|
||||||
assert acct.area_entry_chime[1] is False
|
assert acct.area_entry_chime[1] is False
|
||||||
assert acct.area_quick_arm[1] is True
|
assert acct.area_quick_arm[1] is True
|
||||||
assert acct.area_auto_bypass[1] is False
|
assert acct.area_auto_bypass[1] is False
|
||||||
assert acct.area_all_on_for_alarm[1] is True
|
assert acct.area_all_on_for_alarm[1] is True
|
||||||
assert acct.area_trouble_beep[1] is False
|
assert acct.area_trouble_beep[1] is False
|
||||||
|
assert acct.area_perimeter_chime[1] is False
|
||||||
|
assert acct.area_audible_exit_delay[1] is True
|
||||||
# And the values flowed through MockState.
|
# And the values flowed through MockState.
|
||||||
assert state.areas[1].quick_arm is True
|
assert state.areas[1].quick_arm is True
|
||||||
assert state.areas[1].entry_chime is False
|
assert state.areas[1].entry_chime is False
|
||||||
|
assert state.areas[1].perimeter_chime is False
|
||||||
|
|
||||||
|
# DST configuration — US default (Mar/2nd Sun, Nov/1st Sun).
|
||||||
|
assert acct.dst_start_month == 3
|
||||||
|
assert acct.dst_start_week == 2
|
||||||
|
assert acct.dst_end_month == 11
|
||||||
|
assert acct.dst_end_week == 1
|
||||||
|
|
||||||
|
# Unit type derivation by index range:
|
||||||
|
assert acct.unit_types[1] == 1 # X10 → Standard
|
||||||
|
assert acct.unit_types[257] == 13 # ExpEnc → Output
|
||||||
|
assert acct.unit_types[385] == 13 # VoltOut → Output
|
||||||
|
assert acct.unit_types[393] == 12 # FlagOut → Flag
|
||||||
|
# Unit type/areas threaded into MockUnitState — every unit is X10
|
||||||
|
# type 1 (the named ones in this fixture are all X10).
|
||||||
|
assert state.units[1].unit_type == 1
|
||||||
|
# Area was 0xff (panel default = "all") → normalized to 0x01 in mock.
|
||||||
|
assert state.units[1].areas == 0x01
|
||||||
|
|
||||||
|
# Codes: PINs decode as BE u16. PII fields not in repr().
|
||||||
|
assert acct.code_authority[1] == 1 # COMPUTER → User
|
||||||
|
assert acct.code_authority[4] == 2 # Debra → Manager
|
||||||
|
assert acct.code_authority[5] == 3 # Cage → Installer
|
||||||
|
assert 0 <= acct.code_pins[1] <= 0xFFFF
|
||||||
|
assert "code_pins" not in repr(acct)
|
||||||
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