diff --git a/src/assets/screenshots/programs-detail.png b/src/assets/screenshots/programs-detail.png
new file mode 100644
index 0000000..345a7f8
Binary files /dev/null and b/src/assets/screenshots/programs-detail.png differ
diff --git a/src/assets/screenshots/programs-editor-chain.png b/src/assets/screenshots/programs-editor-chain.png
new file mode 100644
index 0000000..569fbbf
Binary files /dev/null and b/src/assets/screenshots/programs-editor-chain.png differ
diff --git a/src/assets/screenshots/programs-editor-compact.png b/src/assets/screenshots/programs-editor-compact.png
new file mode 100644
index 0000000..dae21ef
Binary files /dev/null and b/src/assets/screenshots/programs-editor-compact.png differ
diff --git a/src/assets/screenshots/programs-editor-structured.png b/src/assets/screenshots/programs-editor-structured.png
new file mode 100644
index 0000000..a5ada69
Binary files /dev/null and b/src/assets/screenshots/programs-editor-structured.png differ
diff --git a/src/assets/screenshots/programs-list.png b/src/assets/screenshots/programs-list.png
new file mode 100644
index 0000000..0d2d58a
Binary files /dev/null and b/src/assets/screenshots/programs-list.png differ
diff --git a/src/content/docs/how-to/edit-programs-in-ha.mdx b/src/content/docs/how-to/edit-programs-in-ha.mdx
new file mode 100644
index 0000000..b3cb1d1
--- /dev/null
+++ b/src/content/docs/how-to/edit-programs-in-ha.mdx
@@ -0,0 +1,206 @@
+---
+title: Edit panel programs from Home Assistant
+description: Browse, search, clone, and edit the panel's built-in automation programs from a Home Assistant side panel — no PC Access required.
+sidebar:
+ order: 5
+---
+
+import { Image } from 'astro:assets';
+import shotList from '../../../assets/screenshots/programs-list.png';
+import shotDetail from '../../../assets/screenshots/programs-detail.png';
+import shotCompact from '../../../assets/screenshots/programs-editor-compact.png';
+import shotChain from '../../../assets/screenshots/programs-editor-chain.png';
+import shotStructured from '../../../assets/screenshots/programs-editor-structured.png';
+
+The integration registers a side-panel item called **Omni Programs**
+that lets you view and edit the panel's built-in automation programs
+without opening PC Access. These are the **panel-side** programs that
+run on the Omni controller itself (so they survive HA outages), as
+opposed to HA automations which run inside HA.
+
+Open Home Assistant in your browser. **Omni Programs** appears in the
+left sidebar alongside Overview, Map, Energy, etc.
+
+
+
+## Browsing & filtering
+
+The header shows the total program count (`330 programs` on the panel
+this screenshot was taken against). Each row is one program — for
+multi-record clausal chains the row shows the **head** slot and
+collapses the chain into a single summary line.
+
+- **Search** — typing into the search box filters by any visible text
+ in the summary (program names, object names, command labels).
+- **Trigger-type chips** — `TIMED` / `EVENT` / `YEARLY` / `WHEN` / `AT`
+ / `EVERY` / `REMARK`. Click to toggle each filter; combinations are
+ OR'd. The right-edge badge on each row tells you the trigger type
+ and condition count.
+- **References filter** — programs referencing a specific object
+ (zone, unit, area, button, thermostat) can be filtered from the HA
+ entity page: open any zone/unit/etc. entity, scroll to *Related*,
+ and click the program count to jump into the side panel pre-filtered
+ to that object.
+
+The list updates in real time as the panel pushes program changes —
+no manual refresh needed.
+
+## The detail panel
+
+Click any row to open the detail panel on the right.
+
+
+
+The structured-English breakdown is the same renderer that produces
+the row summary, but expanded — one clause per line, conditions in
+context.
+
+Four actions:
+
+- **Fire now** — executes the program's THEN action immediately,
+ bypassing any conditions or schedule. Equivalent to PC Access's
+ "Test" button.
+- **Edit** — opens the inline editor (see below).
+- **Clone…** — duplicates the program into a different slot. Prompts
+ for the target slot number; refuses to overwrite a non-empty slot.
+- **Clear** — wipes the slot to all zeros (`FREE`). Confirmation
+ required; this is sent to the panel as `DownloadProgram` with a
+ zero body, matching PC Access's "Delete" behaviour.
+
+## Editing a compact-form program
+
+Compact-form programs (`TIMED`, `EVENT`, `YEARLY`, `REMARK`) fit
+entirely in one 14-byte record — the editor lays them out as a single
+form with trigger fields up top, the action in the middle, and up to
+two inline AND-IF conditions at the bottom.
+
+
+
+The form adapts to the trigger type:
+
+- **TIMED** — hour / minute (or sunrise/sunset offset) + days-of-week
+ bitmask.
+- **EVENT** — a category dropdown (Button press / Zone state change /
+ Unit state change / Fixed event), then category-specific sub-fields.
+ For example, *Button press* shows a button picker; *Zone state
+ change* shows zone + becomes-state.
+- **YEARLY** — month + day-of-month picker.
+- **REMARK** — read-only in this release (see [known
+ limits](#known-limits) below).
+
+The action section is a command picker plus a parameter input whose
+shape follows the command. *Turn ON unit* shows a unit picker; *Arm
+area Night* shows an area picker; *Execute button* shows a button
+picker.
+
+The two inline AND-IF slots each get a Family dropdown (Zone / Unit /
+Time clock / Misc / Area in security mode) and per-family sub-fields.
+
+Hit **Save** to write the program back to the panel via
+`DownloadProgram`. The integration validates the form server-side
+before sending: bad ranges produce a structured error, not a corrupt
+program.
+
+## Editing a multi-record chain
+
+Programs with `prog_type` ≥ 5 (`WHEN` / `AT` / `EVERY`) are
+**clausal chains** — one record per visual line, occupying sequential
+slots in the program table. The editor presents the whole chain as
+one logical unit even though it's stored across multiple slots.
+
+
+
+The editor has three sections:
+
+- **Trigger header** (top) — the `WHEN` / `AT` / `EVERY` record.
+- **Conditions** — zero or more `AND IF` / `OR IF` rows. Use the
+ `+ AND IF` button to add a row to the current group, or `+ OR IF` to
+ start a new alternative group. Each row has an `×` button to remove
+ it.
+- **Actions** — one or more `THEN` rows. Use `+ THEN` to add another
+ action; chains can fire multiple actions in sequence.
+
+**Anti-trample** — when you Save, the editor writes only the slots
+needed for the current chain layout. If you remove a condition or
+action, the slots it used to occupy are explicitly cleared so no
+half-stale records linger on the panel.
+
+## Structured-AND conditions
+
+Most AND IF rows use the **Traditional** form (family + instance +
+operand — "Zone 5 is SECURE", "Unit 12 is ON", etc). But the panel
+also supports **Structured** AND records that compare a typed field
+against either a constant or another typed field:
+
+> AND IF Thermostat 1 Current temperature **>** Thermostat 2 Current temperature
+
+The editor renders these as a dedicated form with operator picker
+and per-arg type pickers:
+
+
+
+**Arg1** can be any of: Zone, Unit, Thermostat, Area, Time/Date.
+Each type has its own field selector — for Thermostat that's
+*Current temperature / Heat setpoint / Cool setpoint / System mode /
+Humidity / …*; for Zone it's *Loop reading / Current state / …*.
+
+**Operator** is one of: `==`, `!=`, `<`, `>`, `is odd`, `is even`,
+`is multiple of`, `in (bitmask)`, `not in (bitmask)`. The unary
+operators (`is odd` / `is even`) hide the Arg2 controls entirely.
+
+**Arg2** can be:
+
+- **Constant** (default for new structured rows) — a 0..65535 value.
+ Used for "TEMP > 70", "Zone.CurrentState == 1", etc.
+- **A typed reference** — Zone / Unit / Thermostat / Area / TimeDate
+ with its own field selector. This is how you author cross-object
+ comparisons like "Thermostat 1 temp > Thermostat 2 temp" or
+ "Zone 1 reading > Zone 2 reading".
+
+Switching Arg2 from Constant to a reference type re-populates the
+form with the appropriate picker; switching back preserves the
+numeric value.
+
+For the wire format behind all this, see the
+[Program record format reference](/reference/program-format/#structured-op-and-records-op--0).
+
+## Known limits
+
+The editor covers the common cases but a few program shapes are
+deliberately presented read-only — they're preserved verbatim on
+Save, you just can't reshape them in the form:
+
+- **REMARK programs** — read-only. The `remark_id` → text lookup
+ table layout is documented but the editor doesn't expose it yet.
+- **Exotic Arg1/Arg2 types** on structured-AND — `UserSetting`,
+ `Auxiliary`, `Audio`, `AccessControl`, `Message`, `System`. These
+ show as a read-only banner with an `×` remove button.
+- **Non-zero `CompConst`** on structured-AND — rarely used; preserved
+ but not exposed as a form control.
+- **Multi-panel installs** — the side panel currently shows the
+ first configured panel. If you have multiple Omni panels, the
+ other panels' programs aren't accessible from the side panel yet.
+
+## Where the data lives
+
+The side panel is a Lit web component (``)
+registered via Home Assistant's `panel_custom` integration. It talks
+to the integration's websocket API directly — there's no separate
+state machine, no caching, no debouncing. Each user action becomes
+one websocket round-trip:
+
+| Action | Websocket command |
+|---|---|
+| List view + filters | `omni_pca/programs/list` |
+| Open detail panel | `omni_pca/programs/get` |
+| Fire now | `omni_pca/programs/fire` |
+| Save (compact) | `omni_pca/programs/write` |
+| Save (chain) | `omni_pca/programs/write_chain` |
+| Clone… | `omni_pca/programs/clone` |
+| Clear | `omni_pca/programs/clear` |
+
+All seven commands validate input server-side and produce structured
+errors (no opaque tracebacks) on bad input. The websocket layer's
+own tests cover the contract — see `tests/ha_integration/
+test_program_websocket.py` in the source tree if you want to extend
+the API surface.
diff --git a/src/content/docs/reference/ha-entities.md b/src/content/docs/reference/ha-entities.md
index 7dcb9f5..9ddc951 100644
--- a/src/content/docs/reference/ha-entities.md
+++ b/src/content/docs/reference/ha-entities.md
@@ -10,6 +10,13 @@ refresh; live state propagates over the panel's unsolicited push channel
within one TCP round-trip, with a 30-second poll backstopping anything that
didn't push.
+In addition to the entity catalogue below, the integration registers an
+**Omni Programs** sidebar item — a dedicated UI for browsing, editing,
+cloning, and firing the panel's built-in automation programs (the
+panel-side rules that run on the controller itself, distinct from HA
+automations). See the [edit-programs-in-ha
+how-to](/how-to/edit-programs-in-ha/) for the walkthrough.
+
| Platform | Entity | Per |
|---|---|---|
| `alarm_control_panel` | Area arm/disarm with code | discovered area |
diff --git a/src/content/docs/reference/program-format.mdx b/src/content/docs/reference/program-format.mdx
index 10fc978..ead8a88 100644
--- a/src/content/docs/reference/program-format.mdx
+++ b/src/content/docs/reference/program-format.mdx
@@ -347,7 +347,7 @@ The block fits in **one** 14-byte record. `prog_type` is `TIMED`,
- For `EVENT`, the event identifier replaces the calendar
month/day at bytes 9-10 (see [the EVENT `Evt` u16 section](#also-for-event-bytes-910-are-an-event-identifier-not-a-date)).
-This is how 100% of records in any panel running firmware <3.0 are
+This is how 100% of records in any panel running firmware before 3.0 are
encoded. It is also how PC Access encodes any block that fits the
constraints, even on firmware ≥3.0 — see the simplification rules
in `frmAutomationEditBlock.cs:589 SimplifyLines`.
@@ -398,7 +398,7 @@ The firmware gate is `Features.Add(MultiLinePrograms, 196608u)` in
`clsCapOMNI_PRO_II.cs:290` (the 24-bit value packs as
`major*65536 + minor*256 + revision` → 3.0.0).
-When MultiLinePrograms is OFF (firmware <3.0):
+When MultiLinePrograms is OFF (firmware before 3.0):
- PC Access's `Or` toolbar button and "Add Comment Block" menu item
are disabled.
@@ -420,7 +420,7 @@ load on a real 2.16A panel.
| Byte(s) | Field | Notes |
| --- | --- | --- |
| 0 | `prog_type` | = 5 |
-| 9-10 (BE u16) | event-id | `(family << 8) | instance` — same encoding as compact-form `EVENT`'s bytes 9-10 in wire form. **No** Mon/Day file-form swap. |
+| 9-10 (BE u16) | event-id | `(family \<\< 8) \| instance` — same encoding as compact-form `EVENT`'s bytes 9-10 in wire form. **No** Mon/Day file-form swap. |
| 1-8, 11-13 | zeros | (action lives in a separate `THEN` record) |
#### `AT` (ProgType=6) — single-occurrence time trigger