From 0343d0f0e826d35d24f7fe255069593a783d48cb Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 14 Jan 2024 00:33:52 +0000 Subject: [PATCH] Fonctional before testu. Miss the service call --- .devcontainer/configuration.yaml | 1 + .../versatile_thermostat/__init__.py | 8 ++ .../versatile_thermostat/base_thermostat.py | 17 +++- .../versatile_thermostat/binary_sensor.py | 97 ++++++++++++++++++- .../versatile_thermostat/config_flow.py | 5 +- .../versatile_thermostat/config_schema.py | 10 ++ .../versatile_thermostat/const.py | 1 + .../thermostat_climate.py | 4 + .../versatile_thermostat/thermostat_switch.py | 3 + .../versatile_thermostat/thermostat_valve.py | 10 +- .../versatile_thermostat/vtherm_api.py | 11 +++ 11 files changed, 162 insertions(+), 5 deletions(-) diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 0583d22..4978db3 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -168,6 +168,7 @@ recorder: - switch - climate - sensor + - binary_sensor template: - binary_sensor: diff --git a/custom_components/versatile_thermostat/__init__.py b/custom_components/versatile_thermostat/__init__.py index e76483e..b0f4d79 100644 --- a/custom_components/versatile_thermostat/__init__.py +++ b/custom_components/versatile_thermostat/__init__.py @@ -105,6 +105,9 @@ async def reload_all_vtherm(hass): ] await asyncio.gather(*reload_tasks) + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + if api: + await api.reload_central_boiler_entities_list() async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -133,6 +136,10 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: await reload_all_vtherm(hass) else: await hass.config_entries.async_reload(entry.entry_id) + # Reload the central boiler list of entities + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + if api is not None: + await api.reload_central_boiler_entities_list() async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -142,6 +149,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if api: api.remove_entry(entry) + await api.reload_central_boiler_entities_list() return unload_ok diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 753f986..247c539 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -283,6 +283,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._is_central_mode = None self._last_central_mode = None + self._is_used_by_central_boiler = False self.post_init(entry_infos) def clean_central_config_doublon(self, config_entry, central_config) -> dict: @@ -569,6 +570,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity): entry_infos.get(CONF_USE_CENTRAL_MODE) is False ) # Default value (None) is True + self._is_used_by_central_boiler = ( + entry_infos.get(CONF_USED_BY_CENTRAL_BOILER) is True + ) + _LOGGER.debug( "%s - Creation of a new VersatileThermostat entity: unique_id=%s", self, @@ -878,6 +883,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode}) self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) + self.send_event(EventType.HVAC_ACTION_EVENT, {"hvac_action": self.hvac_action}) _LOGGER.info( "%s - restored state is target_temp=%.1f, preset_mode=%s, hvac_mode=%s", @@ -1010,6 +1016,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity): action = HVACAction.HEATING return action + @property + def is_used_by_central_boiler(self) -> HVACAction | None: + """Return true is the VTherm is configured to be used by + central boiler""" + return self._is_used_by_central_boiler + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -1855,7 +1867,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity): ) if self.window_bypass_state or not self.is_window_auto_enabled: - _LOGGER.info("%s - Window auto event is ignored because bypass is ON or window auto detection is disabled", self) + _LOGGER.info( + "%s - Window auto event is ignored because bypass is ON or window auto detection is disabled", + self, + ) return if ( diff --git a/custom_components/versatile_thermostat/binary_sensor.py b/custom_components/versatile_thermostat/binary_sensor.py index 5b36892..1e58a28 100644 --- a/custom_components/versatile_thermostat/binary_sensor.py +++ b/custom_components/versatile_thermostat/binary_sensor.py @@ -3,11 +3,13 @@ import logging -from homeassistant.core import HomeAssistant, callback, Event +from homeassistant.core import HomeAssistant, callback, Event, CoreState -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -17,6 +19,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.components.climate import ( + ClimateEntity, + HVACMode, + HVACAction, + DOMAIN as CLIMATE_DOMAIN, +) + +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from .vtherm_api import VersatileThermostatAPI from .commons import VersatileThermostatBaseEntity from .const import ( DOMAIN, @@ -28,6 +39,7 @@ from .const import ( CONF_USE_WINDOW_FEATURE, CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_CENTRAL_CONFIG, + overrides, ) _LOGGER = logging.getLogger(__name__) @@ -332,6 +344,8 @@ class CentralBoilerBinarySensor(BinarySensorEntity): self._attr_unique_id = "central_boiler_state" self._attr_is_on = False self._device_name = entry_infos.get(CONF_NAME) + self._entities = [] + self._hass = hass @property def device_info(self) -> DeviceInfo: @@ -354,3 +368,82 @@ class CentralBoilerBinarySensor(BinarySensorEntity): return "mdi:water-boiler" else: return "mdi:water-boiler-off" + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + @callback + async def _async_startup_internal(*_): + _LOGGER.debug("%s - Calling async_startup_internal", self) + api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api( + self._hass + ) + api.register_central_boiler(self) + await self.listen_vtherms_entities() + + if self.hass.state == CoreState.running: + await _async_startup_internal() + else: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_startup_internal + ) + + async def listen_vtherms_entities(self): + """Initialize the listening of state change of VTherms""" + + # Listen to all VTherm state change + self._entities = [] + entities_id = [] + + component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] + for entity in component.entities: + if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler: + self._entities.append(entity) + entities_id.append(entity.entity_id) + if len(self._entities) > 0: + # Arme l'écoute de la première entité + listener_cancel = async_track_state_change_event( + self._hass, + entities_id, + self.calculate_central_boiler_state, + ) + _LOGGER.info( + "%s - VTherm that could controls the central boiler are %s", + self, + entities_id, + ) + self.async_on_remove(listener_cancel) + else: + _LOGGER.debug("%s - no VTherm could controls the central boiler", self) + + await self.calculate_central_boiler_state(None) + + async def calculate_central_boiler_state(self, _): + """Calculate the central boiler state depending on all VTherm that + controls this central boiler""" + + _LOGGER.debug("%s - calculating the new central boiler state", self) + active = False + for entity in self._entities: + _LOGGER.debug( + "Examining the hvac_action of %s", + entity.name, + ) + if ( + entity.hvac_mode == HVACMode.HEAT + and entity.hvac_action == HVACAction.HEATING + ): + active = True + break + + if self._attr_is_on != active: + if active: + _LOGGER.info("%s - turning on the central boiler", self) + else: + _LOGGER.info("%s - turning off the central boiler", self) + self._attr_is_on = active + self.async_write_ha_state() + + def __str__(self): + return f"VersatileThermostat-{self.name}" diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 1a2b6bc..1a7cdb1 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -281,7 +281,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): """Handle the specific main flow steps""" _LOGGER.debug("Into ConfigFlow.async_step_spec_main user_input=%s", user_input) - schema = STEP_CENTRAL_MAIN_DATA_SCHEMA + if self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CENTRAL_CONFIG: + schema = STEP_CENTRAL_MAIN_DATA_SCHEMA + else: + schema = STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA next_step = self.async_step_type self._infos[COMES_FROM] = "async_step_spec_main" diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py index 09d352f..f53b7d0 100644 --- a/custom_components/versatile_thermostat/config_schema.py +++ b/custom_components/versatile_thermostat/config_schema.py @@ -63,6 +63,16 @@ STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name } ) +STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig(domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]), + ), + vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float), + vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float), + } +) + STEP_CENTRAL_BOILER_SCHEMA = vol.Schema( { vol.Optional(CONF_CENTRAL_BOILER_ACTIVATION_SRV, default=""): str, diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 106b62b..66c99b0 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -403,6 +403,7 @@ class EventType(Enum): POWER_EVENT: str = "versatile_thermostat_power_event" TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event" HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event" + HVAC_ACTION_EVENT: str = "versatile_thermostat_hvac_action_event" PRESET_EVENT: str = "versatile_thermostat_preset_event" WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event" diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index b0f09bb..69fd04d 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -48,6 +48,7 @@ from .const import ( AUTO_FAN_DTEMP_THRESHOLD, AUTO_FAN_DEACTIVATED_MODES, UnknownEntity, + EventType, ) from .vtherm_api import VersatileThermostatAPI @@ -551,6 +552,9 @@ class ThermostatOverClimate(BaseThermostat): async def end_climate_changed(changes): """To end the event management""" if changes: + self.send_event( + EventType.HVAC_ACTION_EVENT, {"hvac_action": self.hvac_action} + ) self.async_write_ha_state() self.update_custom_attributes() await self.async_control_heating() diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index ff50a41..cd48d4d 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -13,6 +13,7 @@ from .const import ( CONF_HEATER_4, CONF_INVERSE_SWITCH, overrides, + EventType, ) from .base_thermostat import BaseThermostat @@ -208,5 +209,7 @@ class ThermostatOverSwitch(BaseThermostat): return if old_state is None: self.hass.create_task(self._check_initial_state()) + + self.send_event(EventType.HVAC_ACTION_EVENT, {"hvac_action": self.hvac_action}) self.async_write_ha_state() self.update_custom_attributes() diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py index f246a55..33513bd 100644 --- a/custom_components/versatile_thermostat/thermostat_valve.py +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -13,7 +13,14 @@ from homeassistant.components.climate import HVACMode from .base_thermostat import BaseThermostat from .prop_algorithm import PropAlgorithm -from .const import CONF_VALVE, CONF_VALVE_2, CONF_VALVE_3, CONF_VALVE_4, overrides +from .const import ( + CONF_VALVE, + CONF_VALVE_2, + CONF_VALVE_3, + CONF_VALVE_4, + overrides, + EventType, +) from .underlyings import UnderlyingValve @@ -123,6 +130,7 @@ class ThermostatOverValve(BaseThermostat): _LOGGER.debug( "%s - _async_valve_changed new_state is %s", self, new_state.state ) + self.send_event(EventType.HVAC_ACTION_EVENT, {"hvac_action": self.hvac_action}) @overrides def update_custom_attributes(self): diff --git a/custom_components/versatile_thermostat/vtherm_api.py b/custom_components/versatile_thermostat/vtherm_api.py index 964e790..76e5203 100644 --- a/custom_components/versatile_thermostat/vtherm_api.py +++ b/custom_components/versatile_thermostat/vtherm_api.py @@ -46,6 +46,7 @@ class VersatileThermostatAPI(dict): super().__init__() self._expert_params = None self._short_ema_params = None + self._central_boiler_entity = None def find_central_configuration(self): """Search for a central configuration""" @@ -87,6 +88,16 @@ class VersatileThermostatAPI(dict): if self._short_ema_params: _LOGGER.debug("We have found short ema params %s", self._short_ema_params) + def register_central_boiler(self, central_boiler_entity): + """Register the central boiler entity. This is used by the CentralBoilerBinarySensor + class to register itself at creation""" + self._central_boiler_entity = central_boiler_entity + + async def reload_central_boiler_entities_list(self): + """Reload the central boiler list of entities if a central boiler is used""" + if self._central_boiler_entity is not None: + await self._central_boiler_entity.listen_vtherms_entities() + @property def self_regulation_expert(self): """Get the self regulation params"""