From db3832c68c786032a51965072adf0e09a010d852 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 11 May 2026 22:35:05 -0600 Subject: [PATCH] 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. --- src/content/docs/reference/program-format.mdx | 82 +++++++++++++++---- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/src/content/docs/reference/program-format.mdx b/src/content/docs/reference/program-format.mdx index d5c13bd..3ebe7a5 100644 --- a/src/content/docs/reference/program-format.mdx +++ b/src/content/docs/reference/program-format.mdx @@ -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