OmniClientV1Adapter (src/omni_pca/v1/adapter.py)
V2-shape facade over OmniClientV1. Exposes the OmniClient surface the
HA coordinator was written against — get_system_information,
list_*_names, get_object_properties (synthesized from streamed names),
get_extended_status (chunked, routed to v1 typed status opcodes),
get_object_status(AREA, ...) (derived from SystemStatus.area_alarms),
events() (EventStream on v1 SystemEvents opcode 35), plus all the
write-method shims.
Chunks unit/zone/thermostat/aux polls per-type because firmware 2.12
NAKs Request*Status with >~62 records in one shot (verified live).
Falls back to "Area 1".."Area 8" when the UploadNames stream returns
zero areas — common on panels where the installer didn't name them.
custom_components/omni_pca/coordinator.py
_ensure_connected picks OmniClientV1Adapter for transport=udp. New
_walk_properties_v1 replaces the v2 RequestProperties walk with a
name-stream + synthesized-Properties pass.
custom_components/omni_pca/config_flow.py
_probe routes to OmniClientV1Adapter for transport=udp instead of
trying to drive v2 OmniClient over UDP (which silently dropped after
handshake, per the earlier diagnosis).
src/omni_pca/events.py
parse_events / _ensure_system_events / EventStream now take an
expected_opcode arg (default v2 SystemEvents=55, v1 callers pass 35).
Word format is byte-identical between v1 and v2, so the typed-event
decoder is unchanged.
src/omni_pca/v1/client.py
_range_status supports the long-form RequestUnitStatus (BE u16
start/end) so panels with unit indices > 255 (sprinklers, flags) work.
Verified end-to-end against firmware 2.12 panel at 192.168.1.9:
config entries:
state=loaded Omni Pro II (host.docker.internal) (mock)
state=loaded Omni Pro II (192.168.1.9) (real, v1+UDP)
real-panel entities created in HA: 96 (30 binary_sensor, 26 light,
15 switch, 13 button, 9 sensor, 3 climate)
cross-check: light.omni_pro_ii_front_porch_2 = on (matches live
probe: unit #2 'FRONT PORCH' state=0x01 brightness=100)
dev/probe_v1_coordinator.py
Coordinator-shaped end-to-end smoke test against the real panel
without HA — drives the full discovery + poll cycle through the
adapter. Useful for regression-checking the v1 wire path.
dev/add_real_panel.py
Programmatically adds the real-panel config entry to the dev HA
stack via the REST config-flow endpoints. Idempotent.
Dev stack
Local Home Assistant + MockPanel for clicking around the integration without a real Omni controller. Useful for screenshots, manual smoke tests, and seeing what the entity layout looks like.
Quick start
cd dev/
make dev-up # docker compose up -d
# wait ~30s for HA to boot
open http://localhost:8123
First time: HA onboarding wizard (any name / location works). Then:
- Settings → Devices & Services → Add Integration
- Search for HAI/Leviton Omni Panel
- Fill in:
- host:
host.docker.internal - port:
14369 - controller key:
000102030405060708090a0b0c0d0e0f
- host:
- Submit. Within a few seconds you should see the Omni Pro II device with ~25 entities (binary sensors, lights, alarm panel, climate, sensors, buttons, switches, the events entity).
What the mock simulates
Five named zones, four units, two areas, two thermostats, three button
macros. User codes 1234 (master, code index 1) and 5678 (code index 2).
Arming the alarm with code 1234 will succeed and the
alarm_control_panel entity transitions through ARMING → ARMED_AWAY in
real time via the panel's push-event simulation. Wrong code → HA error
toast, panel stays disarmed.
Other targets
make dev-logs # tail HA + mock logs
make dev-mock # run only the mock on the host (no docker)
make dev-down # stop the stack
make dev-reset # wipe HA config and start fresh
Notes
- The HA container mounts
../custom_components/omni_pca/read-only, so edits to the integration need a restart (docker compose restart homeassistant) to take effect. - The mock panel binds
0.0.0.0:14369inside the container. If you prefer to talk to it from the host directly (e.g. withomni-pcaCLI), usemake dev-mockto run it natively.