Skip to content
Closed
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
36 changes: 36 additions & 0 deletions OWNd/message/heating.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ def __init__(self, data: str) -> None:
self._measured_temperature = None
self._secondary_temperature = None
self._measured_humidity = None
self._program: int | None = None
self._scenario: int | None = None
self._holiday_end_date: tuple[int, int, int] | None = None
self._holiday_end_time: tuple[int, int] | None = None
self._manual_timed_duration: tuple[int, int] | None = None
Expand Down Expand Up @@ -254,11 +256,35 @@ def __init__(self, data: str) -> None:
self._mode_name = None
self._human_readable_log = f"Zone {self._zone}'s mode is unknown"

# Program / scenario numbers carried in the WHAT (Legrand WHO 4
# p. 5 and p. 64; libqtdevices thermal_device.cpp:209-211, 246-256,
# 291-301): 11xx/21xx/31xx program, 12xx/22xx/32xx scenario.
if self._type == MESSAGE_TYPE_MODE:
if 1101 <= self._mode <= 1199 or 2101 <= self._mode <= 2199 or 3101 <= self._mode <= 3199:
self._program = self._mode % 100
elif 1201 <= self._mode <= 1299 or 2201 <= self._mode <= 2299 or 3201 <= self._mode <= 3299:
self._scenario = self._mode % 100

if (
self._mode in (115, 215, 315)
and self._what_param
and self._what_param[0]
):
# Holiday daily plan: the parameter is the weekly program the
# central unit resumes afterwards, 1101-1103 / 2101-2103
# (Legrand WHO 4 p. 56 and p. 64, "115#parameterH"); libqtdevices
# reads it as whatArgN(0) % 100 (thermal_device.cpp:241, 286).
# It is not a temperature. The grammar only admits digits in
# a WHAT parameter (base.py _STATUS), so int() cannot fail.
self._program = int(self._what_param[0]) % 100
self._human_readable_log += f" (program {self._program})."
elif (
self._type == MESSAGE_TYPE_MODE
and self._what_param
and self._what_param[0] is not None
):
# 110#T / 210#T manual with temperature (Legrand WHO 4 p. 23,
# p. 56), 312#T timed manual (libqtdevices thermal_device.cpp:378).
self._type = MESSAGE_TYPE_MODE_TARGET
self._set_temperature = who4_temperature(self._what_param[0])
if self._set_temperature is not None:
Expand Down Expand Up @@ -657,6 +683,16 @@ def cooling_fan_speed(self) -> int | None:
def cooling_fan_on(self) -> bool | None:
return self._cooling_fan_on

@property
def program(self) -> int | None:
"""Weekly program number (1..16) named by the WHAT, else None."""
return self._program

@property
def scenario(self) -> int | None:
"""Scenario number (1..16) named by the WHAT, else None."""
return self._scenario

@property
def holiday_end_date(self) -> tuple[int, int, int] | None:
"""Holiday / weekend end date (day, month, year), or None."""
Expand Down
54 changes: 54 additions & 0 deletions tests/test_who4_holiday_plan_program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""WHO 4: the parameter of 115#/215#/315# is a weekly program, not a temperature.

Evidence: Legrand "Open Web Net Language, WHO = 4" v2.0.0 p. 56 and p. 64
("115#parameterH", parameterH = 1101-1103); libqtdevices TS10_1_0_23
thermal_device.cpp:241 and :286 (``values_list[DIM_PROGRAM] = msg.whatArgN(0) % 100``);
firmware replay: ``*4*115#1102*#0##`` is ACKed by bt_termo on MyHomeServer1, F454,
F459, F461 and F450 and forwarded as ``D1 00 03 02 C1 14 01 00`` (program byte 01,
no temperature field), see docs in the pull request.
"""

from OWNd.message import (
CLIMATE_MODE_COOL,
CLIMATE_MODE_HEAT,
OWNEvent,
OWNHeatingEvent,
)


def test_holiday_plan_parameter_is_a_program_not_a_temperature():
msg = OWNEvent.parse("*4*115#1102*#0##")
assert isinstance(msg, OWNHeatingEvent)
assert msg.mode == CLIMATE_MODE_HEAT
assert msg.program == 2
assert msg.set_temperature is None
assert msg.message_type == "hvac_mode"


def test_cooling_holiday_plan_parameter():
msg = OWNEvent.parse("*4*215#2102*#0##")
assert msg.mode == CLIMATE_MODE_COOL
assert msg.program == 2
assert msg.set_temperature is None


def test_program_and_scenario_numbers_are_exposed():
# thermal_device.cpp:209-211: program = what - commandRange(what)
assert OWNEvent.parse("*4*1102*#0##").program == 2
assert OWNEvent.parse("*4*2102*#0##").program == 2
assert OWNEvent.parse("*4*3113*#0##").program == 13
assert OWNEvent.parse("*4*1203*#0##").scenario == 3
assert OWNEvent.parse("*4*3209*#0##").scenario == 9
assert OWNEvent.parse("*4*1*23##").program is None
assert OWNEvent.parse("*4*1*23##").scenario is None


def test_manual_with_temperature_parameter_still_reads_the_temperature():
# Legrand WHO 4 p. 23 / p. 56: 110#T manual heating with temperature.
msg = OWNEvent.parse("*4*110#0215*#0##")
assert msg.set_temperature == 21.5
assert msg.message_type == "hvac_mode_target"
# thermal_device.cpp:378: 312#T#2 timed manual, generic mode.
msg = OWNEvent.parse("*4*312#0220#2*#0##")
assert msg.set_temperature == 22.0
assert msg.message_type == "hvac_mode_target"
Loading