programs: add structured-OP AND decoder properties
Final RE pass on the multi-record AND record extension. Authored
"AND IF DATE IS EQUAL TO 12/31" (block 12, slot 13) and resolved
the disk encoding model for the structured-OP case:
byte 0 : ProgType = 8 (AND)
byte 1 : (high byte of LE cond) = OP (enuCondOP)
byte 2 : (low byte of LE cond) = Arg1_ArgType (enuCondArgType)
bytes 3-4 : (cond2 LE) = Arg1_IX
byte 5 : (cmd byte) = Arg1_Field
byte 6 : (par byte) = Arg2_ArgType
bytes 7-8 : (pr2 LE) = Arg2_IX
byte 9 : (month byte) = Arg2_Field
bytes 10-11: (day, days bytes) = CompConst
The C# clsConditionLine.Cond property at clsConditionLine.cs:17-33
bridges the two views: for Traditional case (OP=0), the compact-form
cond u16 is SYNTHESIZED from Arg1_ArgType and Arg1_IX. The byte at
offset 2 (= Arg1_ArgType) holds the ProgramCond family code (ZONE=4,
CTRL=8, ...) when OP=0, or the enuCondArgType value (Zone=2, Unit=3,
Thermostat=4, TimeDate=7, ...) when OP > 0. Same byte, different
semantic interpretation based on OP.
New Program properties:
and_op - byte 1, enuCondOP (0 = Traditional, 1-9 = structured)
and_arg1_argtype - byte 2, family code (Trad) or CondArgType (Struct)
and_arg1_ix - bytes 3-4 raw u16 (= cond2; Python LE decode
happens to equal C# in-memory BE Arg1_IX)
and_arg1_field - byte 5
and_arg2_argtype - byte 6
and_arg2_ix - bytes 7-8 raw u16 (= pr2)
and_arg2_field - byte 9
and_compconst - bytes 10-11
The and_instance property is now smart-branched on and_op:
- Traditional: returns Arg1_IX >> 8 (instance in high byte per
clsConditionLine.Cond setter)
- Structured: returns Arg1_IX directly (raw object index)
Also fixed every_interval: per clsProgram.Interval at
clsProgram.cs:338-348, it reads (Data[2] << 8) | Data[3] which spans
the Cond and Cond2 byte ranges. The correct Python formula is
((cond & 0xFF) << 8) | ((cond2 >> 8) & 0xFF). The earlier byte-swap-of-
cond2 formula happened to work for Interval=5 but would break for
Interval > 255.
2 new tests:
test_and_structured_date_eq_1231 - the captured Date case
test_and_traditional_zone_5_secure_via_structured_view
- same vector via structured accessors
475 tests passing (up from 473).
This commit is contained in:
parent
e560d98f87
commit
290ba5a78d
@ -816,36 +816,159 @@ class Program:
|
||||
def and_instance(self) -> int:
|
||||
"""For AND records (ProgType=8): the object/instance number.
|
||||
|
||||
Stored as a BE u16 at bytes 3-4 of the AND record. Returns:
|
||||
zone # for ZONE family, unit # for CTRL family,
|
||||
``MiscConditional`` value for OTHER family, etc.
|
||||
Returns the semantic instance for both Traditional and
|
||||
Structured AND records. The encoding differs internally:
|
||||
|
||||
Empirical evidence: ``AND IF ZONE 5 SECURE`` → 5,
|
||||
``AND IF UNIT 1 ON`` → 1, ``AND IF NEVER`` → 1
|
||||
(MiscConditional.NEVER).
|
||||
* **Traditional (OP=0):** ``clsConditionLine.Cond`` setter
|
||||
stores ``Arg1.IX = (instance & 0xFF) << 8`` so the instance
|
||||
ends up in the HIGH byte of the u16. Empirically:
|
||||
``AND IF ZONE 5 SECURE`` → cond2 = ``0x0500``, instance = 5.
|
||||
* **Structured (OP > 0):** ``Arg1_IX`` is the raw u16, so
|
||||
the instance is the whole value. ``AND IF ZONE 5 IS
|
||||
BYPASSED`` (hypothetical) → cond2 = 5.
|
||||
|
||||
Per :attr:`and_arg1_ix` for the raw value.
|
||||
Only meaningful when ``prog_type == AND``.
|
||||
"""
|
||||
# Disk bytes 3-4 = BE u16, but ``cond2`` was LE-decoded.
|
||||
# The BE-interpreted value is the byte-swap of ``cond2``.
|
||||
return ((self.cond2 & 0xFF) << 8) | ((self.cond2 >> 8) & 0xFF)
|
||||
if self.and_op == 0:
|
||||
# Traditional: instance is in the high byte of Arg1_IX
|
||||
return (self.cond2 >> 8) & 0xFF
|
||||
# Structured: Arg1_IX is the instance directly
|
||||
return self.cond2
|
||||
|
||||
@property
|
||||
def and_arg1_ix(self) -> int:
|
||||
"""For AND records: the raw ``Arg1_IX`` u16 value (bytes 3-4).
|
||||
|
||||
Equals ``self.cond2`` since the Python LE decode of disk bytes
|
||||
3-4 produces the same value as the C# in-memory BE ``Arg1_IX``
|
||||
(the Read function's LE-to-BE byte swap cancels out at this
|
||||
level). Returns:
|
||||
|
||||
* **Traditional (OP=0):** ``(instance & 0xFF) << 8`` — the
|
||||
instance number shifted up by 8 bits (see
|
||||
:attr:`and_instance` for the unshifted value).
|
||||
* **Structured (OP > 0):** the raw instance number / index.
|
||||
"""
|
||||
return self.cond2
|
||||
|
||||
@property
|
||||
def every_interval(self) -> int:
|
||||
"""For EVERY records (ProgType=7): the recurrence interval.
|
||||
|
||||
Stored as a BE u16 at bytes 3-4. PC Access exposes preset
|
||||
values like "5 SECONDS", "10 SECONDS", "1 MINUTE", etc.; the
|
||||
unit of the integer (seconds vs minutes vs hours) is decided
|
||||
by the controller firmware — needs more captures with varied
|
||||
UI selections to disambiguate. The "5 SECONDS" UI default
|
||||
encodes as ``every_interval == 5``.
|
||||
Per the C# ``clsProgram.Interval`` accessor at
|
||||
``clsProgram.cs:338-348``, Interval = ``(Data[2] << 8) | Data[3]``
|
||||
— i.e. it spans the Cond and Cond2 byte ranges. After Read's
|
||||
LE-to-BE swap, ``Data[2] = cond & 0xFF`` and
|
||||
``Data[3] = (cond2 >> 8) & 0xFF``, so:
|
||||
|
||||
Interval = ((cond & 0xFF) << 8) | ((cond2 >> 8) & 0xFF)
|
||||
|
||||
PC Access exposes preset values like "5 SECONDS", "10 SECONDS",
|
||||
"1 MINUTE", etc.; the unit (seconds vs minutes vs hours) is
|
||||
decided by the controller firmware — needs more captures with
|
||||
varied UI selections to disambiguate. The "5 SECONDS" UI
|
||||
default encodes as ``every_interval == 5``.
|
||||
|
||||
Only meaningful when ``prog_type == EVERY``.
|
||||
"""
|
||||
# Same byte-swap rationale as ``and_instance`` — bytes 3-4
|
||||
# are BE on disk but ``cond2`` is LE-decoded.
|
||||
return ((self.cond2 & 0xFF) << 8) | ((self.cond2 >> 8) & 0xFF)
|
||||
return ((self.cond & 0xFF) << 8) | ((self.cond2 >> 8) & 0xFF)
|
||||
|
||||
# ---- structured AND records (firmware ≥3.0, OP > 0) ----
|
||||
|
||||
@property
|
||||
def and_op(self) -> int:
|
||||
"""For AND records (ProgType=8): the operator byte (`enuCondOP`).
|
||||
|
||||
Returns 0 (``Arg1_Traditional``) for the common case where the
|
||||
condition uses the compact-form ``cond``-style encoding (see
|
||||
:attr:`and_family` and :attr:`and_instance`). For structured
|
||||
comparisons (e.g. ``DATE IS EQUAL TO 12/31``,
|
||||
``TEMPERATURE > 70``) returns ``1`` through ``9`` per
|
||||
:class:`CondOP`.
|
||||
|
||||
Per ``clsProgram.cs:326``, ``OP`` lives at ``Data[1]`` (= high
|
||||
byte of the LE-decoded ``cond`` field, because the Cond setter
|
||||
writes ``Data[1] = value >> 8``).
|
||||
"""
|
||||
return (self.cond >> 8) & 0xFF
|
||||
|
||||
@property
|
||||
def and_arg1_argtype(self) -> int:
|
||||
"""For AND records: the ``Arg1_ArgType`` byte.
|
||||
|
||||
Holds different semantics depending on :attr:`and_op`:
|
||||
|
||||
* **OP == 0 (Traditional):** the compact-form ``ProgramCond``
|
||||
family code (ZONE=4, CTRL=8, TIME=12, SEC=16), *not* the
|
||||
:class:`CondArgType` Zone=2/Unit=3 etc. The byte is reused
|
||||
as a raw byte holder. This is the same value as
|
||||
:attr:`and_family` for the simple-AND case.
|
||||
|
||||
* **OP > 0 (Structured):** an actual :class:`CondArgType`
|
||||
value (Zone=2, Unit=3, Thermostat=4, TimeDate=7, …).
|
||||
|
||||
Per ``clsProgram.cs:351``, ``Arg1_ArgType`` lives at
|
||||
``Data[2]`` (= low byte of the LE-decoded ``cond`` field).
|
||||
"""
|
||||
return self.cond & 0xFF
|
||||
|
||||
@property
|
||||
def and_arg2_argtype(self) -> int:
|
||||
"""For AND records: the ``Arg2_ArgType`` byte (byte 6 on disk).
|
||||
|
||||
Holds a :class:`CondArgType` value when ``OP > 0``. ``0`` =
|
||||
``Constant`` is the common case for comparisons against a
|
||||
literal value (then :attr:`and_arg2_ix` is the constant).
|
||||
|
||||
Reuses the ``par`` byte slot (which serves the cmd-parameter
|
||||
role for compact-form records).
|
||||
"""
|
||||
return self.par
|
||||
|
||||
@property
|
||||
def and_arg2_ix(self) -> int:
|
||||
"""For AND records: ``Arg2_IX`` u16 (bytes 7-8 on disk).
|
||||
|
||||
Equals ``self.pr2`` — Python LE decode of disk bytes 7-8
|
||||
gives the same value as the C# in-memory BE ``Arg2_IX``
|
||||
accessor, because the encoder writes via the same
|
||||
``WriteUInt16(Pr2)`` path the compact form uses.
|
||||
|
||||
For structured comparisons, holds either an object index (if
|
||||
``and_arg2_argtype != Constant``) or the constant operand
|
||||
value packed for the specific Arg1 type. Example: for
|
||||
``DATE IS EQUAL TO 12/31`` the Arg2_IX is ``(12 << 8) | 31 =
|
||||
0x0c1f = 3103`` — month in the high byte, day in the low byte.
|
||||
"""
|
||||
return self.pr2
|
||||
|
||||
@property
|
||||
def and_arg1_field(self) -> int:
|
||||
"""For AND records: ``Arg1_Field`` byte (byte 5 on disk).
|
||||
|
||||
Per-type sub-field index (e.g. ``enuZoneField.CurrentState``
|
||||
for Zone arguments). Reuses the ``cmd`` byte slot.
|
||||
"""
|
||||
return self.cmd
|
||||
|
||||
@property
|
||||
def and_arg2_field(self) -> int:
|
||||
"""For AND records: ``Arg2_Field`` byte (byte 9 on disk).
|
||||
|
||||
Reuses the ``month`` byte slot.
|
||||
"""
|
||||
return self.month
|
||||
|
||||
@property
|
||||
def and_compconst(self) -> int:
|
||||
"""For AND records: ``CompConst`` BE u16 (bytes 10-11 on disk).
|
||||
|
||||
Constant operand for comparison ops that need a literal value
|
||||
beyond ``Arg2_IX``. Zero for the captures we have so far.
|
||||
Reuses the (``day``, ``days``) byte pair.
|
||||
"""
|
||||
return ((self.day & 0xFF) << 8) | (self.days & 0xFF)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""True iff the encoded record would be all-zero.
|
||||
|
||||
@ -568,3 +568,51 @@ def test_then_record_uses_compact_action_layout() -> None:
|
||||
assert p.cmd == 1 # enuUnitCommand.On
|
||||
assert p.par == 0
|
||||
assert p.pr2 == 1 # UNIT 1 (LE u16 at bytes 7-8, same as compact)
|
||||
|
||||
|
||||
# ---- structured AND records (firmware ≥3.0, OP > 0) ------------------
|
||||
|
||||
|
||||
def test_and_structured_date_eq_1231() -> None:
|
||||
"""Structured AND IF DATE IS EQUAL TO 12/31 (block 12 slot 13).
|
||||
|
||||
Captured bytes: 08 07 01 00 00 01 00 1f 0c 00 00 00 00 00
|
||||
|
||||
Decodes per clsProgram.cs:326-436 accessors after Read's LE-to-BE
|
||||
byte swap. The OP is non-zero (Arg1_EQ_Arg2), so this is the
|
||||
"structured" case where Arg1_ArgType holds an actual enuCondArgType
|
||||
value (TimeDate=7) rather than a compact-form family code.
|
||||
"""
|
||||
body = bytes.fromhex("08 07 01 00 00 01 00 1f 0c 00 00 00 00 00".replace(" ", ""))
|
||||
p = Program.from_file_record(body, slot=13)
|
||||
assert p.prog_type == ProgramType.AND
|
||||
assert p.and_op == 1 # enuCondOP.Arg1_EQ_Arg2
|
||||
assert p.and_arg1_argtype == 7 # enuCondArgType.TimeDate
|
||||
assert p.and_instance == 0 # Arg1_IX = 0 (CURRENT_DATE)
|
||||
assert p.and_arg1_field == 1 # Date sub-field
|
||||
assert p.and_arg2_argtype == 0 # enuCondArgType.Constant
|
||||
# Arg2_IX = (month << 8) | day = (12 << 8) | 31 = 0x0c1f
|
||||
assert p.and_arg2_ix == 0x0C1F
|
||||
assert p.and_arg2_ix >> 8 == 12 # month
|
||||
assert p.and_arg2_ix & 0xFF == 31 # day
|
||||
assert p.and_arg2_field == 0
|
||||
assert p.and_compconst == 0
|
||||
|
||||
|
||||
def test_and_traditional_zone_5_secure_via_structured_view() -> None:
|
||||
"""Traditional AND (OP=0) read via the structured-AND accessors.
|
||||
|
||||
For the Traditional case, Arg1_ArgType holds the compact-form
|
||||
family code (ZONE=4) — NOT the enuCondArgType Zone=2. This is the
|
||||
"dual-use byte" behavior documented at clsConditionLine.cs:17-33.
|
||||
"""
|
||||
# AND IF ZONE 5 SECURE — same byte vector as earlier and_zone_5_secure test
|
||||
body = bytes.fromhex("08 04 00 00 05 00 00 00 00 00 00 00 00 00".replace(" ", ""))
|
||||
p = Program.from_file_record(body, slot=7)
|
||||
assert p.and_op == 0 # Arg1_Traditional
|
||||
# Arg1_ArgType holds the ProgramCond family code (ZONE=4), not enuCondArgType.Zone=2
|
||||
assert p.and_arg1_argtype == 4
|
||||
# and_family is the same byte for this case
|
||||
assert p.and_family == p.and_arg1_argtype
|
||||
# The instance number is still in bytes 3-4 BE
|
||||
assert p.and_instance == 5
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user