docs: reference/program-format — wire layout + Mon/Day quirk + worked example
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.
This commit is contained in:
parent
814d945f6d
commit
f45e9fd014
@ -62,6 +62,7 @@ export default defineConfig({
|
||||
items: [
|
||||
{ label: 'Protocol', slug: 'reference/protocol' },
|
||||
{ label: 'File format', slug: 'reference/file-format' },
|
||||
{ label: 'Program record format', slug: 'reference/program-format' },
|
||||
{ label: 'Hardware specs', slug: 'reference/hardware-specs' },
|
||||
{ label: 'Library API', slug: 'reference/library-api' },
|
||||
{ label: 'HA entities', slug: 'reference/ha-entities' },
|
||||
|
||||
253
src/content/docs/reference/program-format.mdx
Normal file
253
src/content/docs/reference/program-format.mdx
Normal file
@ -0,0 +1,253 @@
|
||||
---
|
||||
title: Program record format
|
||||
description: Wire layout of the Omni Pro II's 14-byte program record — the panel-side automation engine's storage unit. Includes the Mon/Day file-format swap, the Remark variant, and a worked example.
|
||||
---
|
||||
|
||||
A "program" in Omni Pro II vocabulary is one line of the panel's built-in
|
||||
automation engine: roughly "if **X** happens at time **Y**, do **Z**." The
|
||||
panel stores up to **1500 programs** in a fixed-size table — in a `.pca`
|
||||
file they live in a 21,000-byte block (1500 × 14 bytes); on the wire they
|
||||
are exchanged one at a time via `clsOLMsgProgramData` /
|
||||
`clsOL2MsgProgramData`. PC Access exposes a visual editor for them; the HA
|
||||
integration currently surfaces them as raw decoded fields only.
|
||||
|
||||
The user-facing model (every program line has a **WHEN**, an optional **&IF**
|
||||
condition, and a **THEN**) is documented in the *Owner's Manual* under
|
||||
"Programming" and the *Installation Manual* under "SETUP MISC → Programs".
|
||||
|
||||
## The 14-byte record
|
||||
|
||||
The Omni Pro II has the `DoubleProgramConditional` feature flag set so every
|
||||
record is **14 bytes** with two condition slots. (Models without the flag
|
||||
would use 12 bytes with one cond — we don't yet support those.)
|
||||
|
||||
| Offset | Field | Type | Notes |
|
||||
|-------:|---|---|---|
|
||||
| 0 | `prog_type` | byte | `ProgramType` enum |
|
||||
| 1-2 | `cond` | BE u16 | Primary condition (opaque this pass) |
|
||||
| 3-4 | `cond2` | BE u16 | Secondary condition (opaque this pass) |
|
||||
| 5 | `cmd` | byte | [`Command`](/reference/library-api/) enum |
|
||||
| 6 | `par` | byte | Command parameter (level, mode, …) |
|
||||
| 7-8 | `pr2` | BE u16 | Secondary parameter (usually object number) |
|
||||
| 9-10 | `month`, `day` | bytes | **Swapped to `day, month` on disk for `EVENT` type** — see below |
|
||||
| 11 | `days` | byte | `Days` bitmask (Mon=`0x02` … Sun=`0x80`) |
|
||||
| 12 | `hour` | byte | 0-23 for absolute time; offset for sunrise/sunset-relative |
|
||||
| 13 | `minute` | byte | 0-59 |
|
||||
|
||||
### `Remark` variant
|
||||
|
||||
When `prog_type == REMARK` (4) the bytes at offsets 1-4 hold a single
|
||||
**32-bit BE RemarkID** instead of cond + cond2:
|
||||
|
||||
| Offset | Field | Type |
|
||||
|-------:|---|---|
|
||||
| 0 | `prog_type` (= 4) | byte |
|
||||
| 1-4 | `remark_id` | BE u32 |
|
||||
| 5-13 | (unused, typically zero) | bytes |
|
||||
|
||||
The lookup from `remark_id` to the user-visible remark string lives in a
|
||||
separate table on disk that we have not yet reverse-engineered.
|
||||
|
||||
## Enums
|
||||
|
||||
### `ProgramType` (byte 0)
|
||||
|
||||
| Value | Name | Meaning |
|
||||
|------:|---|---|
|
||||
| 0 | `FREE` | Unused slot (all 14 bytes zero) |
|
||||
| 1 | `TIMED` | Fires at a specific time of day on selected weekdays |
|
||||
| 2 | `EVENT` | Fires when a panel event occurs (zone open, X-10, …) |
|
||||
| 3 | `YEARLY` | Fires on a specific calendar date each year |
|
||||
| 4 | `REMARK` | Stores a RemarkID + free-text association |
|
||||
| 5 | `WHEN` | Connector — string multiple records into one line *(RE-pending)* |
|
||||
| 6 | `AT` | Connector *(RE-pending)* |
|
||||
| 7 | `EVERY` | Connector *(RE-pending)* |
|
||||
| 8 | `AND` | Connector *(RE-pending)* |
|
||||
| 9 | `OR` | Connector *(RE-pending)* |
|
||||
| 10 | `THEN` | Connector *(RE-pending)* |
|
||||
|
||||
Source: `enuProgramType.cs`.
|
||||
|
||||
### `ProgramCond` (high bits of `cond` / `cond2`)
|
||||
|
||||
| Value | Name | Family |
|
||||
|------:|---|---|
|
||||
| 0 | `OTHER` | catch-all / miscellaneous |
|
||||
| 4 | `ZONE` | zone-state condition |
|
||||
| 8 | `CTRL` | control-unit condition |
|
||||
| 12 | `TIME` | time-clock condition |
|
||||
| 16 | `SEC` | security-mode condition |
|
||||
|
||||
Source: `enuProgramCond.cs`. The internal bit-split of `cond` (selector vs
|
||||
operand within the 16-bit field) is **not yet decoded** — see "What we don't
|
||||
yet know" below.
|
||||
|
||||
### `Days` (byte 11)
|
||||
|
||||
Bitmask with Monday as the *low-order valid bit* — not bit 0:
|
||||
|
||||
| Bit | Name |
|
||||
|-----|---|
|
||||
| `0x02` | Monday |
|
||||
| `0x04` | Tuesday |
|
||||
| `0x08` | Wednesday |
|
||||
| `0x10` | Thursday |
|
||||
| `0x20` | Friday |
|
||||
| `0x40` | Saturday |
|
||||
| `0x80` | Sunday |
|
||||
|
||||
Source: `enuDays.cs`. `0x00` = no day selected.
|
||||
|
||||
## Worked example
|
||||
|
||||
Take this slot from our live fixture (slot 22):
|
||||
|
||||
```
|
||||
01 8d 09 9b 09 44 03 01 00 08 0c 3e 07 0f
|
||||
```
|
||||
|
||||
Decoded as a file record:
|
||||
|
||||
| Offset | Bytes | Field | Value |
|
||||
|-------:|:--|:--|:--|
|
||||
| 0 | `01` | `prog_type` | `TIMED` (1) |
|
||||
| 1-2 | `8d 09` | `cond` | `0x8d09` |
|
||||
| 3-4 | `9b 09` | `cond2` | `0x9b09` |
|
||||
| 5 | `44` | `cmd` | `0x44` (raw — opcode is one of the many `enuUnitCommand` values) |
|
||||
| 6 | `03` | `par` | 3 |
|
||||
| 7-8 | `01 00` | `pr2` | 256 |
|
||||
| 9 | `08` | `month` (TIMED → no swap) | 8 |
|
||||
| 10 | `0c` | `day` | 12 |
|
||||
| 11 | `3e` | `days` | `Mon\|Tue\|Wed\|Thu\|Fri` (weekdays) |
|
||||
| 12 | `07` | `hour` | 07 |
|
||||
| 13 | `0f` | `minute` | 15 |
|
||||
|
||||
So: "TIMED program firing at 07:15 on weekdays, doing command 0x44 with
|
||||
par=3 and pr2=256, gated by the condition pair 0x8d09 / 0x9b09."
|
||||
|
||||
## Quirk: the `EVENT` Mon/Day swap
|
||||
|
||||
For `EVENT`-typed programs on disk only, bytes 9 and 10 are stored in the
|
||||
order `[day, month]` instead of `[month, day]`. The on-the-wire
|
||||
`clsOLMsgProgramData` reply *does not* apply this swap — only
|
||||
`clsProgram.Read` / `Write` (the file-IO methods) do, at
|
||||
`clsProgram.cs:471-484` and `:506-515`.
|
||||
|
||||
The Python decoder hides this:
|
||||
|
||||
```python
|
||||
from omni_pca.programs import Program
|
||||
|
||||
# These two byte strings are the same EVENT program — on disk vs on wire.
|
||||
disk_bytes = bytes.fromhex("02 0c04 0000 01 01 0000 05 0c 00 07 0f".replace(" ", ""))
|
||||
wire_bytes = bytes.fromhex("02 0c04 0000 01 01 0000 0c 05 00 07 0f".replace(" ", ""))
|
||||
|
||||
p_disk = Program.from_file_record(disk_bytes)
|
||||
p_wire = Program.from_wire_bytes(wire_bytes)
|
||||
|
||||
assert p_disk.month == 12 and p_disk.day == 5
|
||||
assert p_wire.month == 12 and p_wire.day == 5
|
||||
assert p_disk == p_wire # same semantic Program
|
||||
```
|
||||
|
||||
The encoder re-applies the swap on the way out, so file/wire round-trip
|
||||
stability is preserved.
|
||||
|
||||
## Also for EVENT: bytes 9/10 are an event identifier, not a date
|
||||
|
||||
`clsProgram.Evt` (line 152) defines a u16 view: `Evt = (Mon << 8) | Day`.
|
||||
For `EVENT`-typed programs the calendar-month/calendar-day interpretation
|
||||
is a misnomer — those two bytes encode a 16-bit *event identifier* (which
|
||||
zone triggered, which X-10 code received, etc.). The dataclass exposes
|
||||
`event_id` as a convenience:
|
||||
|
||||
```python
|
||||
p = Program.from_wire_bytes(wire_bytes)
|
||||
if p.prog_type == ProgramType.EVENT:
|
||||
print(f"event_id = 0x{p.event_id:04x}")
|
||||
```
|
||||
|
||||
The selector/operand decoding inside `event_id` is part of the same
|
||||
"what we don't know" set as `cond` semantics.
|
||||
|
||||
## Wire vs on-disk parity
|
||||
|
||||
| | `.pca` file (slot in table) | `clsOLMsgProgramData` (wire) |
|
||||
|---|---|---|
|
||||
| Total bytes | 14 (slot index implicit) | 16 (prefix + body) |
|
||||
| Prefix | — | `[program_number_hi, program_number_lo]` BE u16 |
|
||||
| Body | 14 bytes as documented above | 14 bytes — **never** swaps Mon/Day |
|
||||
| Identified by | Position in 1500-slot block | `clsOLMsgProgramData.ProgramNumber` (Data[1-2]) |
|
||||
|
||||
The `omni_pca.programs` module distinguishes:
|
||||
|
||||
```python
|
||||
Program.from_wire_bytes(body) # for OmniLink message replies
|
||||
Program.from_file_record(body) # for .pca table slots
|
||||
```
|
||||
|
||||
with matching `encode_wire_bytes()` / `encode_file_record()` round-trip
|
||||
methods.
|
||||
|
||||
## What we don't yet know
|
||||
|
||||
This page covers the byte-level mechanics. The semantic decoding of
|
||||
`cond` / `cond2` — what zone number, security mode, or time clock a
|
||||
specific 16-bit value refers to — is a separate reverse-engineering
|
||||
pass. So is the multi-record clausal encoding hinted at by the
|
||||
`WHEN/AT/EVERY/AND/OR/THEN` connector values in `ProgramType`. Concretely:
|
||||
|
||||
- **`cond` / `cond2` internal bit split.** The high bits encode the
|
||||
`ProgramCond` family (Zone / Ctrl / Time / Sec); we don't yet know
|
||||
where the selector index (zone number, etc.) and the operand
|
||||
("not ready", "Day mode", …) live in the low bits. None of the 330
|
||||
defined programs in our fixture is enough to triangulate this — we'd
|
||||
need to author known programs in PC Access and diff the exported
|
||||
bytes.
|
||||
- **Multi-record clausal encoding.** No program in our live fixture uses
|
||||
the `WHEN / AT / EVERY / AND / OR / THEN` ProgType values — so we
|
||||
can't yet say whether they reference adjacent slots, use extra bytes
|
||||
within a single slot, or live in some separate clause table.
|
||||
- **RemarkID → RemarkText lookup.** The remark-text table on disk has
|
||||
not been located; `RemarkID` decoded fine, but we can't resolve it
|
||||
to a string today.
|
||||
- **`DoubleProgramConditional` capability flag.** We hardcode 14-byte
|
||||
records for the Omni Pro II. Other models may use the 12-byte form.
|
||||
Locating where the panel advertises the flag (and which non-OPII
|
||||
models clear it) is its own follow-up.
|
||||
- **TIMED time-of-day encoding.** Some TIMED programs in the live
|
||||
fixture have `hour > 23` or `minute > 59`, suggesting bytes 12/13
|
||||
also encode sunrise/sunset-relative offsets (Owner's Manual:
|
||||
±0-120 minutes). The flag distinguishing absolute time from
|
||||
relative offset has not been isolated.
|
||||
|
||||
## Python usage
|
||||
|
||||
```python
|
||||
from omni_pca.pca_file import parse_pca_file, KEY_EXPORT
|
||||
from omni_pca.programs import ProgramType, iter_defined
|
||||
|
||||
acct = parse_pca_file("/path/to/Account.pca", key=KEY_EXPORT)
|
||||
|
||||
for p in iter_defined(acct.programs):
|
||||
t = ProgramType(p.prog_type).name
|
||||
print(f"slot {p.slot:4} {t:6} cmd=0x{p.cmd:02x} "
|
||||
f"{p.hour:02d}:{p.minute:02d} days=0x{p.days:02x}")
|
||||
```
|
||||
|
||||
`acct.programs` is a `tuple[Program, ...]` of length 1500; `iter_defined`
|
||||
filters to in-use slots (matches the panel's "non-FREE" definition).
|
||||
|
||||
References:
|
||||
|
||||
- `clsProgram.cs` — field accessors, the Read/Write swap for Event,
|
||||
`ToByteArray` / `FromByteArray`, and the `Evt` u16 view.
|
||||
- `enuProgramType.cs`, `enuProgramCond.cs`, `enuDays.cs` — the byte
|
||||
enums mirrored in `omni_pca.programs`.
|
||||
- `clsHAC.cs:180-549` — wire-format `clsOLMsgProgramData` (v1) and
|
||||
`:1180-1330` — `clsOL2MsgProgramData` (v2). Both prepend a 2-byte BE
|
||||
`ProgramNumber` to the 14-byte body.
|
||||
- [`.pca` file format](/reference/file-format/) — where the 21,000-byte
|
||||
Programs block sits in the body walk.
|
||||
- [Library API → `Program`](/reference/library-api/) — public surface
|
||||
for `Program.from_wire_bytes` / `from_file_record` etc.
|
||||
Loading…
x
Reference in New Issue
Block a user