Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions OWNd/message/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,15 @@ def __init__(self, data: str) -> None:
elif self._dimension_value[0] == "13":
self._device_type = "H4684"
elif self._dimension_value[0] == "200":
# Code 200 is shared by every Linux-based gateway generation seen so
# far (F454, MyHomeServer1, MH202, F461, H4890; MyHOME #370, #466 and
# the F461 plant capture). WHO 1013 dimension 1 tells them apart.
self._device_type = "F454 / MyHomeServer1 / MH202 / F461 / H4890"
# Code 200 is shared across modern gateways (F454, MyHomeServer1,
# MH202, F461, H4890; plus emulated F450/F459/F460 in trace-corpus.tsv)
# and MH201 (fw 3.3.6, 3.6.27; MyHOME #370, #466 and plant fixtures;
# catalogued on 030644 outside the Linux QEMU fleet). WHO 1013
# dimension 1 tells them apart (the MH201 on 3.6.27 answers only
# *#1013**1##, model 10, and NACKs *#1013*0*1##).
self._device_type = (
"F454 / MyHomeServer1 / MH202 / F461 / H4890 / MH201"
)
else:
self._device_type = f"Unknown ({self._dimension_value[0]})"
self._human_readable_log = f"Gateway device type is: {self._device_type}."
Expand Down
18 changes: 9 additions & 9 deletions OWNd/message/heating.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@ def __init__(self, data: str) -> None:
self._type = MESSAGE_TYPE_ACTION
self._is_cooling = self._dimension_value[0] in _valve_active_states
self._is_heating = self._dimension_value[1] in _valve_active_states
self._is_active = self._is_cooling | self._is_heating
# Handle cooling valve status relative to fan speed/status
_cooling_value = int(self._dimension_value[0])
if _cooling_value == 0:
Expand All @@ -528,7 +527,6 @@ def __init__(self, data: str) -> None:
_fan_mode = _cooling_value - 5
if _fan_mode > 0:
self._cooling_fan_on = True
self._is_active = True
self._cooling_fan_speed = _fan_mode
self._human_readable_log = f"Zone {self._zone}'s cooling fan is on at speed {self._cooling_fan_speed}" # pylint: disable=line-too-long
else:
Expand All @@ -550,7 +548,6 @@ def __init__(self, data: str) -> None:
_fan_mode = _heating_value - 5
if _fan_mode > 0:
self._fan_on = True
self._is_active = True
self._fan_speed = _fan_mode
self._human_readable_log += (
f"; heating fan is on at speed {self._fan_speed}."
Expand All @@ -560,7 +557,7 @@ def __init__(self, data: str) -> None:
self._human_readable_log += "; heating fan is off."
# A fan-coil value on one side must not hide an open valve or a running fan
# on the other side: *#4*2*19*5*1## (cooling fan off, heating valve on) is an
# active zone (F461 plant capture, tests/fixtures/f461_live_2026-10-15/).
# active zone (F461 plant capture, tests/test_f461_live_plant.py).
self._is_active = bool(
self._is_cooling
or self._is_heating
Expand Down Expand Up @@ -923,13 +920,16 @@ def set_mode(
mode_name = mode
if mode == CLIMATE_MODE_AUTO:
# Automatic is the central unit's program: the firmware refuses *4*311*Z##
# (NACK, no bus frame; OWNd#77 A3, oracle3/out/termo_f461.tsv) and only
# forwards *4*311*#Z##, a frame addressed to the central unit, so
# (NACK, no bus frame across all 11 emulated gateways in thermo-zone-mode.tsv
# and ownd-pr82-heating.tsv; OWNd#77 A3) and only forwards *4*311*#Z##, a
# frame addressed to the central unit (bus D1 00 03 02 C2 ZZ 00 00), so
# standalone does not apply. Protection (302 / 102 / 202) is different:
# the plain zone form is forwarded as a direct zone command (bus
# 93 ZZ 30 04) and is the only form a standalone thermostat acts on;
# the #Z form goes to a central unit and is ignored when there is none
# (F461 plant capture, tests/fixtures/f461_live_2026-10-15/).
# 93 ZZ 30 04, like *4*303*Z## -> 93 ZZ 30 0F in thermo-zone-mode.tsv)
# and is the only form a standalone thermostat acts on; the #Z form goes
# to a central unit (bus D1 00 03 02 C2 ZZ 07 00 in thermo-ts10.tsv) and
# is ignored when there is none (F461 plant capture,
# tests/test_f461_live_plant.py).
if not zone.startswith("#"):
zone = f"#{zone}"

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ OWNd maintains an automated test suite with strict **100.0% line coverage** veri
| [`OWNd/message/automation.py`](OWNd/message/automation.py) | **100%** | WHO 2: Automation and motorized shutter events and commands |
| [`OWNd/message/base.py`](OWNd/message/base.py) | **100%** | Base message, signaling, and protocol dispatcher registries |
| [`OWNd/message/cen.py`](OWNd/message/cen.py) | **100%** | WHO 15 & WHO 25: CEN, CEN+, and dry contact control events/commands |
| [`OWNd/message/door_entry.py`](OWNd/message/door_entry.py) | **100%** | WHO 6 & WHO 8: Door entry, lock, and video intercom events and commands |
| [`OWNd/message/door_entry.py`](OWNd/message/door_entry.py) | **100%** | WHO 6 & WHO 8: Door entry, electric lock, and video intercom events/commands |
| [`OWNd/message/energy.py`](OWNd/message/energy.py) | **100%** | WHO 18: Energy management, power, and diagnostic pulse events/commands |
| [`OWNd/message/gateway.py`](OWNd/message/gateway.py) | **100%** | WHO 13: Gateway management, clock/date configuration, and model telemetry |
| [`OWNd/message/heating.py`](OWNd/message/heating.py) | **100%** | WHO 4: Thermoregulation, heating/cooling zone control, and temperature decoders |
Expand Down
2 changes: 1 addition & 1 deletion scripts/sync_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"OWNd/message/automation.py": "WHO 2: Automation and motorized shutter events and commands",
"OWNd/message/base.py": "Base message, signaling, and protocol dispatcher registries",
"OWNd/message/cen.py": "WHO 15 & WHO 25: CEN, CEN+, and dry contact control events/commands",
"OWNd/message/door_entry.py": "WHO 6 & WHO 8: Door entry, lock, and video intercom events and commands",
"OWNd/message/door_entry.py": "WHO 6 & WHO 8: Door entry, electric lock, and video intercom events/commands",
"OWNd/message/energy.py": "WHO 18: Energy management, power, and diagnostic pulse events/commands",
"OWNd/message/gateway.py": "WHO 13: Gateway management, clock/date configuration, and model telemetry",
"OWNd/message/heating.py": "WHO 4: Thermoregulation, heating/cooling zone control, and temperature decoders",
Expand Down
12 changes: 7 additions & 5 deletions tests/test_f461_live_plant.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
valve states had set it. The plant reports this frame on every heating-season read.
2. ``set_mode(zone, "protection" | "antifreeze" | "thermal_protection", standalone=True)``
produced the ``#Z`` form, which the gateway forwards to a central unit (emulated
``bt_termo``: ``*4*302*#23##`` -> ``D1 00 03 02 C2 17 07 00``) and which a standalone
thermostat ignores; the plain form (``93 17 30 04``) is the one that works. Only
``311`` (automatic) is a central-unit-only command (``*4*311*Z##`` is NACKed).
``bt_termo``: ``*4*302*#23##`` -> ``D1 00 03 02 C2 17 07 00`` in ``thermo-ts10.tsv``)
and which a standalone thermostat ignores; the plain form (``93 17 30 04``) is the one
that works. Only ``311`` (automatic) is a central-unit-only command (``*4*311*Z##`` is
NACKed across all 11 emulated gateways in ``thermo-zone-mode.tsv``).
3. WHO 13 device type ``200`` was labelled ``F454`` although F454, MyHomeServer1, MH202,
F461 and H4890 all answer it.
F461, H4890 and MH201 (fw 3.3.6, 3.6.27; plus emulated F450/F459/F460 in
``trace-corpus.tsv``) all answer it.
"""
from __future__ import annotations

Expand Down Expand Up @@ -178,7 +180,7 @@ def test_capture_shows_only_the_plain_protection_form_acts() -> None:
def test_device_type_200_is_not_a_single_model() -> None:
msg = OWNEvent.parse("*#13**15*200##")
assert msg.device_type_code == "200"
assert "F454" in msg.device_type and "F461" in msg.device_type
assert "F454" in msg.device_type and "F461" in msg.device_type and "MH201" in msg.device_type
assert msg.human_readable_log == f"Gateway device type is: {msg.device_type}."
assert OWNEvent.parse("*#13**15*2##").device_type == "MHServer"
assert OWNEvent.parse("*#13**16*2*0*11##").device_type_code is None
Expand Down
2 changes: 1 addition & 1 deletion tests/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ def test_owngateway_event_properties() -> None:
assert evt_fw.device_type is None

evt_dev = OWNGatewayEvent("*#13**15*200##")
assert evt_dev.device_type == "F454 / MyHomeServer1 / MH202 / F461 / H4890"
assert evt_dev.device_type == "F454 / MyHomeServer1 / MH202 / F461 / H4890 / MH201"
assert evt_dev.device_type_code == "200"
assert evt_dev.firmware_version is None

Expand Down
Loading