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
6 changes: 6 additions & 0 deletions OWNd/message/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,12 @@
MESSAGE_TYPE_MAIN_TEMPERATURE,
MESSAGE_TYPE_MODE,
MESSAGE_TYPE_MODE_TARGET,
MESSAGE_TYPE_SEASON,
MESSAGE_TYPE_SECONDARY_TEMPERATURE,
MESSAGE_TYPE_TARGET_TEMPERATURE,
MESSAGE_TYPE_ZONE_STATE,
SEASON_CONDITIONING,
SEASON_HEATING,
ZONE_CONTEXT_AUTOMATIC,
ZONE_CONTEXT_COOLING,
ZONE_CONTEXT_GENERIC,
Expand Down Expand Up @@ -145,8 +148,11 @@
"MESSAGE_TYPE_LOCAL_TARGET_TEMPERATURE",
"MESSAGE_TYPE_MODE",
"MESSAGE_TYPE_MODE_TARGET",
"MESSAGE_TYPE_SEASON",
"MESSAGE_TYPE_FAN_SPEED",
"MESSAGE_TYPE_ZONE_STATE",
"SEASON_CONDITIONING",
"SEASON_HEATING",
"CLIMATE_MODE_OFF",
"CLIMATE_MODE_HEAT",
"CLIMATE_MODE_COOL",
Expand Down
138 changes: 132 additions & 6 deletions OWNd/message/heating.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MESSAGE_TYPE_LOCAL_TARGET_TEMPERATURE = "local_target_temperature"
MESSAGE_TYPE_MODE = "hvac_mode"
MESSAGE_TYPE_MODE_TARGET = "hvac_mode_target"
MESSAGE_TYPE_SEASON = "hvac_season"

MESSAGE_TYPE_FAN_SPEED = "fan_speed"
MESSAGE_TYPE_ZONE_STATE = "zone_state"
Expand All @@ -25,6 +26,9 @@
CLIMATE_MODE_COOL = "cool"
CLIMATE_MODE_AUTO = "auto"

SEASON_HEATING = "heating"
SEASON_CONDITIONING = "conditioning"

LOCAL_CONTROL_NORMAL = "normal"
LOCAL_CONTROL_OFFSET = "offset"
LOCAL_CONTROL_OFF = "local_off"
Expand Down Expand Up @@ -173,6 +177,7 @@ def __init__(self, data: str) -> None:

self._mode = None
self._mode_name = None
self._season: str | None = None
self._zone_context: str | None = None
self._zone_state: str | None = None
self._set_temperature = None
Expand All @@ -183,9 +188,12 @@ 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
self._holiday_days: int | None = None

self._is_active = None
self._is_heating = None
Expand All @@ -201,29 +209,66 @@ def __init__(self, data: str) -> None:

if self._what is not None:
self._mode = int(self._what)
if self._mode in [103, 203, 303, 102, 202, 302]:

if self._mode in (0, 1) and not self._what_param:
# Zone operation mode frame: the zone (or the central unit)
# is operating in the heating (1) or conditioning (0) season.
# It is not an operating mode change: Legrand WHO 4 p. 13, 16,
# 19 and 63 list it next to the setpoint frames as "zone
# operation mode acquire frame"; the MyHomeServer1 translator
# emits it with the dimension 12 setpoint report; BTicino's
# client keeps a zone in automatic on it (libqtdevices
# TS10_1_0_23 probe_device.cpp:240-253). Manual / automatic /
# off arrive as 110 / 111 / 103 etc. WHAT 0 / 1 with a
# parameter (0#T, 1#T) is in neither the specification nor
# any capture, so it falls through to the unknown branch.
self._type = MESSAGE_TYPE_SEASON
self._mode_name = None
self._season = (
SEASON_HEATING if self._mode == 1 else SEASON_CONDITIONING
)
self._human_readable_log = (
f"Zone {self._zone}'s season is {self._season}."
)
elif self._mode in [103, 203, 303, 102, 202, 302]:
self._type = MESSAGE_TYPE_MODE
self._mode_name = CLIMATE_MODE_OFF
if self._mode in (102, 103):
self._season = SEASON_HEATING
elif self._mode in (202, 203):
self._season = SEASON_CONDITIONING
self._human_readable_log = (
f"Zone {self._zone}'s mode is set to '{self._mode_name}'"
)
elif (
self._mode in [0, 210, 211, 212, 215]
self._mode in [210, 211, 212, 215]
or (self._mode >= 2101 and self._mode <= 2103)
or (self._mode >= 2201 and self._mode <= 2216)
or (self._mode >= 23001 and self._mode <= 23255)
):
# 23xxx: holiday days in conditioning mode (Legrand WHO 4
# p. 5 / p. 64; libqtdevices thermal_device.cpp:58, 258-262).
self._type = MESSAGE_TYPE_MODE
self._mode_name = CLIMATE_MODE_COOL
self._season = SEASON_CONDITIONING
if self._mode >= 23001:
self._holiday_days = self._mode % 1000
self._human_readable_log = (
f"Zone {self._zone}'s mode is set to '{self._mode_name}'"
)
elif (
self._mode in [1, 110, 111, 112, 115]
self._mode in [110, 111, 112, 115]
or (self._mode >= 1101 and self._mode <= 1103)
or (self._mode >= 1201 and self._mode <= 1216)
or (self._mode >= 13001 and self._mode <= 13255)
):
# 13xxx: holiday days in heating mode (Legrand WHO 4 p. 5 /
# p. 64; libqtdevices thermal_device.cpp:68, 303-307).
self._type = MESSAGE_TYPE_MODE
self._mode_name = CLIMATE_MODE_HEAT
self._season = SEASON_HEATING
if self._mode >= 13001:
self._holiday_days = self._mode % 1000
self._human_readable_log = (
f"Zone {self._zone}'s mode is set to '{self._mode_name}'"
)
Expand All @@ -232,11 +277,11 @@ def __init__(self, data: str) -> None:
or (self._mode >= 3101 and self._mode <= 3116)
or (self._mode >= 3201 and self._mode <= 3216)
or (self._mode >= 33001 and self._mode <= 33255)
or (self._mode >= 23001 and self._mode <= 23255)
or (self._mode >= 13001 and self._mode <= 13255)
):
self._type = MESSAGE_TYPE_MODE
self._mode_name = CLIMATE_MODE_AUTO
if self._mode >= 33001:
self._holiday_days = self._mode % 1000
self._human_readable_log = (
f"Zone {self._zone}'s mode is set to '{self._mode_name}'"
)
Expand All @@ -250,22 +295,77 @@ def __init__(self, data: str) -> None:
self._human_readable_log = (
f"Zone {self._zone}'s remote control is enabled"
)
elif self._mode in (22, 23, 24, 30, 31):
# Central unit system status (Legrand WHO 4 p. 5, p. 64):
# 22 at least one probe OFF, 23 at least one probe in
# antifreeze, 24 at least one probe in manual, 30 failure
# discovered, 31 central unit battery KO.
self._mode_name = None
_status_text = {
22: "at least one probe is OFF",
23: "at least one probe is in antifreeze",
24: "at least one probe is in manual mode",
30: "a failure was discovered",
31: "the central unit battery is KO",
}[self._mode]
self._human_readable_log = (
f"Zone {self._zone}'s central unit reports {_status_text}"
)
else:
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 <= 1103
or 2101 <= self._mode <= 2103
or 3101 <= self._mode <= 3116
):
self._program = self._mode % 100
elif (
1201 <= self._mode <= 1216
or 2201 <= self._mode <= 2216
or 3201 <= self._mode <= 3216
):
self._scenario = self._mode % 100

if (
(
self._mode in (115, 215, 315)
or 13001 <= self._mode <= 13255
or 23001 <= self._mode <= 23255
or 33001 <= self._mode <= 33255
)
and self._what_param
and self._what_param[0]
):
# Holiday daily plan (115#P) and holiday days (13DDD#P, 23DDD#P,
# 33DDD#P, as sent by set_central_holiday): the parameter is the
# weekly program the central unit resumes afterwards, 1101-1103 /
# 2101-2103 / 31PP (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:
self._human_readable_log += f" at {self._set_temperature}°C."
else:
self._human_readable_log += "."
else:
elif self._type != MESSAGE_TYPE_SEASON:
self._human_readable_log += "."

if self._dimension == 0: # Temperature
Expand Down Expand Up @@ -580,6 +680,17 @@ def zone(self) -> int:
def mode(self) -> str | None:
return self._mode_name

@property
def season(self) -> str | None:
"""SEASON_HEATING or SEASON_CONDITIONING named by the WHAT, else None.

Set for the bare season frame (WHAT 1 / 0, message_type
MESSAGE_TYPE_SEASON, mode None) and for every season-specific mode
WHAT (1xx, 11xx, 12xx, 13xxx heating; 2xx, 21xx, 22xx, 23xxx
conditioning). Generic WHATs (3xx, 31xx, 32xx, 33xxx) have no season.
"""
return self._season

@property
def zone_context(self) -> str | None:
"""DIMENSION 7 thermal context (ZONE_CONTEXT_*), else None."""
Expand Down Expand Up @@ -657,11 +768,26 @@ 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..3 for seasonal, 1..16 for generic) 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."""
return self._holiday_end_date

@property
def holiday_days(self) -> int | None:
"""Holiday days (1..255) named by a 13xxx/23xxx/33xxx WHAT, else None."""
return self._holiday_days

@property
def holiday_end_time(self) -> tuple[int, int] | None:
"""Holiday / weekend end time (hour, minute), or None."""
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,14 @@ frame_cenplus_held = OWNCenPlusCommand.still_held(where="21", button=5)
# CEN+ (WHO=25): Button 5 release after a long press -> *25*24#5*21##
frame_cenplus_rel = OWNCenPlusCommand.release(where="21", button=5)

# Central Unit (WHO=4): Set 3550 (#0) master mode to Heating at 21.5°C -> *4*1#0215*#0##
frame_heat = OWNHeatingCommand.set_central_mode(where="#0", mode="heating", temperature=21.5)
# Central Unit (WHO=4): Set 3550 (#0) master mode to Heating -> *4*1*#0##
frame_heat = OWNHeatingCommand.set_central_mode(where="#0", mode="heating")

# Central Unit (WHO=4): Set 4695 (#0#1) master mode to Cooling at 24.0°C -> *4*2#0240*#0#1##
frame_cool = OWNHeatingCommand.set_central_mode(where="#0#1", mode="cooling", temperature=24.0)
# Central Unit (WHO=4): Set 4695 (#0#1) master mode to Cooling -> *4*0*#0#1##
frame_cool = OWNHeatingCommand.set_central_mode(where="#0#1", mode="cooling")

# Central Unit (WHO=4): Turn Central Unit OFF -> *4*303#0215*#0##
frame_off = OWNHeatingCommand.set_central_off(where="#0", mode="heating", temperature=21.5)
# Central Unit (WHO=4): Turn Central Unit OFF -> *4*303*#0##
frame_off = OWNHeatingCommand.set_central_mode(where="#0", mode="off")
```

---
Expand Down
Loading
Loading