From 98e8e893649b020748fbdb97d1fad57205e1e1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= <mail@dahoiv.net> Date: Wed, 18 Aug 2021 21:30:37 +0200 Subject: [PATCH] Mill data coordinator (#53603) Co-authored-by: Paulus Schoutsen <balloob@gmail.com> --- homeassistant/components/mill/__init__.py | 35 +++- homeassistant/components/mill/climate.py | 167 +++++++++----------- homeassistant/components/mill/manifest.json | 2 +- homeassistant/components/mill/sensor.py | 38 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 128 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 75422cd26e1..73cb65daf05 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,15 +1,43 @@ """The mill component.""" +from datetime import timedelta +import logging + from mill import Mill from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + PLATFORMS = ["climate", "sensor"] +class MillDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Mill data.""" + + def __init__( + self, + hass: HomeAssistant, + *, + mill_data_connection: Mill, + ) -> None: + """Initialize global Mill data updater.""" + self.mill_data_connection = mill_data_connection + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_method=mill_data_connection.fetch_heater_data, + update_interval=timedelta(seconds=30), + ) + + async def async_setup_entry(hass, entry): """Set up the Mill heater.""" mill_data_connection = Mill( @@ -20,9 +48,12 @@ async def async_setup_entry(hass, entry): if not await mill_data_connection.connect(): raise ConfigEntryNotReady - await mill_data_connection.find_all_heaters() + hass.data[DOMAIN] = MillDataUpdateCoordinator( + hass, + mill_data_connection=mill_data_connection, + ) - hass.data[DOMAIN] = mill_data_connection + await hass.data[DOMAIN].async_config_entry_first_refresh() hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 16c78329b0b..199bdf393a1 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -11,8 +11,10 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_AWAY_TEMP, @@ -41,11 +43,11 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill climate.""" - mill_data_connection = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN] dev = [] - for heater in mill_data_connection.heaters.values(): - dev.append(MillHeater(heater, mill_data_connection)) + for heater in mill_data_coordinator.data.values(): + dev.append(MillHeater(mill_data_coordinator, heater)) async_add_entities(dev) async def set_room_temp(service): @@ -54,7 +56,7 @@ async def async_setup_entry(hass, entry, async_add_entities): sleep_temp = service.data.get(ATTR_SLEEP_TEMP) comfort_temp = service.data.get(ATTR_COMFORT_TEMP) away_temp = service.data.get(ATTR_AWAY_TEMP) - await mill_data_connection.set_room_temperatures_by_name( + await mill_data_coordinator.mill_data_connection.set_room_temperatures_by_name( room_name, sleep_temp, comfort_temp, away_temp ) @@ -63,122 +65,97 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class MillHeater(ClimateEntity): +class MillHeater(CoordinatorEntity, ClimateEntity): """Representation of a Mill Thermostat device.""" _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_supported_features = SUPPORT_FLAGS - _attr_target_temperature_step = 1 + _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, heater, mill_data_connection): + def __init__(self, coordinator, heater): """Initialize the thermostat.""" - self._heater = heater - self._conn = mill_data_connection + super().__init__(coordinator) + + self._id = heater.device_id self._attr_unique_id = heater.device_id self._attr_name = heater.name - - @property - def available(self): - """Return True if entity is available.""" - return self._heater.available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - res = { - "open_window": self._heater.open_window, - "heating": self._heater.is_heating, - "controlled_by_tibber": self._heater.tibber_control, - "heater_generation": 1 if self._heater.is_gen1 else 2, + self._attr_device_info = { + "identifiers": {(DOMAIN, heater.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": f"generation {1 if heater.is_gen1 else 2}", } - if self._heater.room: - res["room"] = self._heater.room.name - res["avg_room_temp"] = self._heater.room.avg_temp + if heater.is_gen1: + self._attr_hvac_modes = [HVAC_MODE_HEAT] else: - res["room"] = "Independent device" - return res - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._heater.set_temp - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._heater.current_temp - - @property - def fan_mode(self): - """Return the fan setting.""" - return FAN_ON if self._heater.fan_status == 1 else HVAC_MODE_OFF - - @property - def hvac_action(self): - """Return current hvac i.e. heat, cool, idle.""" - if self._heater.is_gen1 or self._heater.is_heating == 1: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._heater.is_gen1 or self._heater.power_status == 1: - return HVAC_MODE_HEAT - return HVAC_MODE_OFF - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - if self._heater.is_gen1: - return [HVAC_MODE_HEAT] - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._update_attr(heater) async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - await self._conn.set_heater_temp(self._heater.device_id, int(temperature)) + await self.coordinator.mill_data_connection.set_heater_temp( + self._id, int(temperature) + ) + await self.coordinator.async_request_refresh() async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" fan_status = 1 if fan_mode == FAN_ON else 0 - await self._conn.heater_control(self._heater.device_id, fan_status=fan_status) + await self.coordinator.mill_data_connection.heater_control( + self._id, fan_status=fan_status + ) + await self.coordinator.async_request_refresh() async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + heater = self.coordinator.data[self._id] + if hvac_mode == HVAC_MODE_HEAT: - await self._conn.heater_control(self._heater.device_id, power_status=1) - elif hvac_mode == HVAC_MODE_OFF and not self._heater.is_gen1: - await self._conn.heater_control(self._heater.device_id, power_status=0) - - async def async_update(self): - """Retrieve latest state.""" - self._heater = await self._conn.update_device(self._heater.device_id) - - @property - def device_id(self): - """Return the ID of the physical device this sensor is part of.""" - return self._heater.device_id - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": f"generation {1 if self._heater.is_gen1 else 2}", + await self.coordinator.mill_data_connection.heater_control( + self._id, power_status=1 + ) + await self.coordinator.async_request_refresh() + elif hvac_mode == HVAC_MODE_OFF and not heater.is_gen1: + await self.coordinator.mill_data_connection.heater_control( + self._id, power_status=0 + ) + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr(self.coordinator.data[self._id]) + self.async_write_ha_state() + + @callback + def _update_attr(self, heater): + self._attr_available = heater.available + self._attr_extra_state_attributes = { + "open_window": heater.open_window, + "heating": heater.is_heating, + "controlled_by_tibber": heater.tibber_control, + "heater_generation": 1 if heater.is_gen1 else 2, } - return device_info + if heater.room: + self._attr_extra_state_attributes["room"] = heater.room.name + self._attr_extra_state_attributes["avg_room_temp"] = heater.room.avg_temp + else: + self._attr_extra_state_attributes["room"] = "Independent device" + self._attr_target_temperature = heater.set_temp + self._attr_current_temperature = heater.current_temp + self._attr_fan_mode = FAN_ON if heater.fan_status == 1 else HVAC_MODE_OFF + if heater.is_gen1 or heater.is_heating == 1: + self._attr_hvac_action = CURRENT_HVAC_HEAT + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE + if heater.is_gen1 or heater.power_status == 1: + self._attr_hvac_mode = HVAC_MODE_HEAT + else: + self._attr_hvac_mode = HVAC_MODE_OFF diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 161bbe274ef..33a7c35c169 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.5.0"], + "requirements": ["millheater==0.5.2"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index ce7704ad1be..11b006e4b6e 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -6,6 +6,8 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -13,27 +15,28 @@ from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill sensor.""" - mill_data_connection = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN] entities = [ - MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) + MillHeaterEnergySensor(mill_data_coordinator, sensor_type, heater) for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR) - for heater in mill_data_connection.heaters.values() + for heater in mill_data_coordinator.data.values() ] async_add_entities(entities) -class MillHeaterEnergySensor(SensorEntity): +class MillHeaterEnergySensor(CoordinatorEntity, SensorEntity): """Representation of a Mill Sensor device.""" _attr_device_class = DEVICE_CLASS_ENERGY _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_state_class = STATE_CLASS_TOTAL_INCREASING - def __init__(self, heater, mill_data_connection, sensor_type): + def __init__(self, coordinator, sensor_type, heater): """Initialize the sensor.""" + super().__init__(coordinator) + self._id = heater.device_id - self._conn = mill_data_connection self._sensor_type = sensor_type self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" @@ -44,20 +47,19 @@ class MillHeaterEnergySensor(SensorEntity): "manufacturer": MANUFACTURER, "model": f"generation {1 if heater.is_gen1 else 2}", } + self._update_attr(heater) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr(self.coordinator.data[self._id]) + self.async_write_ha_state() - async def async_update(self): - """Retrieve latest state.""" - heater = await self._conn.update_device(self._id) + @callback + def _update_attr(self, heater): self._attr_available = heater.available if self._sensor_type == CONSUMPTION_TODAY: - _state = heater.day_consumption + self._attr_native_value = heater.day_consumption elif self._sensor_type == CONSUMPTION_YEAR: - _state = heater.year_consumption - else: - _state = None - if _state is None: - self._attr_native_value = _state - return - - self._attr_native_value = _state + self._attr_native_value = heater.year_consumption diff --git a/requirements_all.txt b/requirements_all.txt index 69da5abc72d..9ec1ccce7c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.5.0 +millheater==0.5.2 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e476881426c..710f4e9ae2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -551,7 +551,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.5.0 +millheater==0.5.2 # homeassistant.components.minio minio==4.0.9 -- GitLab