Skip to content

Commit 1e74b67

Browse files
committed
Report what the appliance is doing, not only what it was asked to do
Each function says separately whether it is running. Without that, a compressor that has reached its target looks exactly like one still working towards it -- both report mode Cool. Also trims the inventory log to the device that reported it, rather than repeating everything seen so far on every burst.
1 parent 4d05067 commit 1e74b67

6 files changed

Lines changed: 129 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ tested.
2323
| Target temperature | 16–30 °C |
2424
| Current temperature | as the appliance measures it |
2525
| Fan | Auto, Low, Mid, High, Night |
26+
| Action | what the appliance is *doing*: cooling, heating, drying, fan, idle |
2627

2728
The modes are the ones the appliance reports about itself. Plain heating is
2829
absent on purpose: an Aventa heats through the heat pump, which is the

custom_components/truma_aventa/climate.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from homeassistant.components.climate import (
88
ClimateEntity,
99
ClimateEntityFeature,
10+
HVACAction,
1011
HVACMode,
1112
)
1213
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
@@ -87,6 +88,29 @@ def hvac_mode(self) -> HVACMode | None:
8788
mode = self.coordinator.data.mode
8889
return _MODE_TO_HVAC.get(mode) if mode is not None else None
8990

91+
@property
92+
def hvac_action(self) -> HVACAction | None:
93+
"""What the appliance is doing, as opposed to what it was asked to do.
94+
95+
Each function reports separately whether it is running, which is the
96+
only way to tell a compressor that has reached the target from one
97+
that is still working towards it.
98+
"""
99+
data = self.coordinator.data
100+
if data.mode == MODE_OFF:
101+
return HVACAction.OFF
102+
if data.cooling_active:
103+
return HVACAction.COOLING
104+
if data.heating_active:
105+
return HVACAction.HEATING
106+
if data.dehumid_active:
107+
return HVACAction.DRYING
108+
if data.circulation_active:
109+
return HVACAction.FAN
110+
if data.mode is None:
111+
return None
112+
return HVACAction.IDLE
113+
90114
@property
91115
def current_temperature(self) -> float | None:
92116
"""Room temperature as the appliance measures it."""

custom_components/truma_aventa/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@
3030
"bleak-retry-connector>=3.5.0",
3131
"cbor2>=5.6.0"
3232
],
33-
"version": "0.9.2"
33+
"version": "0.9.3"
3434
}

custom_components/truma_aventa/truma_ble/device.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@
8484
(TOPIC_AIR_COOLING, "Temp"): "current_temperature",
8585
(TOPIC_AIR_HEATING, "Mode"): "heating_fan_mode",
8686
(TOPIC_AIR_CIRCULATION, "FanLevel"): "fan_level",
87+
(TOPIC_AIR_COOLING, "Active"): "cooling_active",
88+
(TOPIC_AIR_HEATING, "Active"): "heating_active",
89+
(TOPIC_AIR_CIRCULATION, "Active"): "circulation_active",
90+
(TOPIC_AIR_DEHUMID, "Active"): "dehumid_active",
8791
(TOPIC_AMBIENT_LIGHT, "Active"): "light_on",
8892
(TOPIC_AMBIENT_LIGHT, "LightStep"): "light_step",
8993
(TOPIC_IDENTIFY, "Name"): "name",
@@ -591,12 +595,14 @@ def _handle_frame(self, frame: Frame) -> dict[str, Any]:
591595
if "LastMessage" in body and _LOGGER.isEnabledFor(logging.DEBUG):
592596
# The sentinel ends a discovery burst, which is the one moment the
593597
# full inventory of what this appliance reports is known.
598+
prefix = f"{frame.src:04X}/"
599+
own = sorted(key for key in raw if key.startswith(prefix))
594600
_LOGGER.debug(
595601
"%s: 0x%04X reported %d parameter(s): %s",
596602
self._name,
597603
frame.src,
598-
len(raw),
599-
", ".join(f"{key}={raw[key]!r}" for key in sorted(raw)),
604+
len(own),
605+
", ".join(f"{key[len(prefix) :]}={raw[key]!r}" for key in own),
600606
)
601607
if _LOGGER.isEnabledFor(logging.DEBUG):
602608
_LOGGER.debug(

custom_components/truma_aventa/truma_ble/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ class TrumaState:
4545
#: AirCirculation, used while ventilating.
4646
fan_level: int | None = None
4747

48+
#: Whether each function is actually running. The mode says what the
49+
#: appliance was asked to do; these say what it is doing.
50+
cooling_active: int | None = None
51+
heating_active: int | None = None
52+
circulation_active: int | None = None
53+
dehumid_active: int | None = None
54+
4855
#: AmbientLight.
4956
light_on: int | None = None
5057
light_step: int | None = None

tests/test_climate.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Tests for the climate entity."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
from typing import Any
8+
9+
import pytest
10+
from homeassistant.components.climate import HVACAction, HVACMode
11+
12+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
13+
14+
from custom_components.truma_aventa.climate import TrumaClimate
15+
from custom_components.truma_aventa.truma_ble.const import (
16+
MODE_COOLING,
17+
MODE_OFF,
18+
MODE_VENTILATING,
19+
)
20+
from custom_components.truma_aventa.truma_ble.models import TrumaState
21+
22+
23+
class _Device:
24+
address = "FC:DE:C5:F0:A6:35"
25+
name = "Truma iNetX-F0A635"
26+
27+
28+
class _Coordinator:
29+
"""The little of a coordinator an entity touches before it is added."""
30+
31+
def __init__(self, state: TrumaState) -> None:
32+
self.data = state
33+
self.device = _Device()
34+
self.key = "Truma iNetX-F0A635"
35+
self.available = True
36+
self.last_update_success = True
37+
38+
def async_add_listener(self, *_: Any) -> Any:
39+
return lambda: None
40+
41+
42+
def _climate(**state: Any) -> TrumaClimate:
43+
"""A climate entity over the given state."""
44+
return TrumaClimate(_Coordinator(TrumaState(**state)))
45+
46+
47+
def test_nothing_known_yet_reports_nothing() -> None:
48+
"""Before the appliance has answered there is no action to report."""
49+
assert _climate().hvac_action is None
50+
51+
52+
def test_switched_off_is_off() -> None:
53+
"""Off is a state of its own, not idleness."""
54+
assert _climate(mode=MODE_OFF).hvac_action is HVACAction.OFF
55+
56+
57+
def test_cooling_while_the_compressor_runs() -> None:
58+
"""The mode says what was asked for; Active says what is happening."""
59+
entity = _climate(mode=MODE_COOLING, cooling_active=1)
60+
assert entity.hvac_mode is HVACMode.COOL
61+
assert entity.hvac_action is HVACAction.COOLING
62+
63+
64+
def test_at_temperature_the_appliance_is_idle() -> None:
65+
"""Cooling requested, compressor off: the target has been reached."""
66+
entity = _climate(mode=MODE_COOLING, cooling_active=0)
67+
assert entity.hvac_mode is HVACMode.COOL
68+
assert entity.hvac_action is HVACAction.IDLE
69+
70+
71+
@pytest.mark.parametrize(
72+
("running", "expected"),
73+
[
74+
("heating_active", HVACAction.HEATING),
75+
("dehumid_active", HVACAction.DRYING),
76+
("circulation_active", HVACAction.FAN),
77+
],
78+
)
79+
def test_each_function_reports_its_own_action(running: str, expected: Any) -> None:
80+
"""Every function says separately whether it is running."""
81+
assert _climate(mode=MODE_VENTILATING, **{running: 1}).hvac_action is expected
82+
83+
84+
def test_temperatures_come_from_tenths() -> None:
85+
"""The appliance counts in tenths of a degree."""
86+
entity = _climate(current_temperature=251, target_temperature=220)
87+
assert entity.current_temperature == 25.1
88+
assert entity.target_temperature == 22.0

0 commit comments

Comments
 (0)