Project moved to a self-hosted Gitea at git.supported.systems under the warehack.ing org. Updated: pyproject.toml project.urls.Repository custom_components/omni_pca/manifest.json documentation, issue_tracker custom_components/omni_pca/README.md every link CHANGELOG.md release tag URL Tests still 351 + 1 skip. No code changed.
10 KiB
10 KiB
Changelog
All notable changes to this project. Date-based versioning (CalVer, YYYY.M.D); each release date corresponds to a backwards-incompatible boundary.
2026.5.10 — 2026-05-10
First release. Working library + Home Assistant custom component, validated end-to-end against an in-process mock panel and a real HA instance running in Docker. Not yet validated against a live panel because the user's panel's network module is currently off.
Protocol layer (the reverse engineering)
- Decompiled HAI's PC Access 3.17 (.NET) with ilspycmd; identified two namespaces —
HAI_Shared(protocol/crypto/domain) andPCAccess3(UI). Decompilation lives inpca-re/decompiled/. - Reverse-engineered the
.pcaandPCA01.CFGfile format — Borland-Pascal LCG keystream XORed byte-by-byte. Two hardcoded keys:KEY_PC01 = 0x14326573forPCA01.CFGKEY_EXPORT = 0x17569237for import/export.pcaPer-installation.pcafiles use a third key derived from the panel's installer code; that key is stored in plaintext insidePCA01.CFGafter first-stage decryption.
- Documented the Omni-Link II wire protocol byte-for-byte (
pca-re/notes/handshake.md), including two non-public quirks absent fromjomnilinkII,pyomnilink, and every public Omni-Link writeup we found:- Session key =
ControllerKey[0:11] || (ControllerKey[11:16] XOR SessionID[0:5])— not just the panel's ControllerKey directly. Source:clsOmniLinkConnection.cs:1886-1892. - Per-block XOR pre-whitening before AES — first two bytes of every 16-byte block are XORed with the packet's 16-bit sequence number, same mask all blocks. Source:
clsOmniLinkConnection.cs:396-401.
- Session key =
- Located a latent bug in PC Access itself: a
LargeVocabularyskip-path uses a buffer sized for the non-LargeVocabulary case. Harmless on every shipping panel (the count check always satisfies the constraint) but documented inpca-re/notes/body_parser.md.
Library — omni_pca
crypto.py— AES-128-ECB with PaddingMode.Zeros semantics,derive_session_key(), per-block XOR pre-whitening,encrypt_message_payload()/decrypt_message_payload(). All citations to C# source line numbers.opcodes.py— Three IntEnums byte-exact to the C# decompilation:PacketType(12 values),OmniLinkMessageType(104 v1 opcodes),OmniLink2MessageType(83 v2 opcodes). PlusConnectionType,ProtocolVersion.packet.py/message.py— OuterPacket(4-byte header + payload) and innerMessageframing. CRC-16/MODBUS (poly0xA001).pca_file.py— Borland LCG XOR cipher,PcaReaderwithu8/u16/u32/string8/string8_fixed/string16/string16_fixed,parse_pca01_cfg(),parse_pca_file(). Account-info fields defaultrepr=Falseto avoid accidental PII leakage in logs.connection.py—OmniConnection: async TCP, full secure-session handshake (4 packets), monotonic per-direction sequence numbers with0xFFFF → 1wraparound (skips 0), TCP framing that decrypts the first 16-byte block to learn the inner message length, reader task dispatching solicited replies to Futures and unsolicited messages to a queue, automatic reconnect onOmniConnectionError, custom exceptions (HandshakeError,InvalidEncryptionKeyError,ProtocolError,RequestTimeoutError).models.py— 21 typed frozen-slots dataclasses for every Omni object:SystemInformation,SystemStatus,ZoneProperties/Status,UnitProperties/Status,AreaProperties/Status,ThermostatProperties/Status,ButtonProperties,ProgramProperties,CodeProperties,MessageProperties,AuxSensorStatus,AudioZoneProperties/Status,AudioSourceProperties/Status,UserSettingProperties/Status. PlusSecurityMode,HvacMode,FanMode,HoldMode,ZoneType,ObjectTypeenums and temperature converters (Omni's linear°F = round(raw * 9/10) - 40).commands.py—CommandIntEnum (64 values, sourced fromenuUnitCommand.cswhich is the canonical command enum despite the misleading name),SecurityCommandResponse,CommandFailedError.client.py— High-levelOmniClientwith 18 methods:get_system_information,get_system_status,get_object_properties,list_*_names,execute_security_command,execute_command,get_object_status,get_extended_status,acknowledge_alerts, typed wrappers (turn_unit_on/off,set_unit_level,bypass_zone/restore_zone,set_thermostat_{system,fan,hold}_mode,set_thermostat_{heat,cool}_setpoint_raw,execute_button,execute_program,show_message,clear_message),events()async iterator over typedSystemEventobjects.events.py—SystemEventhierarchy. 26 typed subclasses (ZoneStateChanged,UnitStateChanged,ArmingChanged,AlarmActivated/Cleared,AcLost/Restored,BatteryLow/Restored,UserMacroButton,PhoneLineDead/Restored, …) +UnknownEventcatch-all. SystemEvents (opcode 55) packets carry multiple events;parse_events()returns a list.EventStreamflattens batches across messages.mock_panel.py— Stateful async TCP server emulating an Omni Pro II controller. Handles handshake,RequestSystemInformation/Status,RequestPropertiesfor Zone/Unit/Area/Thermostat/Button,RequestStatus/RequestExtendedStatus,Command,ExecuteSecurityCommand,AcknowledgeAlerts. State changes push synthesizedSystemEventspackets back to the client.__main__.py— CLI:omni-pca decode-pca <file> [--field controller_key|host|port] [--include-pii],omni-pca mock-panel,omni-pca version. PII opt-in.
Home Assistant integration — custom_components/omni_pca/
coordinator.py—OmniDataUpdateCoordinatorwith long-livedOmniClient, one-time discovery pass at first refresh (enumerates zones, units, areas, thermostats, buttons), periodic 30s poll for live state, background event-listener task consumingclient.events()and patching state in-place on each push.ConfigEntryAuthFailedonInvalidEncryptionKeyErrortriggers HA's reauth flow.- Eight platforms wrapping the library client:
alarm_control_panel— one per area, supports Day/Night/Away/Vacation/DayInstant arm modes with code validationbinary_sensor— one per binary zone (state + bypass diagnostic) plus 3 system-level (AC, battery, trouble)button— one per panel button macroclimate— one per thermostat (OFF/HEAT/COOL/HEAT_COOL + fan + preset modes)event— one per panel, relays 12 typed event types to HA automationslight— one per unit (dimmable; non-dimmable relays silently ignore brightness)sensor— analog zones (temperature/humidity/power), per-thermostat diagnostic temp/humidity/outdoor sensors, panel model+firmware sensor, last-event sensorswitch— per-zone bypass control (config entity_category)
config_flow.py— User + reauth steps. Host/port/controller_key with hex validation. Probes the panel viaOmniClient.get_system_information()before creating the entry; surfaces auth/cannot_connect errors with HA-friendly strings.services.yaml+services.py— 7 services (bypass_zone,restore_zone,execute_program,show_message,clear_message,acknowledge_alerts,send_command). Idempotent registration; each takes aconfig_entryselector so users pick the panel.diagnostics.py— Snapshot dump with controller key redacted and zone/unit/area names sha256-hashed.helpers.py— Pure functions for everything HA-shape-dependent: zone-type→device-class, brightness conversion, HVAC mode round-trip, temperature inverse, alarm state translation, event-type strings. Nohomeassistant.*imports; 61 unit tests covering it.manifest.json—iot_class: local_push,version: 2026.5.10,config_flow: true, requiresomni-pca==2026.5.10.hacs.jsonat project root for HACS distribution.
Tests
- 351 passing, 1 skipped. Ruff clean across
src/,tests/,custom_components/. - 17 e2e tests connecting
OmniClienttoMockPanelover real TCP, proving the full handshake + encryption + framing stack roundtrips. - 12 HA-side integration tests using
pytest-homeassistant-custom-component— boot HA in-process, drive the config flow, exercise services, verify state mutations. Full HA-side suite runs in <1 second. - 61 unit tests on
custom_components/omni_pca/helpers.pyrunning without HA installed. - Unit tests for every library module (crypto KAT vectors, CRC-16, packet/message ser-de, .pca decrypt, command payloads, event parsing).
Developer tooling
dev/docker-compose.yml+dev/Makefile— One-command HA + MockPanel stack for manual smoke testing and screenshot capture.dev/run_mock_panel.py— Long-running mock seeded with 5 zones, 4 units, 2 areas, 2 thermostats, 3 buttons, 2 user codes.dev/screenshot.py— End-to-end automated demo: onboards HA via REST, adds the integration via config-flow API, drives headless chromium via playwright to capture six deep-linked PNGs (overview, integrations list, integration detail, device page, entities table, developer states).
Documentation
docs/JOURNEY.md— 6,000+ word raw chronological narrative from "pile of binaries" through "351 tests green, screenshots captured". Source material for future writeups.pca-re/notes/findings.md— RE technical findings (cipher, file format, protocol overview).pca-re/notes/handshake.md— Byte-level handshake spec with C# source line citations.pca-re/notes/body_parser.md— .pca body schema + the LargeVocabulary latent bug.- Top-level
README.md— Library + HA quick start. custom_components/omni_pca/README.md— Entity table, services list, automation example, troubleshooting.dev/README.md— Docker dev stack walkthrough.
Known gaps
- Live panel validation: blocked on the user's panel's Ethernet module being enabled. Mock panel proves the stack roundtrips; the live lap is one TCP connect away once the panel is reachable.
- Programs discovery: the library's v1.0 has no
RequestPropertiespath for Program objects; the HA coordinator returns an empty programs dict. Programs can still be executed by index via theomni_pca.execute_programservice. - PyPI publish:
omni-pcanot yet on PyPI; HAmanifest.jsonrequirements line will only resolve once it is. For now users either install the wheel manually or pip-install from a Git URL. - HACS submission: pending live-panel validation.