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: [
|
items: [
|
||||||
{ label: 'Protocol', slug: 'reference/protocol' },
|
{ label: 'Protocol', slug: 'reference/protocol' },
|
||||||
{ label: 'File format', slug: 'reference/file-format' },
|
{ label: 'File format', slug: 'reference/file-format' },
|
||||||
|
{ label: 'Program record format', slug: 'reference/program-format' },
|
||||||
{ label: 'Hardware specs', slug: 'reference/hardware-specs' },
|
{ label: 'Hardware specs', slug: 'reference/hardware-specs' },
|
||||||
{ label: 'Library API', slug: 'reference/library-api' },
|
{ label: 'Library API', slug: 'reference/library-api' },
|
||||||
{ label: 'HA entities', slug: 'reference/ha-entities' },
|
{ 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