Models: 16 status/properties dataclasses + enums + temp converters
src/omni_pca/models.py — adds typed parsers for the full Omni object surface beyond the initial Zone/Unit/Area properties: - ZoneStatus, UnitStatus, AreaStatus (live state) - ThermostatProperties, ThermostatStatus (with omni_temp_to_celsius/_fahrenheit via clsText.DecodeTempRaw — the scale is LINEAR, not non-linear as I'd hypothesized: C = raw/2 - 40) - ButtonProperties, ProgramProperties, CodeProperties, MessageProperties - AuxSensorStatus - AudioZoneProperties + AudioZoneStatus - AudioSourceProperties + AudioSourceStatus - UserSettingProperties + UserSettingStatus Module helpers: - IntEnums: ObjectType, SecurityMode, HvacMode, FanMode, HoldMode, ThermostatKind, ZoneType, UserSettingKind - OBJECT_TYPE_TO_PROPERTIES / OBJECT_TYPE_TO_STATUS dispatch tables - omni_temp_to_celsius/_fahrenheit linear conversion (citation: clsText.cs) 42 new tests in tests/test_models_extended.py; 139 total pass.
This commit is contained in:
parent
2e439364bd
commit
08974e2ec4
File diff suppressed because it is too large
Load Diff
519
tests/test_models_extended.py
Normal file
519
tests/test_models_extended.py
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
"""Unit tests for the extended omni_pca.models classes & helpers.
|
||||||
|
|
||||||
|
These cover the second wave of typed dataclasses (ZoneStatus, UnitStatus,
|
||||||
|
ThermostatStatus, etc.) plus the temperature converters and value enums.
|
||||||
|
The first-wave dataclasses live in test_models.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from omni_pca.models import (
|
||||||
|
OBJECT_TYPE_TO_PROPERTIES,
|
||||||
|
OBJECT_TYPE_TO_STATUS,
|
||||||
|
AreaStatus,
|
||||||
|
AudioSourceProperties,
|
||||||
|
AudioSourceStatus,
|
||||||
|
AudioZoneProperties,
|
||||||
|
AudioZoneStatus,
|
||||||
|
AuxSensorStatus,
|
||||||
|
ButtonProperties,
|
||||||
|
CodeProperties,
|
||||||
|
FanMode,
|
||||||
|
HoldMode,
|
||||||
|
HvacMode,
|
||||||
|
MessageProperties,
|
||||||
|
ObjectType,
|
||||||
|
ProgramProperties,
|
||||||
|
SecurityMode,
|
||||||
|
ThermostatKind,
|
||||||
|
ThermostatProperties,
|
||||||
|
ThermostatStatus,
|
||||||
|
UnitStatus,
|
||||||
|
UserSettingKind,
|
||||||
|
UserSettingProperties,
|
||||||
|
UserSettingStatus,
|
||||||
|
ZoneStatus,
|
||||||
|
ZoneType,
|
||||||
|
omni_temp_to_celsius,
|
||||||
|
omni_temp_to_fahrenheit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _name_field(name: str, width: int) -> bytes:
|
||||||
|
encoded = name.encode("latin-1")
|
||||||
|
if len(encoded) > width:
|
||||||
|
raise ValueError("name too long for field")
|
||||||
|
return encoded + b"\x00" * (width - len(encoded))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Enums ----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_security_mode_enum_values() -> None:
|
||||||
|
"""Pinned values from enuSecurityMode.cs."""
|
||||||
|
assert SecurityMode.OFF == 0
|
||||||
|
assert SecurityMode.DAY == 1
|
||||||
|
assert SecurityMode.NIGHT == 2
|
||||||
|
assert SecurityMode.AWAY == 3
|
||||||
|
assert SecurityMode.VACATION == 4
|
||||||
|
assert SecurityMode.DAY_INSTANT == 5
|
||||||
|
assert SecurityMode.NIGHT_DELAYED == 6
|
||||||
|
# arming-in-progress family
|
||||||
|
assert SecurityMode.ARMING_AWAY == 11
|
||||||
|
assert SecurityMode.ARMING_NIGHT_DELAYED == 14
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_hvac_mode_enum_values() -> None:
|
||||||
|
assert HvacMode.OFF == 0
|
||||||
|
assert HvacMode.HEAT == 1
|
||||||
|
assert HvacMode.COOL == 2
|
||||||
|
assert HvacMode.AUTO == 3
|
||||||
|
assert HvacMode.EMERGENCY_HEAT == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_fan_and_hold_mode_enums() -> None:
|
||||||
|
assert FanMode.AUTO == 0
|
||||||
|
assert FanMode.ON == 1
|
||||||
|
assert FanMode.CYCLE == 2
|
||||||
|
assert HoldMode.OFF == 0
|
||||||
|
assert HoldMode.HOLD == 1
|
||||||
|
assert HoldMode.VACATION == 2
|
||||||
|
assert HoldMode.OLD_ON == 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_zone_type_enum_subset() -> None:
|
||||||
|
assert ZoneType.ENTRY_EXIT == 0
|
||||||
|
assert ZoneType.PERIMETER == 1
|
||||||
|
assert ZoneType.FIRE == 32
|
||||||
|
assert ZoneType.AUXILIARY == 64
|
||||||
|
assert ZoneType.HUMIDITY == 84
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_object_type_enum_values() -> None:
|
||||||
|
assert ObjectType.ZONE == 1
|
||||||
|
assert ObjectType.UNIT == 2
|
||||||
|
assert ObjectType.AREA == 5
|
||||||
|
assert ObjectType.THERMOSTAT == 6
|
||||||
|
assert ObjectType.AUDIO_ZONE == 10
|
||||||
|
assert ObjectType.USER_SETTING == 13
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_thermostat_kind_and_user_setting_enums() -> None:
|
||||||
|
assert ThermostatKind.NOT_USED == 0
|
||||||
|
assert ThermostatKind.AUTO_HEAT_COOL == 1
|
||||||
|
assert ThermostatKind.HEAT_ONLY == 3
|
||||||
|
assert UserSettingKind.UNUSED == 0
|
||||||
|
assert UserSettingKind.TEMPERATURE == 3
|
||||||
|
assert UserSettingKind.LEVEL == 8
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Temperature converters -----------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_omni_temp_to_celsius_kat() -> None:
|
||||||
|
"""Pinned values from clsText.DecodeTempRaw, Celsius branch."""
|
||||||
|
assert omni_temp_to_celsius(0) == -40.0
|
||||||
|
assert omni_temp_to_celsius(80) == 0.0
|
||||||
|
assert omni_temp_to_celsius(128) == 24.0
|
||||||
|
assert omni_temp_to_celsius(140) == 30.0
|
||||||
|
assert omni_temp_to_celsius(200) == 60.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_omni_temp_to_fahrenheit_kat() -> None:
|
||||||
|
"""Pinned values from clsText.DecodeTempRaw, Fahrenheit branch.
|
||||||
|
|
||||||
|
The C# rounds with int(raw*9/10 + 0.5) - 40, so we replicate that.
|
||||||
|
"""
|
||||||
|
# raw=44 → int(44*0.9+0.5)-40 = int(40.1)-40 = 40-40 = 0°F
|
||||||
|
assert omni_temp_to_fahrenheit(44) == 0.0
|
||||||
|
# raw=128 → int(128*0.9+0.5)-40 = int(115.7)-40 = 115-40 = 75°F
|
||||||
|
assert omni_temp_to_fahrenheit(128) == 75.0
|
||||||
|
# raw=200 → int(200*0.9+0.5)-40 = int(180.5)-40 = 180-40 = 140°F
|
||||||
|
assert omni_temp_to_fahrenheit(200) == 140.0
|
||||||
|
# raw=0 (sentinel "not available") still goes through linearly
|
||||||
|
assert omni_temp_to_fahrenheit(0) == -40.0
|
||||||
|
|
||||||
|
|
||||||
|
# ---- ZoneStatus -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_zone_status_secure() -> None:
|
||||||
|
payload = bytes([0, 5, 0x00, 128]) # zone 5, all bits clear, loop=128
|
||||||
|
z = ZoneStatus.parse(payload)
|
||||||
|
assert z.index == 5
|
||||||
|
assert z.raw_status == 0x00
|
||||||
|
assert z.loop == 128
|
||||||
|
assert z.current_state == 0
|
||||||
|
assert z.latched_state == 0
|
||||||
|
assert z.arming_state == 0
|
||||||
|
assert z.is_secure
|
||||||
|
assert not z.is_open
|
||||||
|
assert not z.is_in_alarm
|
||||||
|
assert not z.is_bypassed
|
||||||
|
assert not z.is_trouble
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_zone_status_tripped_armed() -> None:
|
||||||
|
# current=NotReady(1) + tripped(4) + armed(16) = 0x15
|
||||||
|
payload = bytes([0, 1, 0x15, 0])
|
||||||
|
z = ZoneStatus.parse(payload)
|
||||||
|
assert z.current_state == 1
|
||||||
|
assert z.latched_state == 4
|
||||||
|
assert z.arming_state == 16
|
||||||
|
assert not z.is_secure
|
||||||
|
assert z.is_in_alarm
|
||||||
|
assert not z.is_bypassed
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_zone_status_bypassed_and_trouble() -> None:
|
||||||
|
# bypassed(32) + trouble-now(2) = 0x22
|
||||||
|
payload = bytes([0, 2, 0x22, 0])
|
||||||
|
z = ZoneStatus.parse(payload)
|
||||||
|
assert z.is_bypassed
|
||||||
|
assert z.is_trouble
|
||||||
|
assert not z.is_secure
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_zone_status_had_trouble_history_bit() -> None:
|
||||||
|
# only the "had trouble" history bit (0x40) set
|
||||||
|
payload = bytes([0, 7, 0x40, 0])
|
||||||
|
z = ZoneStatus.parse(payload)
|
||||||
|
assert z.is_trouble
|
||||||
|
assert z.is_secure # current condition still 0
|
||||||
|
assert not z.is_in_alarm
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_zone_status_short_payload_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ZoneStatus.parse(b"\x00\x01\x00")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- UnitStatus -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_unit_status_off() -> None:
|
||||||
|
payload = bytes([0, 3, 0, 0, 0])
|
||||||
|
u = UnitStatus.parse(payload)
|
||||||
|
assert u.index == 3
|
||||||
|
assert u.state == 0
|
||||||
|
assert u.time_remaining_secs == 0
|
||||||
|
assert not u.is_on
|
||||||
|
assert u.brightness == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_unit_status_relay_on() -> None:
|
||||||
|
payload = bytes([0, 4, 1, 0x00, 60]) # On, 60s remaining
|
||||||
|
u = UnitStatus.parse(payload)
|
||||||
|
assert u.is_on
|
||||||
|
assert u.brightness == 100 # On with no level → treat as 100%
|
||||||
|
assert u.time_remaining_secs == 60
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_unit_status_dimmer_level() -> None:
|
||||||
|
# state 175 = 75% brightness
|
||||||
|
payload = bytes([0, 12, 175, 0, 0])
|
||||||
|
u = UnitStatus.parse(payload)
|
||||||
|
assert u.is_on
|
||||||
|
assert u.brightness == 75
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_unit_status_scene_no_brightness() -> None:
|
||||||
|
# state 5 = Scene C (clsUnit.cs:517-525). brightness undefined.
|
||||||
|
payload = bytes([0, 9, 5, 0, 0])
|
||||||
|
u = UnitStatus.parse(payload)
|
||||||
|
assert u.is_on
|
||||||
|
assert u.brightness is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_unit_status_short_payload_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
UnitStatus.parse(b"\x00\x01\x01")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- AreaStatus -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_area_status_armed_away() -> None:
|
||||||
|
# area 1, mode=Away(3), no alarms, no timers
|
||||||
|
payload = bytes([0, 1, 3, 0x00, 0, 0])
|
||||||
|
a = AreaStatus.parse(payload)
|
||||||
|
assert a.index == 1
|
||||||
|
assert a.mode == 3
|
||||||
|
assert a.mode_name == "AWAY"
|
||||||
|
assert a.is_armed
|
||||||
|
assert not a.alarm_active
|
||||||
|
assert a.last_user == 0 # documented: not in wire format
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_area_status_off_with_entry_timer() -> None:
|
||||||
|
payload = bytes([0, 2, 0, 0x00, 30, 0])
|
||||||
|
a = AreaStatus.parse(payload)
|
||||||
|
assert a.mode == 0
|
||||||
|
assert a.mode_name == "OFF"
|
||||||
|
assert not a.is_armed
|
||||||
|
assert a.entry_timer_secs == 30
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_area_status_active_alarm() -> None:
|
||||||
|
# mode=Day(1), alarms bitfield non-zero
|
||||||
|
payload = bytes([0, 1, 1, 0x04, 0, 0])
|
||||||
|
a = AreaStatus.parse(payload)
|
||||||
|
assert a.mode == 1
|
||||||
|
assert a.alarm_active
|
||||||
|
assert a.is_armed
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_area_status_unknown_mode() -> None:
|
||||||
|
payload = bytes([0, 1, 99, 0, 0, 0])
|
||||||
|
a = AreaStatus.parse(payload)
|
||||||
|
assert a.mode_name.startswith("Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- ThermostatProperties -------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_thermostat_properties_parse() -> None:
|
||||||
|
payload = (
|
||||||
|
bytes([6]) # Thermostat
|
||||||
|
+ bytes([0, 1]) # index 1
|
||||||
|
+ bytes([1]) # communicating
|
||||||
|
+ bytes([0]) # temperature
|
||||||
|
+ bytes([0]) # heat sp
|
||||||
|
+ bytes([0]) # cool sp
|
||||||
|
+ bytes([0]) # mode
|
||||||
|
+ bytes([0]) # fan
|
||||||
|
+ bytes([0]) # hold
|
||||||
|
+ bytes([1]) # type=AutoHeatCool
|
||||||
|
+ _name_field("Den", 12)
|
||||||
|
)
|
||||||
|
t = ThermostatProperties.parse(payload)
|
||||||
|
assert t.index == 1
|
||||||
|
assert t.name == "Den"
|
||||||
|
assert t.thermostat_type == 1
|
||||||
|
assert t.communicating is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_thermostat_properties_wrong_type_rejected() -> None:
|
||||||
|
payload = bytes([1] + [0] * 22)
|
||||||
|
with pytest.raises(ValueError, match="expected Thermostat"):
|
||||||
|
ThermostatProperties.parse(payload)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- ThermostatStatus -----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_thermostat_status_parse() -> None:
|
||||||
|
# idx=2, status=1, temp=128 (24°C/75°F), heat=120 (20°C),
|
||||||
|
# cool=140 (30°C), mode=Heat, fan=Auto, hold=Off,
|
||||||
|
# humidity=130 (33% via F decode),
|
||||||
|
# humidify=120, dehumidify=140, outdoor=110, h_or_c=1
|
||||||
|
payload = bytes([0, 2, 1, 128, 120, 140, 1, 0, 0, 130, 120, 140, 110, 1])
|
||||||
|
t = ThermostatStatus.parse(payload)
|
||||||
|
assert t.index == 2
|
||||||
|
assert t.temperature_raw == 128
|
||||||
|
assert t.temperature_c == 24.0
|
||||||
|
assert t.temperature_f == 75.0
|
||||||
|
assert t.heat_setpoint_raw == 120
|
||||||
|
assert t.heat_setpoint_c == 20.0
|
||||||
|
assert t.cool_setpoint_raw == 140
|
||||||
|
assert t.cool_setpoint_c == 30.0
|
||||||
|
assert t.system_mode == 1
|
||||||
|
assert t.fan_mode == 0
|
||||||
|
assert t.hold_mode == 0
|
||||||
|
assert t.humidity_raw == 130
|
||||||
|
assert t.outdoor_temperature_raw == 110
|
||||||
|
assert t.horc_status == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_thermostat_status_short_payload_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ThermostatStatus.parse(b"\x00\x02\x01")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- ButtonProperties -----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_button_properties_parse() -> None:
|
||||||
|
payload = bytes([3, 0, 4]) + _name_field("Welcome", 12)
|
||||||
|
b = ButtonProperties.parse(payload)
|
||||||
|
assert b.index == 4
|
||||||
|
assert b.name == "Welcome"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_button_properties_wrong_type_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError, match="expected Button"):
|
||||||
|
ButtonProperties.parse(bytes([1, 0, 1]) + _name_field("X", 12))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- CodeProperties -------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_code_properties_parse_no_digits_exposed() -> None:
|
||||||
|
payload = bytes([4, 0, 2]) + _name_field("Alice", 12)
|
||||||
|
c = CodeProperties.parse(payload)
|
||||||
|
assert c.index == 2
|
||||||
|
assert c.name == "Alice"
|
||||||
|
# Belt-and-suspenders: the dataclass really has only these two fields
|
||||||
|
# of interest. There is no "digits" / "code" attribute.
|
||||||
|
assert not hasattr(c, "digits")
|
||||||
|
assert not hasattr(c, "code")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- MessageProperties ----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_message_properties_parse() -> None:
|
||||||
|
payload = (
|
||||||
|
bytes([7, 0, 1])
|
||||||
|
+ _name_field("Hello world!!", 15)
|
||||||
|
+ bytes([0]) # gap (Data[19] in C# offset)
|
||||||
|
+ bytes([0xFF]) # area-group bitfield
|
||||||
|
)
|
||||||
|
m = MessageProperties.parse(payload)
|
||||||
|
assert m.index == 1
|
||||||
|
assert m.name == "Hello world!!"
|
||||||
|
assert m.text == m.name # OL2 v1 quirk: name doubles as text
|
||||||
|
assert m.areas == 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
# ---- ProgramProperties ----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_program_properties_parse_with_remark() -> None:
|
||||||
|
body = bytes(range(14)) # 14 bytes
|
||||||
|
payload = bytes([0, 7]) + body + b"sunset blink\x00"
|
||||||
|
p = ProgramProperties.parse(payload)
|
||||||
|
assert p.index == 7
|
||||||
|
assert p.raw_body == body
|
||||||
|
assert p.name == "sunset blink"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_program_properties_parse_no_remark() -> None:
|
||||||
|
body = b"\x00" * 14
|
||||||
|
payload = bytes([0, 1]) + body
|
||||||
|
p = ProgramProperties.parse(payload)
|
||||||
|
assert p.index == 1
|
||||||
|
assert p.name == ""
|
||||||
|
assert len(p.raw_body) == 14
|
||||||
|
|
||||||
|
|
||||||
|
# ---- AuxSensorStatus ------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_aux_sensor_status_parse() -> None:
|
||||||
|
# idx=4, output=0, value=140 (30°C), low=120 (20°C), high=160 (40°C)
|
||||||
|
payload = bytes([0, 4, 0, 140, 120, 160])
|
||||||
|
a = AuxSensorStatus.parse(payload)
|
||||||
|
assert a.index == 4
|
||||||
|
assert a.value_raw == 140
|
||||||
|
assert a.temperature_c == 30.0
|
||||||
|
assert a.low_c == 20.0
|
||||||
|
assert a.high_c == 40.0
|
||||||
|
|
||||||
|
|
||||||
|
# ---- AudioZone ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_audio_zone_properties_parse() -> None:
|
||||||
|
payload = (
|
||||||
|
bytes([10, 0, 1]) # AudioZone, idx 1
|
||||||
|
+ bytes([1]) # power on
|
||||||
|
+ bytes([2]) # source 2
|
||||||
|
+ bytes([60]) # volume 60
|
||||||
|
+ bytes([0]) # mute off
|
||||||
|
+ _name_field("Living", 12)
|
||||||
|
)
|
||||||
|
az = AudioZoneProperties.parse(payload)
|
||||||
|
assert az.index == 1
|
||||||
|
assert az.name == "Living"
|
||||||
|
assert az.power is True
|
||||||
|
assert az.source == 2
|
||||||
|
assert az.volume == 60
|
||||||
|
assert az.mute is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_audio_zone_status_parse() -> None:
|
||||||
|
# idx=3, power on, source 5, vol 80, mute on
|
||||||
|
payload = bytes([0, 3, 1, 5, 80, 1])
|
||||||
|
s = AudioZoneStatus.parse(payload)
|
||||||
|
assert s.index == 3
|
||||||
|
assert s.power is True
|
||||||
|
assert s.source == 5
|
||||||
|
assert s.volume == 80
|
||||||
|
assert s.mute is True
|
||||||
|
|
||||||
|
|
||||||
|
# ---- AudioSource ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_audio_source_properties_parse() -> None:
|
||||||
|
payload = bytes([9, 0, 2]) + _name_field("Spotify", 12)
|
||||||
|
s = AudioSourceProperties.parse(payload)
|
||||||
|
assert s.index == 2
|
||||||
|
assert s.name == "Spotify"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_audio_source_status_parse() -> None:
|
||||||
|
# idx=1, seq=42, position=1, field=2, text="Now Playing"
|
||||||
|
payload = bytes([0, 1, 42, 1, 2]) + b"Now Playing\x00rest"
|
||||||
|
s = AudioSourceStatus.parse(payload)
|
||||||
|
assert s.index == 1
|
||||||
|
assert s.sequence == 42
|
||||||
|
assert s.position == 1
|
||||||
|
assert s.field_id == 2
|
||||||
|
assert s.text == "Now Playing"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- UserSetting ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_user_setting_properties_parse() -> None:
|
||||||
|
# type=Temperature(3), value=140 (= 30°C)
|
||||||
|
payload = bytes([13, 0, 5, 3, 0, 140]) + _name_field("HotPoint", 15)
|
||||||
|
s = UserSettingProperties.parse(payload)
|
||||||
|
assert s.index == 5
|
||||||
|
assert s.name == "HotPoint"
|
||||||
|
assert s.setting_type == 3
|
||||||
|
assert s.value == 140
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_user_setting_status_parse() -> None:
|
||||||
|
payload = bytes([0, 5, 3, 0, 140])
|
||||||
|
s = UserSettingStatus.parse(payload)
|
||||||
|
assert s.index == 5
|
||||||
|
assert s.setting_type == 3
|
||||||
|
assert s.value == 140
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Dispatch tables ------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_object_type_to_properties_dispatch() -> None:
|
||||||
|
assert OBJECT_TYPE_TO_PROPERTIES[ObjectType.ZONE].__name__ == "ZoneProperties"
|
||||||
|
assert OBJECT_TYPE_TO_PROPERTIES[ObjectType.UNIT].__name__ == "UnitProperties"
|
||||||
|
assert (
|
||||||
|
OBJECT_TYPE_TO_PROPERTIES[ObjectType.THERMOSTAT].__name__
|
||||||
|
== "ThermostatProperties"
|
||||||
|
)
|
||||||
|
assert OBJECT_TYPE_TO_PROPERTIES[ObjectType.MESSAGE].__name__ == "MessageProperties"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_object_type_to_status_dispatch() -> None:
|
||||||
|
assert OBJECT_TYPE_TO_STATUS[ObjectType.ZONE].__name__ == "ZoneStatus"
|
||||||
|
assert OBJECT_TYPE_TO_STATUS[ObjectType.AUXILIARY].__name__ == "AuxSensorStatus"
|
||||||
|
assert OBJECT_TYPE_TO_STATUS[ObjectType.AUDIO_ZONE].__name__ == "AudioZoneStatus"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_dispatch_round_trip_zone() -> None:
|
||||||
|
"""A caller that only knows the object_type byte can still parse
|
||||||
|
a Properties payload through the dispatch table."""
|
||||||
|
payload = (
|
||||||
|
bytes([1, 0, 42, 0, 0, 0, 1, 0])
|
||||||
|
+ _name_field("Front Door", 15)
|
||||||
|
)
|
||||||
|
parser = OBJECT_TYPE_TO_PROPERTIES[payload[0]]
|
||||||
|
parsed = parser.parse(payload)
|
||||||
|
assert parsed.index == 42
|
||||||
|
assert parsed.name == "Front Door"
|
||||||
Loading…
x
Reference in New Issue
Block a user