Skip to content

Commit

Permalink
Issue #113 - Add multi thermostat over climate
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Collin committed Oct 7, 2023
1 parent 79eb4a0 commit 72c4105
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 38 deletions.
28 changes: 20 additions & 8 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
70 changes: 49 additions & 21 deletions custom_components/versatile_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
# CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_AC_MODE,
UnknownEntity,
EventType,
Expand Down Expand Up @@ -328,16 +331,19 @@ def post_init(self, entry_infos):
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):
Expand All @@ -348,7 +354,6 @@ def post_init(self, entry_infos):
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(
Expand Down Expand Up @@ -1582,6 +1587,7 @@ async def _async_climate_changed(self, event):
if not new_state:
return

changes = False
new_hvac_mode = new_state.state

old_state = event.data.get("old_state")
Expand All @@ -1597,9 +1603,9 @@ async def _async_climate_changed(self, event):
)

# 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",
Expand All @@ -1619,8 +1625,13 @@ async def _async_climate_changed(self, event):
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
Expand All @@ -1638,6 +1649,7 @@ async def _async_climate_changed(self, event):
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)
Expand All @@ -1658,14 +1670,20 @@ async def _async_climate_changed(self, event):
stop_power_date.isoformat(),
self._underlying_climate_delta_t,
)

# 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)

self.update_custom_attributes()
await self._async_control_heating()
changes = True

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

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):
Expand Down Expand Up @@ -2364,9 +2382,19 @@ def update_custom_attributes(self):
"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
Expand Down
12 changes: 12 additions & 0 deletions custom_components/versatile_thermostat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
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,
Expand Down Expand Up @@ -231,6 +234,15 @@ def __init__(self, infos) -> None:
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,
}
)
Expand Down
9 changes: 9 additions & 0 deletions custom_components/versatile_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
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"
Expand Down Expand Up @@ -130,6 +133,9 @@
[
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
Expand Down Expand Up @@ -159,6 +165,9 @@
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,
Expand Down
20 changes: 16 additions & 4 deletions custom_components/versatile_thermostat/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
},
Expand Down Expand Up @@ -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": {
Expand All @@ -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"
}
},
Expand Down
20 changes: 16 additions & 4 deletions custom_components/versatile_thermostat/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
},
Expand Down Expand Up @@ -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": {
Expand All @@ -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"
}
},
Expand Down
12 changes: 12 additions & 0 deletions custom_components/versatile_thermostat/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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)"
}
},
Expand Down Expand Up @@ -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": {
Expand All @@ -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)"
}
},
Expand Down
Loading

0 comments on commit 72c4105

Please sign in to comment.