From 4267d5cac2470a845872a4852f4e12f245718b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20H=C3=B6gstr=C3=B6m?= Date: Tue, 17 May 2022 15:43:02 +0200 Subject: [PATCH 1/4] Add support for external temperature sensor --- README.md | 5 +- custom_components/climate_group/climate.py | 57 ++++++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4ea58c2..b3b095a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Home Assistant Climate Group Groups multiple climate devices to a single entity. Useful if you have for instance multiple radiator thermostats in a room and want to control them all together. +Supports reading room temperature from external sensor or defaults to displaying the average current temperature reported by the configured climate devices. +Note: The value from the external temperature sensor is not reported back to the climate devices. It is only used to display the current temperature in the Climate Group entity card. Inspired/copied from light_group component (https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/group/light.py) ## How to install: @@ -24,13 +26,14 @@ Put this inside configuration.yaml in config folder of hass.io climate: - platform: climate_group name: 'Climate Friendly Name' - temperature_unit: C # default to celsius, 'C' or 'F' + temperature_unit: C # Optional - default to celsius, 'C' or 'F' entities: - climate.clima1 - climate.clima2 - climate.clima3 - climate.heater - climate.termostate + external_sensor: sensor.temperaturesensor1 # Optional - defaults to not being used, enter entityID of the external sensor ``` (use the entities you want to have in your climate_group) diff --git a/custom_components/climate_group/climate.py b/custom_components/climate_group/climate.py index a523290..e620972 100755 --- a/custom_components/climate_group/climate.py +++ b/custom_components/climate_group/climate.py @@ -16,6 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components import climate +from homeassistant.components import sensor from homeassistant.components.climate import ClimateEntity, PLATFORM_SCHEMA from homeassistant.components.climate.const import * from homeassistant.const import ( @@ -27,6 +28,8 @@ CONF_ENTITIES, CONF_NAME, ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import State, callback from homeassistant.helpers.event import async_track_state_change @@ -34,14 +37,18 @@ _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Climate Group" +DEPENDENCIES = ['sensor'] +DEFAULT_NAME = "Climate Group" +CONF_EXT_SENSOR = "external_sensor" CONF_EXCLUDE = "exclude" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_CELSIUS): cv.string, + #vol.Optional(CONF_EXT_SENSOR, default=None): cv.entity_domain(sensor.DOMAIN), #cv.entity_id, + vol.Optional(CONF_EXT_SENSOR, default=''): cv.string, #cv.entity_id, vol.Required(CONF_ENTITIES): cv.entities_domain(climate.DOMAIN), vol.Optional(CONF_EXCLUDE, default=[]): vol.All( cv.ensure_list, @@ -90,6 +97,7 @@ async def async_setup_platform( config[CONF_ENTITIES], config.get(CONF_EXCLUDE), config.get(CONF_TEMPERATURE_UNIT), + config.get(CONF_EXT_SENSOR) ) ] ) @@ -99,11 +107,18 @@ class ClimateGroup(ClimateEntity): """Representation of a climate group.""" def __init__( - self, name: str, entity_ids: List[str], excluded: List[str], unit: str + #self, name: str, entity_ids: List[str], excluded: List[str], unit: str, sensor_entity_id: str + self, + name: str, + entity_ids: List[str], + excluded: List[str], + unit: str, + external_sensor: Optional[str] = None, ) -> None: """Initialize a climate group.""" self._name = name # type: str self._entity_ids = entity_ids # type: List[str] + self._sensor_entity_id = external_sensor if "c" in unit.lower(): self._unit = TEMP_CELSIUS else: @@ -128,6 +143,7 @@ def __init__( self._preset_modes = None self._preset = None self._excluded = excluded + async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -142,6 +158,17 @@ def async_state_changed_listener( self._async_unsub_state_changed = async_track_state_change( self.hass, self._entity_ids, async_state_changed_listener ) + + # Track changes of the temperature sensor - Only if external sensor is used + if self._sensor_entity_id: + # Add listener + async_track_state_change(self.hass, self._sensor_entity_id, + self._async_temp_sensor_changed) + sensor_state = self.hass.states.get(self._sensor_entity_id) + #if sensor_state and sensor_state.state != "STATE_UNKNOWN": + if sensor_state is not None and sensor_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN): + self._async_update_temp(sensor_state) + await self.async_update() async def async_will_remove_from_hass(self): @@ -292,6 +319,24 @@ async def async_set_hvac_mode(self, hvac_mode): climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, data, blocking=True ) + # Update climate group current temperature - Only used if external temperature sensor is configured + async def _async_temp_sensor_changed(self, entity_id, old_state, new_state): + """Handle temperature sensor changes. External sensor???""" + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return + + self._async_update_temp(new_state) + await self.async_update_ha_state() + + @callback + def _async_update_temp(self, state): + """Update thermostat with latest state from temperature sensor.""" + try: + if state.state != "STATE_UNKNOWN": + self._current_temp = float(state.state) + except ValueError as ex: + _LOGGER.error("Unable to update from temperature sensor: %s", ex) + async def async_update(self): """Query all members and determine the climate group state.""" raw_states = [self.hass.states.get(x) for x in self._entity_ids] @@ -370,9 +415,11 @@ async def async_update(self): ) # end add - self._current_temp = _reduce_attribute( - filtered_states, ATTR_CURRENT_TEMPERATURE - ) + # Only if external temperature sensor is NOT used + if not self._sensor_entity_id: + self._current_temp = _reduce_attribute( + filtered_states, ATTR_CURRENT_TEMPERATURE + ) _LOGGER.debug( f"Target temp: {self._target_temp}; Target temp low: {self._target_temp_low}; Target temp high: {self._target_temp_high}; Current temp: {self._current_temp}" From 97a9c41bb5614d884fbebc4bd782328f1ab09ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20H=C3=B6gstr=C3=B6m?= Date: Tue, 17 May 2022 16:30:24 +0200 Subject: [PATCH 2/4] Update sample configuration --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3b095a..9ff3b22 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ climate: - climate.clima3 - climate.heater - climate.termostate - external_sensor: sensor.temperaturesensor1 # Optional - defaults to not being used, enter entityID of the external sensor + external_sensor: sensor.temperaturesensor1 # Optional - defaults to not being used, enter entityID of the external sensor ``` (use the entities you want to have in your climate_group) From e714fe922a189c9b722ea9818e90574e2137f8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20H=C3=B6gstr=C3=B6m?= Date: Tue, 17 May 2022 17:16:18 +0200 Subject: [PATCH 3/4] Update formatting --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9ff3b22..c3d408e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Home Assistant Climate Group Groups multiple climate devices to a single entity. Useful if you have for instance multiple radiator thermostats in a room and want to control them all together. Supports reading room temperature from external sensor or defaults to displaying the average current temperature reported by the configured climate devices. + Note: The value from the external temperature sensor is not reported back to the climate devices. It is only used to display the current temperature in the Climate Group entity card. + Inspired/copied from light_group component (https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/group/light.py) ## How to install: From 28acf8da7df2a3b579166e53560ba64ccea11186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20H=C3=B6gstr=C3=B6m?= Date: Tue, 17 May 2022 20:32:10 +0200 Subject: [PATCH 4/4] cleanup --- custom_components/climate_group/climate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/climate_group/climate.py b/custom_components/climate_group/climate.py index e620972..5d031e7 100755 --- a/custom_components/climate_group/climate.py +++ b/custom_components/climate_group/climate.py @@ -47,8 +47,7 @@ { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_CELSIUS): cv.string, - #vol.Optional(CONF_EXT_SENSOR, default=None): cv.entity_domain(sensor.DOMAIN), #cv.entity_id, - vol.Optional(CONF_EXT_SENSOR, default=''): cv.string, #cv.entity_id, + vol.Optional(CONF_EXT_SENSOR, default=''): cv.string, # would rather use cv.entity_id or cv.entity_domain(sensor.DOMAIN) but HA configuration checker does not allow "entity" checks to be None, which breaks the "optional" part vol.Required(CONF_ENTITIES): cv.entities_domain(climate.DOMAIN), vol.Optional(CONF_EXCLUDE, default=[]): vol.All( cv.ensure_list, @@ -107,7 +106,6 @@ class ClimateGroup(ClimateEntity): """Representation of a climate group.""" def __init__( - #self, name: str, entity_ids: List[str], excluded: List[str], unit: str, sensor_entity_id: str self, name: str, entity_ids: List[str], @@ -165,7 +163,6 @@ def async_state_changed_listener( async_track_state_change(self.hass, self._sensor_entity_id, self._async_temp_sensor_changed) sensor_state = self.hass.states.get(self._sensor_entity_id) - #if sensor_state and sensor_state.state != "STATE_UNKNOWN": if sensor_state is not None and sensor_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN): self._async_update_temp(sensor_state)