Compare commits

..

5 Commits

Author SHA1 Message Date
Jean-Marc Collin
9f238df4e7 Issue #698 - nb nb_device_active_for_boiler don't work for climate without hvac_action 2024-12-21 16:09:49 +00:00
Jean-Marc Collin
6226d26c6d With tests and list of active 2024-12-21 15:39:05 +00:00
Sebastian Noe
9839ed4920 Add attribute with active underlyings for easier tracking and setup (#658)
* Add attribute with active underlyings for easier tracking and setup

* Issue #645   add sonoff trvzb (#651)

* With Sonoff configuration ok

* Fix configuration

* Next (not finished)

* With 1rst implementation of VTherm TRVZB and underlying

* Work in simuated environment

* Fix Testus

* Release

* Fix release name

* Add #602 - implement a max_on_percent setting

* Calculate offset_calibration as room_temp - local_temp
Fix hvac_action calculation

* Fix hvac_action
Fix offset_calibration=room_temp - (local_temp - current_offset)

* Fix underlying target is not updated

* Issue #655 - combine motion and presence

* Fix Valve testus. Improve sending the open percent to valve

* Update custom_components/versatile_thermostat/translations/en.json

Co-authored-by: Alexander Dransfield <2844540+alexdrans@users.noreply.github.com>

* Indus step1

* Step 2 - renaming. All tests ok

* Step 2: manual tests ok

* First unit test ok

* Test multi ok

* All tests ok. Add a multi test for climate with valve regulation

* With testu for config_flow ok

* Documentation (not finished)

* Fix #661 - central boiler doesn't starts with Sonoff TRVZB

* Remove // testing

* Fix exception when there is no offset at all

* Fix class attributes and instance attributes mixing

* Documentation 2

* Documentation 3

* Documentation ++

* documentation

* Try to fix the central boiler calculation

* Fix #669

* Documentation ++

* Documentation ok for FR

* Readme FR|EN

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
Co-authored-by: Alexander Dransfield <2844540+alexdrans@users.noreply.github.com>

* Documentation rework

* En links

* Documentation issue #650

---------

Co-authored-by: Sebastian Noe <sebastian.schneider@boxine.de>
Co-authored-by: Jean-Marc Collin <jm.collin.78@gmail.com>
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
Co-authored-by: Alexander Dransfield <2844540+alexdrans@users.noreply.github.com>
2024-12-21 09:36:46 +01:00
Jean-Marc Collin
02f60770e8 Try to fix issue #702 2024-12-21 08:28:13 +00:00
Edwin ten Haaf
23ddff95b3 fix for 'It's not possible to set regulation mode to Expert using Automations' (#718)
Co-authored-by: Edwin ten Haaf <edwin.ten.haaf@rawworks.nl>
2024-12-20 08:41:16 +01:00
7 changed files with 436 additions and 40 deletions

View File

@@ -82,6 +82,10 @@ T = TypeVar("T", bound=UnderlyingEntity)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device.""" """Representation of a base class for all Versatile Thermostat device."""
# breaking change with 2024.12 climate workaround
_attr_swing_horizontal_modes = []
_attr_swing_horizontal_mode = ""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
ClimateEntity._entity_component_unrecorded_attributes.union( ClimateEntity._entity_component_unrecorded_attributes.union(
frozenset( frozenset(
@@ -127,7 +131,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"max_power_sensor_entity_id", "max_power_sensor_entity_id",
"temperature_unit", "temperature_unit",
"is_device_active", "is_device_active",
"nb_device_actives", "device_actives",
"target_temperature_step", "target_temperature_step",
"is_used_by_central_boiler", "is_used_by_central_boiler",
"temperature_slope", "temperature_slope",
@@ -997,14 +1001,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return False return False
@property @property
def nb_device_actives(self) -> int: def device_actives(self) -> int:
"""Calculate the number of active devices""" """Calculate the active devices"""
ret = 0 ret = []
for under in self._underlyings: for under in self._underlyings:
if under.is_device_active: if under.is_device_active:
ret += 1 ret.append(under.entity_id)
return ret return ret
@property
def nb_device_actives(self) -> int:
"""Calculate the number of active devices"""
return len(self.device_actives)
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the sensor temperature.""" """Return the sensor temperature."""
@@ -2676,6 +2685,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"timezone": str(self._current_tz), "timezone": str(self._current_tz),
"temperature_unit": self.temperature_unit, "temperature_unit": self.temperature_unit,
"is_device_active": self.is_device_active, "is_device_active": self.is_device_active,
"device_actives": self.device_actives,
"nb_device_actives": self.nb_device_actives, "nb_device_actives": self.nb_device_actives,
"ema_temp": self._ema_temp, "ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler, "is_used_by_central_boiler": self.is_used_by_central_boiler,

View File

@@ -118,7 +118,7 @@ async def async_setup_entry(
SERVICE_SET_AUTO_REGULATION_MODE, SERVICE_SET_AUTO_REGULATION_MODE,
{ {
vol.Required("auto_regulation_mode"): vol.In( vol.Required("auto_regulation_mode"): vol.In(
["None", "Light", "Medium", "Strong", "Slow"] ["None", "Light", "Medium", "Strong", "Slow", "Expert"]
), ),
}, },
"service_set_auto_regulation_mode", "service_set_auto_regulation_mode",

View File

@@ -644,6 +644,10 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
"""Representation of the threshold of the number of VTherm """Representation of the threshold of the number of VTherm
which should be active to activate the boiler""" which should be active to activate the boiler"""
_entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset({"active_device_ids"})
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor""" """Initialize the energy sensor"""
self._hass = hass self._hass = hass
@@ -653,6 +657,14 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self._attr_unique_id = "nb_device_active_boiler" self._attr_unique_id = "nb_device_active_boiler"
self._attr_value = self._attr_native_value = None # default value self._attr_value = self._attr_native_value = None # default value
self._entities = [] self._entities = []
self._attr_active_device_ids = [] # Holds the entity ids of active devices
@property
def extra_state_attributes(self) -> dict:
"""Return additional attributes for the sensor."""
return {
"active_device_ids": self._attr_active_device_ids,
}
@property @property
def icon(self) -> str | None: def icon(self) -> str | None:
@@ -718,19 +730,19 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self.calculate_nb_active_devices, self.calculate_nb_active_devices,
) )
_LOGGER.info( _LOGGER.info(
"%s - the underlyings that could controls the central boiler are %s", "%s - the underlyings that could control the central boiler are %s",
self, self,
underlying_entities_id, underlying_entities_id,
) )
self.async_on_remove(listener_cancel) self.async_on_remove(listener_cancel)
else: else:
_LOGGER.debug("%s - no VTherm could controls the central boiler", self) _LOGGER.debug("%s - no VTherm could control the central boiler", self)
await self.calculate_nb_active_devices(None) await self.calculate_nb_active_devices(None)
async def calculate_nb_active_devices(self, event: Event): async def calculate_nb_active_devices(self, event: Event):
"""Calculate the number of active VTherm that have an """Calculate the number of active VTherm that have an
influence on central boiler""" influence on the central boiler and update the list of active device names."""
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event) # _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
@@ -757,6 +769,8 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
old_state is not None old_state is not None
and new_state.state == old_state.state and new_state.state == old_state.state
and new_hvac_action == old_hvac_action and new_hvac_action == old_hvac_action
# issue 698 - force recalculation when underlying climate doesn't have any hvac_action
and new_hvac_action is not None
): ):
# A false state change # A false state change
return return
@@ -774,20 +788,28 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
) )
nb_active = 0 nb_active = 0
active_device_ids = []
for entity in self._entities: for entity in self._entities:
nb_active += entity.nb_device_actives device_actives = entity.device_actives
_LOGGER.debug( _LOGGER.debug(
"After examining the hvac_action of %s, nb_active is %s", "After examining the hvac_action of %s, device_actives is %s",
entity.name, entity.name,
nb_active, device_actives,
) )
nb_active += len(device_actives)
active_device_ids.extend(device_actives)
self._attr_native_value = nb_active self._attr_native_value = nb_active
_LOGGER.debug( self._attr_active_device_ids = active_device_ids
"%s - Number of active underlying entities is %s", self, nb_active
)
self.async_write_ha_state() self.async_write_ha_state()
@property
def active_device_ids(self) -> list:
"""Get the list of active device id"""
return self._attr_active_device_ids
def __str__(self): def __str__(self):
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"

View File

@@ -277,12 +277,15 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
return self.valve_open_percent > 0 return self.valve_open_percent > 0
@property @property
def nb_device_actives(self) -> int: def device_actives(self) -> int:
"""Calculate the number of active devices""" """Calculate the number of active devices"""
if self.is_device_active: if self.is_device_active:
return len(self._underlyings_valve_regulation) return [
under.opening_degree_entity_id
for under in self._underlyings_valve_regulation
]
else: else:
return 0 return []
@property @property
def activable_underlying_entities(self) -> list | None: def activable_underlying_entities(self) -> list | None:

View File

@@ -579,6 +579,7 @@ class MockNumber(NumberEntity):
def set_native_value(self, value: float): def set_native_value(self, value: float):
"""Change the value""" """Change the value"""
self._attr_native_value = value self._attr_native_value = value
self.async_write_ha_state()
async def create_thermostat( async def create_thermostat(

View File

@@ -2,7 +2,7 @@
""" Test the central_configuration """ """ Test the central_configuration """
import asyncio import asyncio
from datetime import datetime from datetime import datetime, timedelta
from unittest.mock import patch, call from unittest.mock import patch, call
@@ -29,6 +29,8 @@ from custom_components.versatile_thermostat.binary_sensor import (
CentralBoilerBinarySensor, CentralBoilerBinarySensor,
) )
from custom_components.versatile_thermostat.sensor import NbActiveDeviceForBoilerSensor
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -103,7 +105,7 @@ async def test_update_central_boiler_state_simple(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: switch1.entity_id, CONF_UNDERLYING_LIST: [switch1.entity_id],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -147,6 +149,13 @@ async def test_update_central_boiler_state_simple(
assert boiler_binary_sensor is not None assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a heater # 1. start a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -195,6 +204,9 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. stop a heater # 2. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -235,6 +247,9 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
@@ -272,10 +287,12 @@ async def test_update_central_boiler_state_multiple(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: switch1.entity_id, CONF_UNDERLYING_LIST: [
CONF_HEATER_2: switch2.entity_id, switch1.entity_id,
CONF_HEATER_3: switch3.entity_id, switch2.entity_id,
CONF_HEATER_4: switch4.entity_id, switch3.entity_id,
switch4.entity_id,
],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -302,10 +319,18 @@ async def test_update_central_boiler_state_multiple(
assert entity.underlying_entities[1].entity_id == "switch.switch2" assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3" assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4" assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert entity.nb_device_actives == 0 assert entity.device_actives == []
assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat # Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -338,7 +363,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 1 assert entity.device_actives == ["switch.switch1"]
assert mock_service_call.call_count == 1 assert mock_service_call.call_count == 1
# No switch of the boiler # No switch of the boiler
@@ -356,6 +381,9 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. start a 2nd heater # 2. start a 2nd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -368,7 +396,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 2 assert entity.device_actives == ["switch.switch1", "switch.switch2"]
# Only the first heater is started by the algo # Only the first heater is started by the algo
assert mock_service_call.call_count == 1 assert mock_service_call.call_count == 1
@@ -388,6 +416,12 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2 assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
]
# 3. start a 3rd heater # 3. start a 3rd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -436,6 +470,13 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3 assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
]
# 4. start a 4th heater # 4. start a 4th heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -466,6 +507,14 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 4 assert api.nb_active_device_for_boiler == 4
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 4
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 5. stop a heater # 5. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -484,6 +533,13 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3 assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 6. stop a 2nd heater # 6. stop a 2nd heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -524,6 +580,12 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2 assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
]
entity.remove_thermostat() entity.remove_thermostat()
@@ -558,7 +620,7 @@ async def test_update_central_boiler_state_simple_valve(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_VALVE: valve1.entity_id, CONF_UNDERLYING_LIST: [valve1.entity_id],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False, CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
@@ -594,7 +656,7 @@ async def test_update_central_boiler_state_simple_valve(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.nb_device_actives == 0 assert entity.device_actives == []
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor" hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -602,6 +664,13 @@ async def test_update_central_boiler_state_simple_valve(
assert boiler_binary_sensor is not None assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a valve # 1. start a valve
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -616,7 +685,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 1 assert entity.device_actives == ["number.valve1"]
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -644,6 +713,11 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.valve1",
]
# 2. stop a heater # 2. stop a heater
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -658,7 +732,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.nb_device_actives == 0 assert entity.device_actives == []
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -687,6 +761,9 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()
@@ -721,7 +798,7 @@ async def test_update_central_boiler_state_simple_climate(
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: climate1.entity_id, CONF_UNDERLYING_LIST: [climate1.entity_id],
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -748,6 +825,13 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler_threshold == 1 assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat # Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -756,7 +840,7 @@ async def test_update_central_boiler_state_simple_climate(
now: datetime = datetime.now(tz=tz) now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.nb_device_actives == 0 assert entity.device_actives == []
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity( boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor" hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -779,7 +863,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.HEATING assert entity.hvac_action == HVACAction.HEATING
assert entity.nb_device_actives == 1 assert entity.device_actives == ["climate.climate1"]
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -807,6 +891,11 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 1 assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"climate.climate1",
]
# 2. stop a climate # 2. stop a climate
with patch( with patch(
"homeassistant.core.ServiceRegistry.async_call" "homeassistant.core.ServiceRegistry.async_call"
@@ -821,7 +910,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.nb_device_actives == 0 assert entity.device_actives == []
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
@@ -850,6 +939,277 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 0 assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_climate_valve_regulation(
hass: HomeAssistant,
# skip_hass_states_is_state,
# skip_hass_states_get,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavior with a climate with valve regulation"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(hass, "climate1", "theClimate1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [climate1.entity_id],
CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"],
CONF_CLOSING_DEGREE_LIST: [],
CONF_OFFSET_CALIBRATION_LIST: [],
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_DTEMP: 0,
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
open_degree_entity = MockNumber(hass, "mock_opening_degree", "Opening degree")
open_degree_entity.set_native_value(0)
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
), patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entity._set_now(now)
await send_temperature_change_event(entity, 30, now)
await send_ext_temperature_change_event(entity, 30, now)
await hass.async_block_till_done()
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
# the VTherm should not heat now
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
assert entity.activable_underlying_entities[0]._percent_open == 0
assert entity.device_actives == []
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a climate
open_degree_entity.set_native_value(100)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
now = now + timedelta(minutes=1)
entity._set_now(now)
await send_temperature_change_event(entity, 10, now)
# we have to simulate the climate also else the test don't work
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.HEATING)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(100)
# Wait for state event propagation
await hass.async_block_till_done()
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.mock_opening_degree"]
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.mock_opening_degree",
]
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
# 2. stop a climate
open_degree_entity.set_native_value(0)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
await send_temperature_change_event(entity, 25, now)
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.IDLE)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(0)
# Wait for state event propagation
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.OFF
assert entity.device_actives == []
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat() entity.remove_thermostat()

View File

@@ -18,10 +18,10 @@ from .const import *
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
# @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why # this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why
@pytest.mark.skip # @pytest.mark.skip
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get): async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
"""Test the normal full start of a thermostat in thermostat_over_climate type""" """Test the normal full start of a thermostat in thermostat_over_climate type"""
@@ -138,13 +138,13 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
assert mock_service_call.call_count == 3 assert mock_service_call.call_count == 3
mock_service_call.assert_has_calls( mock_service_call.assert_has_calls(
[ [
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
call("climate","set_temperature",{ call("climate","set_temperature",{
"entity_id": "climate.mock_climate", "entity_id": "climate.mock_climate",
"temperature": 15, # temp-min "temperature": 15, # temp-min
}, },
), ),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
# we have no current_temperature yet # we have no current_temperature yet
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}), # call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
] ]