Skip to content
This repository was archived by the owner on Oct 8, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
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:
Expand All @@ -24,13 +28,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)
54 changes: 49 additions & 5 deletions custom_components/climate_group/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -27,21 +28,26 @@
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
from homeassistant.helpers.typing import HomeAssistantType, ConfigType

_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=''): 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,
Expand Down Expand Up @@ -90,6 +96,7 @@ async def async_setup_platform(
config[CONF_ENTITIES],
config.get(CONF_EXCLUDE),
config.get(CONF_TEMPERATURE_UNIT),
config.get(CONF_EXT_SENSOR)
)
]
)
Expand All @@ -99,11 +106,17 @@ 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,
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:
Expand All @@ -128,6 +141,7 @@ def __init__(
self._preset_modes = None
self._preset = None
self._excluded = excluded


async def async_added_to_hass(self) -> None:
"""Register callbacks."""
Expand All @@ -142,6 +156,16 @@ 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 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):
Expand Down Expand Up @@ -292,6 +316,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]
Expand Down Expand Up @@ -370,9 +412,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}"
Expand Down