From 7b789f8cfb8507df535d1a8a2e9dde79d094120f Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 13 May 2026 16:39:30 -0600 Subject: [PATCH] pca_file: Latitude / Longitude / TimeZone from SetupData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three single-byte scalars sandwiched between the TimeClocks block and AnnounceAlarms (clsHAC.cs:3064-3066). Raw bytes — no N/S/E/W modifier at this position (those live in the optional WorldWideLatitude feature block past DST). TimeZone is hours west of UTC on OMNI_PRO_II. Live fixture decodes as 44°N / 117°W / TZ 7 (Pacific Daylight) — matches a real northern-US install on the panel time. PcaAccount gains latitude / longitude / time_zone (int each). Walker reads them via the existing _read_scalar_byte helper. Full suite: 499 passed, 1 skipped. --- src/omni_pca/pca_file.py | 30 ++++++++++++++++++++++++++++++ tests/test_e2e_program_echo.py | 5 +++++ 2 files changed, 35 insertions(+) diff --git a/src/omni_pca/pca_file.py b/src/omni_pca/pca_file.py index 3270033..a4dd201 100644 --- a/src/omni_pca/pca_file.py +++ b/src/omni_pca/pca_file.py @@ -325,6 +325,15 @@ class PcaAccount: enable_pc_access: bool = False pc_access_code: int = field(default=0, repr=False) + # Panel geographic configuration — raw bytes used by the firmware + # to compute sunrise/sunset for time-of-day programs. No N/S/E/W + # modifier at this position (those live in the WorldWideLatitude + # feature block past DST). TimeZone is hours west of UTC on + # OMNI_PRO_II (7=PDT, 8=PST). + latitude: int = 0 + longitude: int = 0 + time_zone: int = 0 + # 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, @@ -428,6 +437,15 @@ _CAP_OMNI_PRO_II: dict[str, int] = { # 1916: DSTEndWeek (enuDSTWeek) # HouseCodes.Count derives as (lastX10 - firstX10 + 1) / 16 = 16 for # OMNI_PRO_II (clsHouseCodes.cs:35). + # Three single-byte scalars sandwiched between the TimeClocks block + # and AnnounceAlarms (clsHAC.cs:3064-3066). Latitude / Longitude are + # raw degrees (no N/S/E/W modifier in this position — those live in + # the WorldWideLatitude feature block after DST). TimeZone is the + # panel's UTC offset selector; OMNI_PRO_II uses raw hours west of + # UTC (e.g. 7 = Pacific Daylight, 8 = Pacific Standard). + "latitudeOffset": 1893, + "longitudeOffset": 1894, + "timeZoneOffset": 1895, "perimeterChimeOffset": 1897, "audibleExitDelayOffset": 1905, "dstStartMonthOffset": 1913, @@ -657,6 +675,9 @@ class _ConnectionWalk: installer_code: int = 0 enable_pc_access: bool = False pc_access_code: int = 0 + latitude: int = 0 + longitude: int = 0 + time_zone: int = 0 dst_start_month: int = 0 dst_start_week: int = 0 dst_end_month: int = 0 @@ -780,6 +801,9 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk: installer_code = _read_be_u16("installerCodeOffset") pc_access_code = _read_be_u16("pcAccessCodeOffset") + latitude = _read_scalar_byte("latitudeOffset") + longitude = _read_scalar_byte("longitudeOffset") + time_zone = _read_scalar_byte("timeZoneOffset") epa_off = cap.get("enablePCAccessOffset") enable_pc_access = ( bool(setup_data[epa_off]) @@ -964,6 +988,9 @@ def _walk_to_connection(r: PcaReader, cap: dict[str, int]) -> _ConnectionWalk: installer_code=installer_code, enable_pc_access=enable_pc_access, pc_access_code=pc_access_code, + latitude=latitude, + longitude=longitude, + time_zone=time_zone, dst_start_month=dst_start_month, dst_start_week=dst_start_week, dst_end_month=dst_end_month, @@ -1078,6 +1105,9 @@ def parse_pca_file(path_or_bytes: str | os.PathLike[str] | bytes, key: int) -> P account.installer_code = walk.installer_code account.enable_pc_access = walk.enable_pc_access account.pc_access_code = walk.pc_access_code + account.latitude = walk.latitude + account.longitude = walk.longitude + account.time_zone = walk.time_zone account.dst_start_month = walk.dst_start_month account.dst_start_week = walk.dst_start_week account.dst_end_month = walk.dst_end_month diff --git a/tests/test_e2e_program_echo.py b/tests/test_e2e_program_echo.py index 58942e3..c882b0a 100644 --- a/tests/test_e2e_program_echo.py +++ b/tests/test_e2e_program_echo.py @@ -425,6 +425,11 @@ async def test_mockstate_from_pca_serves_real_panel_programs() -> None: assert "installer_code" not in r assert "pc_access_code" not in r + # Geographic configuration — northern-US install on Pacific time. + assert 25 <= acct.latitude <= 49 # continental US lat range + assert 67 <= acct.longitude <= 125 # continental US long range + assert acct.time_zone in (5, 6, 7, 8, 9, 10) # US zones EST..AKST + # 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