Skip to content

Commit

Permalink
Fix #661 - central boiler doesn't starts with Sonoff TRVZB
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Collin committed Nov 25, 2024
1 parent c090692 commit f29097f
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 26 deletions.
11 changes: 11 additions & 0 deletions custom_components/versatile_thermostat/base_thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"max_power_sensor_entity_id",
"temperature_unit",
"is_device_active",
"nb_device_actives",
"target_temperature_step",
"is_used_by_central_boiler",
"temperature_slope",
Expand Down Expand Up @@ -995,6 +996,15 @@ def is_device_active(self) -> bool:
return True
return False

@property
def nb_device_actives(self) -> int:
"""Calculate the number of active devices"""
ret = 0
for under in self._underlyings:
if under.is_device_active:
ret += 1
return ret

@property
def current_temperature(self) -> float | None:
"""Return the sensor temperature."""
Expand Down Expand Up @@ -2661,6 +2671,7 @@ def update_custom_attributes(self):
"timezone": str(self._current_tz),
"temperature_unit": self.temperature_unit,
"is_device_active": self.is_device_active,
"nb_device_actives": self.nb_device_actives,
"ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
Expand Down
12 changes: 6 additions & 6 deletions custom_components/versatile_thermostat/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def __init__(
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

old_state = self._attr_is_on
self._attr_is_on = self.my_climate.security_state is True
Expand Down Expand Up @@ -147,7 +147,7 @@ def __init__(
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

old_state = self._attr_is_on
self._attr_is_on = self.my_climate.overpowering_state is True
Expand Down Expand Up @@ -186,7 +186,7 @@ def __init__(
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

old_state = self._attr_is_on
# Issue 120 - only take defined presence value
Expand Down Expand Up @@ -236,7 +236,7 @@ def __init__(
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
# Issue 120 - only take defined presence value
if self.my_climate.motion_state in [STATE_ON, STATE_OFF]:
Expand Down Expand Up @@ -277,7 +277,7 @@ def __init__(
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""

_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
# Issue 120 - only take defined presence value
if self.my_climate.presence_state in [STATE_ON, STATE_OFF]:
Expand Down Expand Up @@ -317,7 +317,7 @@ def __init__(
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on
if self.my_climate.window_bypass_state in [True, False]:
self._attr_is_on = self.my_climate.window_bypass_state
Expand Down
38 changes: 20 additions & 18 deletions custom_components/versatile_thermostat/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

energy = self.my_climate.total_energy
if energy is None:
Expand Down Expand Up @@ -188,7 +188,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf(
self.my_climate.mean_cycle_power
Expand Down Expand Up @@ -245,7 +245,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

on_percent = (
float(self.my_climate.proportional_algorithm.on_percent)
Expand Down Expand Up @@ -300,7 +300,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

old_state = self._attr_native_value
self._attr_native_value = self.my_climate.valve_open_percent
Expand Down Expand Up @@ -342,7 +342,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

on_time = (
float(self.my_climate.proportional_algorithm.on_time_sec)
Expand Down Expand Up @@ -391,7 +391,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

off_time = (
float(self.my_climate.proportional_algorithm.off_time_sec)
Expand Down Expand Up @@ -439,7 +439,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

old_state = self._attr_native_value
self._attr_native_value = self.my_climate.last_temperature_measure
Expand Down Expand Up @@ -468,7 +468,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

old_state = self._attr_native_value
self._attr_native_value = self.my_climate.last_ext_temperature_measure
Expand Down Expand Up @@ -497,7 +497,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

last_slope = self.my_climate.last_temperature_slope
if last_slope is None:
Expand Down Expand Up @@ -550,7 +550,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

new_temp = self.my_climate.regulated_target_temp
if new_temp is None:
Expand Down Expand Up @@ -601,7 +601,7 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
# _LOGGER.debug("%s - climate state change", self._attr_unique_id)

new_ema = self.my_climate.ema_temperature
if new_ema is None:
Expand Down Expand Up @@ -732,21 +732,23 @@ async def calculate_nb_active_devices(self, _):
"""Calculate the number of active VTherm that have an
influence on central boiler"""

_LOGGER.debug("%s - calculating the number of active VTherm", self)
_LOGGER.debug(
"%s - calculating the number of active underlying device for boiler activation",
self,
)
nb_active = 0
for entity in self._entities:
_LOGGER.debug(
"Examining the hvac_action of %s",
entity.name,
)
if (
entity.hvac_mode in [HVACMode.HEAT, HVACMode.AUTO]
and entity.hvac_action == HVACAction.HEATING
):
for under in entity.underlying_entities:
nb_active += 1 if under.is_device_active else 0
nb_active += entity.nb_device_actives

self._attr_native_value = nb_active
_LOGGER.debug(
"%s - Number of active underlying entities is %s", self, nb_active
)

self.async_write_ha_state()

def __str__(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ def is_device_active(self) -> bool:
"""A hack to overrides the state from underlyings"""
return self.valve_open_percent > 0

@property
def nb_device_actives(self) -> int:
"""Calculate the number of active devices"""
if self.is_device_active:
return len(self._underlyings_valve_regulation)
else:
return 0

@overrides
async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
"""This should not be possible in valve regulation mode"""
Expand Down
9 changes: 9 additions & 0 deletions tests/test_central_boiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ async def test_update_central_boiler_state_multiple(
assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert entity.nb_device_actives == 0

assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
Expand Down Expand Up @@ -337,6 +338,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1)

assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 1

assert mock_service_call.call_count == 1
# No switch of the boiler
Expand Down Expand Up @@ -366,6 +368,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1)

assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 2

# Only the first heater is started by the algo
assert mock_service_call.call_count == 1
Expand Down Expand Up @@ -591,6 +594,7 @@ async def test_update_central_boiler_state_simple_valve(
now: datetime = datetime.now(tz=tz)

assert entity.hvac_mode == HVACMode.HEAT
assert entity.nb_device_actives == 0

boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
Expand All @@ -612,6 +616,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1)

assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 1

assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
Expand Down Expand Up @@ -653,6 +658,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1)

assert entity.hvac_action == HVACAction.IDLE
assert entity.nb_device_actives == 0

assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
Expand Down Expand Up @@ -750,6 +756,7 @@ async def test_update_central_boiler_state_simple_climate(
now: datetime = datetime.now(tz=tz)

assert entity.hvac_mode == HVACMode.HEAT
assert entity.nb_device_actives == 0

boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
Expand All @@ -772,6 +779,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5)

assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 1

assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
Expand Down Expand Up @@ -813,6 +821,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5)

assert entity.hvac_action == HVACAction.IDLE
assert entity.nb_device_actives == 0

assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
Expand Down
11 changes: 9 additions & 2 deletions tests/test_overclimate_valve.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
)

assert mock_get_state.call_count > 5 # each temp sensor + each valve
assert vtherm.nb_device_actives == 0


# initialize the temps
Expand Down Expand Up @@ -200,6 +201,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get

assert vtherm.hvac_action is HVACAction.HEATING
assert vtherm.is_device_active is True
assert vtherm.nb_device_actives == 1

Check failure on line 204 in tests/test_overclimate_valve.py

View workflow job for this annotation

GitHub Actions / testu

test_over_climate_valve_mono assert 3 == 1 + where 3 = <entity climate.theoverclimatemockname=heat>.nb_device_actives

# 2. Starts heating very slowly (18.9 vs 19)
now = now + timedelta(minutes=2)
Expand Down Expand Up @@ -245,6 +247,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get

assert vtherm.hvac_action is HVACAction.HEATING
assert vtherm.is_device_active is True
assert vtherm.nb_device_actives == 1

# 3. Stop heating 21 > 19
now = now + timedelta(minutes=2)
Expand Down Expand Up @@ -290,8 +293,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get

assert vtherm.hvac_action is HVACAction.OFF
assert vtherm.is_device_active is False


assert vtherm.nb_device_actives == 0

await hass.async_block_till_done()

Expand Down Expand Up @@ -415,6 +417,7 @@ async def test_over_climate_valve_multi_presence(
await vtherm.async_set_hvac_mode(HVACMode.HEAT)

assert vtherm.target_temperature == 17.2
assert vtherm.nb_device_actives == 0

# 2: set presence on -> should activate the valve and change target
# fmt: off
Expand Down Expand Up @@ -445,6 +448,8 @@ async def test_over_climate_valve_multi_presence(
]
)

assert vtherm.nb_device_actives >= 2 # should be 2 but when run in // with the first test it give 3

# 3: set presence off -> should deactivate the valve and change target
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
Expand Down Expand Up @@ -473,3 +478,5 @@ async def test_over_climate_valve_multi_presence(
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)

assert vtherm.nb_device_actives == 0

0 comments on commit f29097f

Please sign in to comment.