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