The previous "What we don't yet know" entry for RemarkID -> RemarkText
is now closed. Replaced the placeholder note with the actual format
(clsPrograms.ReadRemarks layout) and a Python usage example showing
how to resolve a Program's remark_id against PcaAccount.remarks.
Removed the "RemarkID -> RemarkText lookup" line from the
"what we don't yet know" list.
New reference page documenting the Omni Pro II's 14-byte program record
format. Mirrors file-format.mdx's structure: byte-offset table, enum
tables (ProgramType, ProgramCond, Days), worked example decoding a
real slot from the live fixture, callouts for the EVENT-only Mon/Day
swap on disk, and an explicit "what we don't yet know" section that
lists the gaps a future editor pass would have to close (cond bit
split, multi-record clausal encoding, RemarkID -> RemarkText lookup,
DPC capability flag, sunrise/sunset offset flag).
Sidebar adds the entry between File format and Hardware specs.
Re-captured against the current dev stack via dev/screenshot.py which
now injects selectedTheme={dark:true} into localStorage before HA boots.
Six shots, all matching the docs site's dark theme:
01-overview.png lovelace dashboard
02-integrations-list.png integrations dashboard (shows BOTH panels
now -- mock @ host.docker.internal + real
@ 192.168.1.9 -- as "2 devices")
03-omni-pca-config.png omni_pca integration detail
04-panel-device.png device deep-link
05-entities-omni.png entities filtered by config entry
06-developer-states.png developer-tools/state
Two new pages distilled from HAI's official OmniPro II Installation
Manual 3-2 + Product Specifications datasheet (Quadomated mirror).
reference/hardware-specs.md
Capacity ceilings (176 zones, 511 units, 8 areas, 64 thermostats,
128 buttons, 1500 programs, 99 codes, 128 messages, etc.), digital
communicator + network features, electrical specs with the per-output
current caps and the 24-hour battery-standby derating numbers,
physical dimensions, environmental ranges, listings, languages, and
part-number lookup. The numbers here are the source of every cap in
clsCapOMNI_PRO_II.cs and the upper bounds for the protocol's range
fields, so anyone debugging "why does my panel NAK at index N" should
start here.
explanation/zone-unit-numbering.md
Appendix C of the install manual, transcribed and explained: how the
panel maps physical hardware to its 1-176 zone and 1-511 unit
address spaces. Documents the four overlapping families that share
the unit number line -- X-10 (1-256, by house code), ALC bus
(parallel address space within those slots), physical outputs
(257-392), and panel flags (393-511) -- which is why the working
panel reports units at index 257+ (sprinkler outputs on the first
17A00 expansion enclosure) and 393+ (named flags) even though only
a dozen lights are wired up.
Closes a real debugging mystery from Phase 2/3 of the v1+UDP work:
OmniClientV1's long-form RequestUnitStatus path (BE u16 start/end)
exists specifically to address units > 255, which only happens
because of this firmware-fixed address layout.
astro.config.mjs
Slot both new pages into the existing Reference and Explanation
sidebar groups.
The previous homepage opened with prose ('What it is') and listed three
generic 'where to start' cards. New version is more concrete:
- Hero block with three call-to-action buttons (Add to HA, Decode .pca,
Source on Gitea) using Starlight's hero frontmatter
- '## In Home Assistant' section opens with what users actually get,
followed by a 2x2 grid of clickable HA screenshots (integrations
list, integration page, device controls, dev-tools states) — each
thumbnail links to the relevant detail page with a short caption
- '## In Python' section: 15-line OmniClient + events() example so
the library shape is immediately legible, plus four bullets
highlighting opcode coverage / typed dataclasses / mock controller
/ async-first
- '## Two non-public protocol quirks' kept verbatim — still the
headline RE finding
- '## Read the in-depth content' replaces the three generic cards
with six specific ones routing to install how-to, dev-stack
tutorial, protocol reference, quirks explainer, file format,
and journey
Build clean, 23 pages. Local container rebuilt + recreated. The
new sections render: In Home Assistant, In Python, Two non-public
protocol quirks, Read the in-depth content.
Aligns with the warehack-ing host convention (matches birdcage-docs):
Dockerfile — multi-stage with named targets:
base shared npm ci + COPY
dev npx astro dev --host 0.0.0.0 --port 4321 (HMR enabled)
build npx astro build (produces /app/dist)
prod caddy:2-alpine serves /srv with /health probe + HEALTHCHECK
docker-compose.yml — picks the target via APP_ENV in .env:
build.target = ${APP_ENV:-dev}
caddy.reverse_proxy upstream port = ${APP_PORT:-4321}
Adds the streaming/HMR caddy labels CLAUDE.md requires for Vite-over-
Caddy: flush_interval=-1, transport.read/write_timeout=0, keepalive,
stream_timeout=24h, stream_close_delay=5s
Volume mounts ./src, ./public, astro.config.mjs (live in dev, harmless
in prod since the prod stage doesn't reference /app)
astro.config.mjs — vite.server.hmr block now picks up VITE_HMR_HOST
env var and configures host/protocol/clientPort for wss-on-443 HMR
through Caddy. Falls back to undefined when unset so plain
'npm run dev' still works.
Caddyfile — adds /health endpoint for the Dockerfile HEALTHCHECK,
SPA-style try_files fallback chain (path -> path/index.html -> /index.html).
Makefile — adds 'make dev' / 'make prod' that rewrite APP_ENV+APP_PORT
in .env then rebuild. Tail-logs after up/restart so you see startup
diagnostics without a separate 'make logs'.
.env.example — five vars (COMPOSE_PROJECT_NAME, APP_ENV, APP_PORT,
PUBLIC_DOMAIN, VITE_HMR_HOST), defaulting to prod mode against the
public hai-omni-pro-ii.warehack.ing domain. .env stays gitignored so
each environment (local, remote) can override.
Local container rebuilt + recreated, healthy, /health returns 200,
PUBLIC_DOMAIN locally still set to .l.warehack.ing in the host .env.
quickstart:
- 'pip install omni-pca' -> pip install from git+https at the v2026.5.10 tag
- uvx --from form documented for no-install runs
- note callout that PyPI publish is pending and what to switch to once it lands
install-in-home-assistant:
- Common errors table: replace generic 'check internet' fix with a
concrete pre-install command using the release wheel URL
- Known gaps: same wheel install command shown inline with HA-OS hint
Build clean, 23 pages.
src/assets/diagrams/packet-control-empty.svg
Covers the five empty-payload control types: NoMessage (0x00),
ClientRequestNewSession (0x01), ClientSessionTerminated (0x05),
ControllerSessionTerminated (0x06), ControllerCannotStartNewSession
(0x07). Single 4-byte header diagram with the type byte highlighted,
'(no payload)' shown as a dashed empty box, and all five type values
enumerated below.
src/assets/diagrams/packet-controller-ack-new-session.svg
Type 0x02. 11 bytes total — 4-byte header + 2-byte protocol version
(0x00 0x01) + 5-byte SessionID nonce. SessionID cells in accent
colour because they're what feed quirk #1's session key XOR mix.
Bottom annotation explains the proto-version is hard-coded.
src/assets/diagrams/packet-secure-session.svg
Types 0x03 and 0x04 share the same shape both directions. Two-row
layout: top shows the plaintext (5-byte SessionID echo + 11 zero-pad
cells with 'zero pad to 16 bytes'); bottom shows the wire form
(header + 16-byte AES ciphertext block). Highlights the symmetric
client-up/controller-down design.
src/assets/diagrams/packet-omnilink-message.svg
Covers all four conversation packet types in one diagram: 0x10
(v1 encrypted), 0x11 (v1 plaintext), 0x20 (v2 encrypted), 0x21 (v2
plaintext). Top row shows the encrypted variant with N x 16-byte
ciphertext blocks; brace down to bottom row showing the inner
Message format (start byte + length + opcode + data + CRC u16 LE).
Same diagram serves all 4 since the layout is identical except
for the type byte and whether AES is applied.
src/content/docs/reference/protocol.mdx
Added five new sections so every PacketType is now documented:
- NoMessage (0x00) folded into the ClientRequestNewSession heading
(same empty-payload layout)
- ClientSessionTerminated (0x05) / ControllerSessionTerminated (0x06)
/ ControllerCannotStartNewSession (0x07) get a combined section
with a one-paragraph explanation of when each fires
- OmniLinkMessage (0x10), OmniLinkUnencryptedMessage (0x11),
OmniLink2Message (0x20), OmniLink2UnencryptedMessage (0x21)
consolidated into one section with the diagram + a note about
which TCP/PC-Access actually uses
ClientRequestSecureSession + ControllerAckSecureSession merged into
one section since they share the diagram; original prose preserved
as #### subsections beneath.
Build: 23 pages clean. Protocol page now 118 KB (was 92 KB) — six
inline SVG titles confirmed via grep on the rendered HTML. Every
packet type defined in omni_pca.opcodes.PacketType (12 values) is
now visualized in the docs.
The auto-extracted manual SVGs were unusable PDF text-glyph soup. These
are fresh, theme-aware (currentColor everywhere, accent via the
--sl-color-accent CSS var), and built to teach.
src/assets/diagrams/handshake-sequence.svg
Sequence diagram with CLIENT and CONTROLLER swim lanes, five steps:
ClientRequestNewSession -> ControllerAckNewSession (carries SessionID)
-> derive SessionKey (inline note) -> ClientRequestSecureSession
(encrypted, accent-coloured) -> ControllerAckSecureSession (encrypted)
-> first OmniLink2Message. Plaintext arrows in currentColor, encrypted
arrows in accent.
src/assets/diagrams/packet-structure.svg
Bytes-on-the-wire box diagram: outer Packet header (seq u16 + type +
reserved + encrypted payload) decomposed below into the inner Message
(start byte 0x21, length, opcode, data, CRC u16 LE). Plain vs encrypted
fields colour-coded with a legend.
src/assets/diagrams/session-key-derivation.svg
Quirk #1 visual. Three rows of byte cells: ControllerKey (16 bytes,
with bytes 0..10 in plain colour and 11..15 highlighted), SessionID
(5 bytes), and the resulting SessionKey with the XOR boundary
visible. XOR operator in the accent colour to draw the eye.
src/assets/diagrams/per-block-whitening.svg
Quirk #2 visual. seq pill at the top, three blocks below (block 1,
block 2, block N) each showing 16 byte cells with the first two
highlighted in accent and labelled with the seq XOR mask. Drives home
that it's the SAME mask on EVERY block.
src/assets/diagrams/architecture.svg
Three groups (LIBRARY, HA INTEGRATION, TEST SURFACE) with boxes
inside. Library shows the four protocol-layer modules + connection +
client + models + events. HA shows coordinator + 8 platforms. Test
surface shows MockPanel (accent-coloured), HA test harness, e2e tests,
unit tests. One accent-coloured arrow runs from OmniConnection across
to MockPanel labelled 'TCP/4369 (encrypted)'.
src/assets/diagrams/pca-file-format.svg
Key chain: hardcoded keyPC01 -> decrypts PCA01.CFG (boxes for the
CFG fields including the highlighted pca_key) -> arrow showing the
extracted pca_key -> decrypts the .pca file (boxes for PCA03 magic,
account info, model byte, body, and the highlighted ControllerKey)
-> caption 'feeds session-key derivation (quirk #1)'.
Wired in via inline-SVG-via-?raw-import + set:html (so currentColor
adapts to the theme). Required converting four pages to .mdx:
reference/protocol.mdx + handshake + packet diagrams
reference/file-format.mdx + pca-file-format diagram
explanation/quirks.mdx + session-key + whitening diagrams
explanation/architecture.mdx + architecture diagram
Two MDX paper cuts during conversion: bare '<100ms' and '<50ms' in
architecture.mdx confused the JSX parser; backticked them as .
Build: 23 pages clean. Verified inline SVG ships in the rendered HTML
(grep for SVG title IDs returns 2/2 hits per relevant page). Container
rebuilt + redeployed. Protocol page is now 92750 bytes (was ~63000),
quirks page 84156 (was ~63000).
src/content/docs/how-to/install-in-home-assistant.md (~900 w)
Sidebar order 0 so it appears first in the How-to section. Covers:
- Prereqs: HA 2026.1+, panel reachable, ControllerKey
- Backup-first nudge before adding any custom component
- HACS install path (custom repo until upstream publish)
- Manual git+cp path that works on every HA install type
- The 'untested custom integration' WARNING is expected
- Add-via-UI walkthrough with screenshots of integration page +
device page
- Customise: entity rename, areas, dashboards, logbook
- Common errors table (4 toast messages and their fixes)
- 'Loads but no entities' deep-dive (3 ordered causes)
- Reauth flow when ControllerKey rotates
- Cross-links to the other how-tos and entity/service references
- Known gaps: HACS not published yet, omni-pca not on PyPI yet,
live panel validation pending
23 pages now (was 22). Sidebar autogenerates so it appears at the top
of How-to without further config.
src/content/docs/tutorials/ (3 pages, learning-oriented)
decrypt-your-pca.md (~600 w)
Walks the user through installing the CLI and running decode-pca
against a real .pca file, ending with --include-pii to confirm the
decryption landed on real plaintext (their own customer name).
Cites the file format reference for what's actually happening.
dev-stack.md (~700 w)
Boots the docker dev stack, onboards HA in 60 seconds, adds the
integration with the documented host/port/key, then five concrete
things to try (toggle a light, arm an area with right and wrong
code, trigger a button, watch developer states). Includes the
panel-device screenshot.
first-script.md (~750 w)
Twenty-line Python script: connect, get system info, walk zones,
then evolves through three more steps to add an event-stream
consumer and a command dispatch. Shows ASCII output so the user
knows what to expect on success. Cross-links the two protocol
quirks pages.
src/content/docs/how-to/ (6 pages, task-oriented recipes)
find-controller-key.md (~400 w)
Four ways: from .pca file, PC Access UI, panel keypad, generate
a new one. Plus a smoke-test command to verify the key works.
automate-on-alarm.md (~600 w)
HA event automation pattern keyed off the omni_pca event entity's
event_type / event_data attributes. Includes an alarm-type-specific
filter table and a fire-alarm worked example.
bypass-zone.md (~400 w)
Three flavours: HA service call, HA per-zone switch entity, raw
Python. Includes verification snippets and caveats around
installer-disabled bypass and code requirements.
send-panel-message.md (~350 w)
show_message / clear_message services with a 'laundry done'
automation example. Notes that Omni messages are pre-programmed in
PC Access, not free-form.
decode-a-packet.md (~750 w)
Step-by-step: take a hex dump, decode the outer Packet, derive the
session key from the ack, decrypt with per-block whitening,
decode the inner Message, dispatch on opcode. Includes tcpdump
capture commands at the end.
migrate-from-jomnilinkii.md (~700 w)
What changes when swapping from jomnilinkII / pyomnilink to
omni-pca. Method-name translation table, async-vs-sync surface,
pattern-matching event handler example, what's gained (quirks,
types, mock) and what's lost (years of production hardening).
Build: 22 pages clean (was 13), sitemap regenerated, Pagefind index
covers everything. Container rebuilt + recreated; verified
/how-to/automate-on-alarm/ returns HTTP 200 with the right title.
Sidebar autogenerates from the directories so all nine pages appear
without further config.
src/assets/manual/omnipro-ii-wordmark.png
Lifted from the Owner's Manual cover (Owner_s_Manual_page_1_img_1).
1280x592, 13KB, 8-bit grayscale. Used as the eager-loaded hero on
index.mdx via Astro's Image component, with CSS filter:invert(1) so
the black-on-white wordmark renders crisp on the dark theme.
src/assets/screenshots/ (six PNGs, copied from omni-pca/dev/artifacts):
01-overview.png HA Lovelace
02-integrations-list.png HAI/Leviton tile in the integrations list
03-omni-pca-config.png '1 device, 38 entities' integration page
04-panel-device.png Omni Pro II device page with all controls
05-entities-omni.png config-entry filtered entity table
06-developer-states.png alarm_control_panel.omni_pro_ii_main raw
attributes from Developer Tools
Wired into pages:
index.mdx wordmark hero (eager load)
start/quickstart.md 04-panel-device.png at the bottom of
step 3 so the reader sees the payoff
reference/ha-entities.md new 'What it looks like in HA' section
with four screenshots (integrations
list, integration detail, device page,
developer states)
Astro Image processed all screenshots into webp at request size:
06-developer-states 188 KB -> 91 KB after VP8 encoding.
Build: 13 pages clean in 1.66s, sitemap and Pagefind index regenerated.
Container rebuilt + recreated; verified HTTP 200 with 33315-byte index
page and the /reference/ha-entities/ page references four /_astro/*.webp
URLs that all return 200 from the running container.
The .l.warehack.ing subdomain is the convention for the user's local
services. DNS for hai-omni-pro-ii.l.warehack.ing already resolves to
the host; caddy-docker-proxy picks up the new caddy label and routes
appropriately.
Patched .env, .env.example, docker-compose.yml, README.md,
astro.config.mjs.
Container rebuilt and restarted; verified caddy-docker-proxy is
serving 200 OK with 33051 bytes of rendered HTML on the internal
network, and the host-level Caddy issues a 308 redirect to https://
on the new hostname.
src/content/docs/ — twelve pages totalling ~18,800 words, ported from
the omni-pca repo's docs and reference material:
index.mdx (377 w) landing page with three CardGrid links
start/quickstart.md (572 w) three flows: decode .pca / talk to
panel / install in HA
reference/protocol.md (2525 w) byte-level Omni-Link II spec, full
packet+message layouts, the two
non-public quirks, opcode tables
reference/file-format.md (1593 w) XOR-LCG cipher, key derivation,
PCA01.CFG schema, .pca PCA03 header
reference/library-api.md (1170 w) module-by-module Python API summary
reference/ha-entities.md (1070 w) per-platform entity catalogue
reference/ha-services.md (567 w) seven services + automation YAML
explanation/quirks.md (1448 w) the headline RE essay — session-key
XOR mix + per-block whitening, why
no public client documents them
explanation/architecture.md (1123 w) library + HA + mock + tests
explanation/pc-access-bug.md (1131 w) LargeVocabulary off-by-N
journey.md (6194 w) chronological retrospective ported
from omni-pca/docs/JOURNEY.md
changelog.md (1213 w) full 2026.5.10 release notes
Dockerfile — pinned node:lts-alpine and caddy:latest (registry-1
.docker.io was returning 'tls: internal error' on node:25-alpine and
caddy:2-alpine pulls; the pinned tags are cached locally and work).
TODO comment notes to bump back to node:25 once registry stabilises.
.gitignore — added .env / .env.local just in case.
Build: 13 pages built clean in 1.83s, sitemap + Pagefind search index
emitted. Container runs at hai-omni-docs-docs (caddy network), accepts
requests with Host: hai-omni-pro-ii.warehack.ing, returns rendered
Starlight HTML with title/description meta intact. Once DNS for
hai-omni-pro-ii.warehack.ing points at the host, the site is live.
Astro 6 + Starlight 0.39 documentation site for omni-pca, organised
around the Diatáxis framework (Tutorials / How-to / Reference /
Explanation), plus a chronological Journey page and Changelog.
Theme: muted slate-blue with amber accents. astro-icon + lucide
preinstalled. Astro telemetry and Starlight devToolbar both off.
Deployment: multi-stage Dockerfile (node:25-alpine builder ->
caddy:2-alpine runtime), inner Caddy serves static dist on :80,
outer caddy-docker-proxy on the host terminates TLS for
hai-omni-pro-ii.warehack.ing.