From 72c4105bbd0bc771c20d880a42e0428e307d48be Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 8 Oct 2023 01:23:25 +0200 Subject: [PATCH] Issue #113 - Add multi thermostat over climate --- .devcontainer/configuration.yaml | 28 +++++--- .../versatile_thermostat/climate.py | 66 +++++++++++++------ .../versatile_thermostat/config_flow.py | 12 ++++ .../versatile_thermostat/const.py | 9 +++ .../versatile_thermostat/strings.json | 20 ++++-- .../versatile_thermostat/translations/en.json | 20 ++++-- .../versatile_thermostat/translations/fr.json | 12 ++++ .../versatile_thermostat/translations/it.json | 14 +++- 8 files changed, 145 insertions(+), 36 deletions(-) diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 1fb96c8..7233c66 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -72,6 +72,18 @@ input_boolean: fake_heater_4switch4: name: Heater (multiswitch4) icon: mdi:radiator + fake_heater_4climate1: + name: Heater (multiclimate1) + icon: mdi:radiator + fake_heater_4climate2: + name: Heater (multiclimate2) + icon: mdi:radiator + fake_heater_4climate3: + name: Heater (multiclimate3) + icon: mdi:radiator + fake_heater_4climate4: + name: Heater (multiclimate4) + icon: mdi:radiator # input_boolean to simulate the motion sensor entity. Only for development environment. fake_motion_sensor1: name: Motion Sensor 1 @@ -99,20 +111,20 @@ climate: heater: input_boolean.fake_heater_switch3 target_sensor: input_number.fake_temperature_sensor1 - platform: generic_thermostat - name: Underlying thermostat5 - heater: input_boolean.fake_heater_switch3 + name: Underlying thermostat 4-1 + heater: input_boolean.fake_heater_4climate1 target_sensor: input_number.fake_temperature_sensor1 - platform: generic_thermostat - name: Underlying thermostat6 - heater: input_boolean.fake_heater_switch3 + name: Underlying thermostat 4-2 + heater: input_boolean.fake_heater_4climate2 target_sensor: input_number.fake_temperature_sensor1 - platform: generic_thermostat - name: Underlying thermostat7 - heater: input_boolean.fake_heater_switch3 + name: Underlying thermostat 4-3 + heater: input_boolean.fake_heater_4climate3 target_sensor: input_number.fake_temperature_sensor1 - platform: generic_thermostat - name: Underlying thermostat8 - heater: input_boolean.fake_heater_switch3 + name: Underlying thermostat 4-4 + heater: input_boolean.fake_heater_4climate4 target_sensor: input_number.fake_temperature_sensor1 - platform: generic_thermostat name: Underlying thermostat9 diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 4d11da7..36dde30 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -129,6 +129,9 @@ from .const import ( # CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_CLIMATE, + CONF_CLIMATE_2, + CONF_CLIMATE_3, + CONF_CLIMATE_4, CONF_AC_MODE, UnknownEntity, EventType, @@ -328,16 +331,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self._cycle_min = entry_infos.get(CONF_CYCLE_MIN) # Initialize underlying entities + self._underlyings = [] self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE) if self._thermostat_type == CONF_THERMOSTAT_CLIMATE: self._is_over_climate = True - self._underlyings.append( - UnderlyingClimate( - hass=self._hass, - thermostat=self, - climate_entity_id=entry_infos.get(CONF_CLIMATE), - ) - ) + for climate in [CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4]: + if entry_infos.get(climate): + self._underlyings.append( + UnderlyingClimate( + hass=self._hass, + thermostat=self, + climate_entity_id=entry_infos.get(climate), + ) + ) else: lst_switches = [entry_infos.get(CONF_HEATER)] if entry_infos.get(CONF_HEATER_2): @@ -348,7 +354,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): lst_switches.append(entry_infos.get(CONF_HEATER_4)) delta_cycle = self._cycle_min * 60 / len(lst_switches) - self._underlyings = [] for idx, switch in enumerate(lst_switches): self._underlyings.append( UnderlyingSwitch( @@ -1582,6 +1587,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): if not new_state: return + changes = False new_hvac_mode = new_state.state old_state = event.data.get("old_state") @@ -1597,9 +1603,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): ) # Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command - if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE: - _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF") - new_hvac_mode = HVACMode.OFF + #if self._hvac_mode == HVACMode.OFF and new_hvac_mode == HVACMode.COOL and new_hvac_action == HVACAction.IDLE: + # _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF") + # new_hvac_mode = HVACMode.OFF _LOGGER.info( "%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s", @@ -1619,8 +1625,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): HVACMode.AUTO, HVACMode.FAN_ONLY, None - ]: + ] and self._hvac_mode != new_hvac_mode: + changes = True self._hvac_mode = new_hvac_mode + # Do not try to update all underlying state, else we will have a loop + if self._is_over_climate: + for under in self._underlyings: + await under.set_hvac_mode(new_hvac_mode) # Interpretation of hvac HVAC_ACTION_ON = [ # pylint: disable=invalid-name @@ -1638,6 +1649,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self, self._underlying_climate_start_hvac_action_date.isoformat(), ) + changes = True if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON: stop_power_date = self.get_last_updated_date_or_now(new_state) @@ -1658,14 +1670,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): stop_power_date.isoformat(), self._underlying_climate_delta_t, ) + changes = True - # Manage new target temperature set - if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature: - _LOGGER.info("%s - Target temp have change to %s", self, new_target_temp) - await self.async_set_temperature(temperature = new_target_temp) + if not changes: + # try to manage new target temperature set if state + _LOGGER.debug("Do temperature check. temperature is %s, new_state.attributes is %s", self.target_temperature, new_state.attributes) + if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature: + _LOGGER.info("%s - Target temp have change to %s", self, new_target_temp) + await self.async_set_temperature(temperature = new_target_temp) + changes = True - self.update_custom_attributes() - await self._async_control_heating() + if changes: + self.async_write_ha_state() + self.update_custom_attributes() + await self._async_control_heating() @callback async def _async_update_temp(self, state: State): @@ -2364,9 +2382,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): "window_auto_max_duration": self._window_auto_max_duration, } if self._is_over_climate: - self._attr_extra_state_attributes["underlying_climate"] = self._underlyings[ + self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[ 0 ].entity_id + self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[ + 1 + ].entity_id if len(self._underlyings) > 1 else None + self._attr_extra_state_attributes["underlying_climate_2"] = self._underlyings[ + 2 + ].entity_id if len(self._underlyings) > 2 else None + self._attr_extra_state_attributes["underlying_climate_3"] = self._underlyings[ + 3 + ].entity_id if len(self._underlyings) > 3 else None + self._attr_extra_state_attributes[ "start_hvac_action_date" ] = self._underlying_climate_start_hvac_action_date diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 34b678f..845bb56 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -81,6 +81,9 @@ from .const import ( CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_SWITCH, CONF_CLIMATE, + CONF_CLIMATE_2, + CONF_CLIMATE_3, + CONF_CLIMATE_4, CONF_USE_WINDOW_FEATURE, CONF_USE_MOTION_FEATURE, CONF_USE_PRESENCE_FEATURE, @@ -231,6 +234,15 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): vol.Required(CONF_CLIMATE): selector.EntitySelector( selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), ), + vol.Optional(CONF_CLIMATE_2): selector.EntitySelector( + selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), + ), + vol.Optional(CONF_CLIMATE_3): selector.EntitySelector( + selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), + ), + vol.Optional(CONF_CLIMATE_4): selector.EntitySelector( + selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), + ), vol.Optional(CONF_AC_MODE, default=False): cv.boolean, } ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 27b4697..1b7ddf9 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -65,6 +65,9 @@ CONF_THERMOSTAT_TYPE = "thermostat_type" CONF_THERMOSTAT_SWITCH = "thermostat_over_switch" CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate" CONF_CLIMATE = "climate_entity_id" +CONF_CLIMATE_2 = "climate_entity2_id" +CONF_CLIMATE_3 = "climate_entity3_id" +CONF_CLIMATE_4 = "climate_entity4_id" CONF_USE_WINDOW_FEATURE = "use_window_feature" CONF_USE_MOTION_FEATURE = "use_motion_feature" CONF_USE_PRESENCE_FEATURE = "use_presence_feature" @@ -130,6 +133,9 @@ ALL_CONF = ( [ CONF_NAME, CONF_HEATER, + CONF_HEATER_2, + CONF_HEATER_3, + CONF_HEATER_4, CONF_TEMP_SENSOR, CONF_EXTERNAL_TEMP_SENSOR, CONF_POWER_SENSOR, @@ -159,6 +165,9 @@ ALL_CONF = ( CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_CLIMATE, + CONF_CLIMATE_2, + CONF_CLIMATE_3, + CONF_CLIMATE_4, CONF_USE_WINDOW_FEATURE, CONF_USE_MOTION_FEATURE, CONF_USE_PRESENCE_FEATURE, diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index 4a2a93a..a59d018 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -26,11 +26,14 @@ "description": "Linked entities attributes", "data": { "heater_entity_id": "Heater switch", - "heater_entity2_id": "2nd Heater switch", - "heater_entity3_id": "3rd Heater switch", - "heater_entity4_id": "4th Heater switch", + "heater_entity2_id": "2nd heater switch", + "heater_entity3_id": "3rd heater switch", + "heater_entity4_id": "4th heater switch", "proportional_function": "Algorithm", - "climate_entity_id": "Underlying thermostat", + "climate_entity_id": "Underlying climate", + "climate_entity2_id": "2nd underlying climate", + "climate_entity3_id": "3rd underlying climate", + "climate_entity4_id": "4th underlying climate", "ac_mode": "AC mode" }, "data_description": { @@ -40,6 +43,9 @@ "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "proportional_function": "Algorithm to use (TPI is the only one for now)", "climate_entity_id": "Underlying climate entity id", + "climate_entity2_id": "2nd underlying climate entity id", + "climate_entity3_id": "3rd underlying climate entity id", + "climate_entity4_id": "4th underlying climate entity id", "ac_mode": "Use the Air Conditioning (AC) mode" } }, @@ -170,6 +176,9 @@ "heater_entity4_id": "4th Heater switch", "proportional_function": "Algorithm", "climate_entity_id": "Underlying thermostat", + "climate_entity2_id": "2nd underlying climate", + "climate_entity3_id": "3rd underlying climate", + "climate_entity4_id": "4th underlying climate", "ac_mode": "AC mode" }, "data_description": { @@ -179,6 +188,9 @@ "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "proportional_function": "Algorithm to use (TPI is the only one for now)", "climate_entity_id": "Underlying climate entity id", + "climate_entity2_id": "2nd underlying climate entity id", + "climate_entity3_id": "3rd underlying climate entity id", + "climate_entity4_id": "4th underlying climate entity id", "ac_mode": "Use the Air Conditioning (AC) mode" } }, diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 4a2a93a..a59d018 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -26,11 +26,14 @@ "description": "Linked entities attributes", "data": { "heater_entity_id": "Heater switch", - "heater_entity2_id": "2nd Heater switch", - "heater_entity3_id": "3rd Heater switch", - "heater_entity4_id": "4th Heater switch", + "heater_entity2_id": "2nd heater switch", + "heater_entity3_id": "3rd heater switch", + "heater_entity4_id": "4th heater switch", "proportional_function": "Algorithm", - "climate_entity_id": "Underlying thermostat", + "climate_entity_id": "Underlying climate", + "climate_entity2_id": "2nd underlying climate", + "climate_entity3_id": "3rd underlying climate", + "climate_entity4_id": "4th underlying climate", "ac_mode": "AC mode" }, "data_description": { @@ -40,6 +43,9 @@ "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "proportional_function": "Algorithm to use (TPI is the only one for now)", "climate_entity_id": "Underlying climate entity id", + "climate_entity2_id": "2nd underlying climate entity id", + "climate_entity3_id": "3rd underlying climate entity id", + "climate_entity4_id": "4th underlying climate entity id", "ac_mode": "Use the Air Conditioning (AC) mode" } }, @@ -170,6 +176,9 @@ "heater_entity4_id": "4th Heater switch", "proportional_function": "Algorithm", "climate_entity_id": "Underlying thermostat", + "climate_entity2_id": "2nd underlying climate", + "climate_entity3_id": "3rd underlying climate", + "climate_entity4_id": "4th underlying climate", "ac_mode": "AC mode" }, "data_description": { @@ -179,6 +188,9 @@ "heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used", "proportional_function": "Algorithm to use (TPI is the only one for now)", "climate_entity_id": "Underlying climate entity id", + "climate_entity2_id": "2nd underlying climate entity id", + "climate_entity3_id": "3rd underlying climate entity id", + "climate_entity4_id": "4th underlying climate entity id", "ac_mode": "Use the Air Conditioning (AC) mode" } }, diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index 6c98727..6c890a6 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -30,6 +30,9 @@ "heater_entity4_id": "4ème radiateur", "proportional_function": "Algorithme", "climate_entity_id": "Thermostat sous-jacent", + "climate_entity2_id": "2ème thermostat sous-jacent", + "climate_entity3_id": "3ème thermostat sous-jacent", + "climate_entity4_id": "4ème thermostat sous-jacent", "ac_mode": "AC mode ?" }, "data_description": { @@ -39,6 +42,9 @@ "heater_entity4_id": "Optionnel entity id du 4ème radiateur", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "climate_entity_id": "Entity id du thermostat sous-jacent", + "climate_entity2_id": "Entity id du 2ème thermostat sous-jacent", + "climate_entity3_id": "Entity id du 3ème thermostat sous-jacent", + "climate_entity4_id": "Entity id du 4ème thermostat sous-jacent", "ac_mode": "Utilisation du mode Air Conditionné (AC)" } }, @@ -170,6 +176,9 @@ "heater_entity4_id": "4ème radiateur", "proportional_function": "Algorithme", "climate_entity_id": "Thermostat sous-jacent", + "climate_entity2_id": "2ème thermostat sous-jacent", + "climate_entity3_id": "3ème thermostat sous-jacent", + "climate_entity4_id": "4ème thermostat sous-jacent", "ac_mode": "AC mode ?" }, "data_description": { @@ -179,6 +188,9 @@ "heater_entity4_id": "Optionnel entity id du 4ème radiateur", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "climate_entity_id": "Entity id du thermostat sous-jacent", + "climate_entity2_id": "Entity id du 2ème thermostat sous-jacent", + "climate_entity3_id": "Entity id du 3ème thermostat sous-jacent", + "climate_entity4_id": "Entity id du 4ème thermostat sous-jacent", "ac_mode": "Utilisation du mode Air Conditionné (AC)" } }, diff --git a/custom_components/versatile_thermostat/translations/it.json b/custom_components/versatile_thermostat/translations/it.json index 09bd69e..9fb4091 100644 --- a/custom_components/versatile_thermostat/translations/it.json +++ b/custom_components/versatile_thermostat/translations/it.json @@ -31,6 +31,9 @@ "heater_entity4_id": "Quarto riscaldatore", "proportional_function": "Algoritmo", "climate_entity_id": "Termostato sottostante", + "climate_entity2_id": "Secundo termostato sottostante", + "climate_entity3_id": "Terzo termostato sottostante", + "climate_entity4_id": "Quarto termostato sottostante", "ac_mode": "AC mode ?" }, "data_description": { @@ -40,6 +43,9 @@ "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "climate_entity_id": "Entity id del termostato sottostante", + "climate_entity2_id": "Entity id del secundo termostato sottostante", + "climate_entity3_id": "Entity id del terzo termostato sottostante", + "climate_entity4_id": "Entity id del quarto termostato sottostante", "ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?" } }, @@ -170,6 +176,9 @@ "heater_entity4_id": "Quarto interruttore riscaldatore", "proportional_function": "Algoritmo", "climate_entity_id": "Termostato sottostante", + "climate_entity2_id": "Secundo termostato sottostante", + "climate_entity3_id": "Terzo termostato sottostante", + "climate_entity4_id": "Quarto termostato sottostante", "ac_mode": "AC mode ?" }, "data_description": { @@ -179,6 +188,9 @@ "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "climate_entity_id": "Entity id del termostato sottostante", + "climate_entity2_id": "Entity id del secundo termostato sottostante", + "climate_entity3_id": "Entity id del terzo termostato sottostante", + "climate_entity4_id": "Entity id del quarto termostato sottostante", "ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?" } }, @@ -301,4 +313,4 @@ } } } -} +} \ No newline at end of file