docs/program-format: document cond/cond2 bit split per family

New section breaking down the 5 condition families (OTHER / ZONE /
CTRL / TIME / SEC) with their bit layouts, worked examples, and a
Python usage block showing Condition.decode and Program.condition().
Removes the corresponding entry from the "what we don't yet know"
list.

Also rewords the ProgramCond table to point at the new section
instead of saying the bit split is unknown.
This commit is contained in:
Ryan Malloy 2026-05-11 22:35:05 -06:00
parent 7057fa4410
commit db3832c68c

View File

@ -95,19 +95,19 @@ for p in iter_defined(acct.programs):
Source: `enuProgramType.cs`.
### `ProgramCond` (high bits of `cond` / `cond2`)
### `ConditionFamily` (high bits of `cond` / `cond2`)
| Value | Name | Family |
|------:|---|---|
| 0 | `OTHER` | catch-all / miscellaneous |
| 0 | `OTHER` | misc-conditional (`DARK`, `AC_POWER_OFF`, …) |
| 4 | `ZONE` | zone-state condition |
| 8 | `CTRL` | control-unit condition |
| 8 | `CTRL` | control-unit (light/output) condition |
| 12 | `TIME` | time-clock condition |
| 16 | `SEC` | security-mode condition |
| 16 | `SEC` | security-mode condition (catch-all) |
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.
Family is found by `(cond >> 8) & 0xFC` — bits 2-7 of the high byte.
Source: `enuProgramCond.cs`. See ["cond / cond2 bit
split"](#condcond2-bit-split) below for the full per-family decode.
### `Days` (byte 11)
@ -152,6 +152,67 @@ Decoded as a file record:
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."
## `cond`/`cond2` bit split
The 16-bit `cond` (and `cond2`) field packs **family + selector + operand**.
The high byte's bits 2-7 (i.e. `(cond >> 8) & 0xFC`) discriminate the family;
the bottom byte and low bits of the high byte carry the rest.
| Family | Match | Bit layout | Selector | Operand |
|---|---|---|---|---|
| `OTHER` | `(cond >> 8) & 0xFC == 0x00` | `....‥‥‥‥ ‥‥‥‥mmmm` | bits 0-3 = `MiscConditional` | (no operand) |
| `ZONE` | `... == 0x04` | `000001oo zzzzzzzz` | bits 0-7 = zone # | bit 9 = `0`=SECURE, `1`=NOT_READY |
| `CTRL` | `... == 0x08` | `000010oo uuuuuuuu (+ bit 8 = high u)` | bits 0-8 = unit # | bit 9 = `0`=OFF, `1`=ON |
| `TIME` | `... == 0x0C` | `000011oo cccccccc` | bits 0-7 = clock # | bit 9 = `0`=DISABLED, `1`=ENABLED |
| `SEC` | anything else (incl. `0x10`+) | `xmmm aaaa ........` | bits 8-11 = area # (0 = any) | bits 12-14 = `SecurityMode`, bit 15 = arming-transition flag |
Worked examples:
```
cond = 0x0605 → ZONE, zone 5, NOT_READY ("Zone 5 NOT_READY")
cond = 0x0A0F → CTRL, unit 15, ON ("Unit 15 ON")
cond = 0x0E03 → TIME, clock 3, ENABLED ("Time clock 3 ENABLED")
cond = 0x000B → OTHER, BATTERY_OK
cond = 0x8100 → SEC, area 1, mode=OFF ("Area 1 OFF")
cond = 0xB100 → SEC, area 1, ARMING AWAY (bit 15 + mode=3)
```
Python usage:
```python
from omni_pca.programs import Condition, ConditionFamily, Program, ProgramType
p = Program(
prog_type=int(ProgramType.TIMED),
cond=0x0605, # "Zone 5 NOT_READY"
cond2=0xB100, # "Area 1 ARMING AWAY"
)
c1 = p.condition()
assert c1.family is ConditionFamily.ZONE
assert c1.selector == 5 and c1.operand == 1
assert c1.describe() == "Zone 5 NOT_READY"
```
`Condition.describe()` does the rendering with index-based labels
(`"Zone 5"`, `"Unit 12"`) — name lookups need the panel name tables and
are left to the caller. A future helper that takes a name map would be
the obvious next step for a UI editor.
A few details worth knowing:
- **`OTHER` ignores high bits.** `cond=0x010B` and `cond=0x000B` both decode
to `BATTERY_OK` — PC Access's encoder sometimes leaves high bits set on
Other-family conditions; the decoder masks them off.
- **`SEC` with mode=Off + bit 15** is the plain "Area X is OFF" encoding,
NOT an arming transition. The arming-transition rendering kicks in only
when bit 15 *and* the mode bits 12-14 are non-zero (per `clsText.cs:2263`).
- **Area `0` in `SEC`** is the "no specific area" / "any area" form —
PC Access renders the area name as blank in this case.
Source: `clsText.GetConditionalText` at clsText.cs:2224-2273 (decode) and
`frmAutomationEditCondition.cs:615-2550` (encode/UI). The `MiscConditional`
enum mirrors `enuMiscConditional.cs`.
## TIMED programs: absolute time vs sunrise/sunset offset
The `hour` byte at offset 12 is **overloaded** as a one-of-three
@ -258,13 +319,6 @@ 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