From f29097fbc2fc9758ee4b570d7c4e13ba062c2d39 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Mon, 25 Nov 2024 19:21:07 +0000 Subject: [PATCH] Fix #661 - central boiler doesn't starts with Sonoff TRVZB --- .../versatile_thermostat/base_thermostat.py | 11 ++++++ .../versatile_thermostat/binary_sensor.py | 12 +++--- .../versatile_thermostat/sensor.py | 38 ++++++++++--------- .../thermostat_climate_valve.py | 8 ++++ tests/test_central_boiler.py | 9 +++++ tests/test_overclimate_valve.py | 11 +++++- 6 files changed, 63 insertions(+), 26 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 06c5fa0..dc1d21f 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -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", @@ -995,6 +996,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): 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.""" @@ -2661,6 +2671,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "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), diff --git a/custom_components/versatile_thermostat/binary_sensor.py b/custom_components/versatile_thermostat/binary_sensor.py index edd491d..36a1e06 100644 --- a/custom_components/versatile_thermostat/binary_sensor.py +++ b/custom_components/versatile_thermostat/binary_sensor.py @@ -108,7 +108,7 @@ class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): @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 @@ -147,7 +147,7 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity @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 @@ -186,7 +186,7 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): @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 @@ -236,7 +236,7 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): @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]: @@ -277,7 +277,7 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity): 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]: @@ -317,7 +317,7 @@ class WindowByPassBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity @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 diff --git a/custom_components/versatile_thermostat/sensor.py b/custom_components/versatile_thermostat/sensor.py index 344315a..02d3b1d 100644 --- a/custom_components/versatile_thermostat/sensor.py +++ b/custom_components/versatile_thermostat/sensor.py @@ -133,7 +133,7 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity): @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: @@ -188,7 +188,7 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity): @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 @@ -245,7 +245,7 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity): @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) @@ -300,7 +300,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity): @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 @@ -342,7 +342,7 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity): @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) @@ -391,7 +391,7 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity): @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) @@ -439,7 +439,7 @@ class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): @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 @@ -468,7 +468,7 @@ class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): @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 @@ -497,7 +497,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity): @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: @@ -550,7 +550,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): @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: @@ -601,7 +601,7 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): @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: @@ -732,21 +732,23 @@ class NbActiveDeviceForBoilerSensor(SensorEntity): """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): diff --git a/custom_components/versatile_thermostat/thermostat_climate_valve.py b/custom_components/versatile_thermostat/thermostat_climate_valve.py index af779e0..4a30d11 100644 --- a/custom_components/versatile_thermostat/thermostat_climate_valve.py +++ b/custom_components/versatile_thermostat/thermostat_climate_valve.py @@ -275,6 +275,14 @@ class ThermostatOverClimateValve(ThermostatOverClimate): """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""" diff --git a/tests/test_central_boiler.py b/tests/test_central_boiler.py index be2915e..40c24c1 100644 --- a/tests/test_central_boiler.py +++ b/tests/test_central_boiler.py @@ -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 @@ -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 @@ -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 @@ -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" @@ -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( @@ -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( @@ -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" @@ -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( @@ -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( diff --git a/tests/test_overclimate_valve.py b/tests/test_overclimate_valve.py index 497fc2d..46b5aab 100644 --- a/tests/test_overclimate_valve.py +++ b/tests/test_overclimate_valve.py @@ -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 @@ -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 # 2. Starts heating very slowly (18.9 vs 19) now = now + timedelta(minutes=2) @@ -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) @@ -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() @@ -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 @@ -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, \ @@ -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