From 8661cf463a81e6e30a78ec195c329e1a5399373e Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Fri, 1 May 2020 11:29:58 -0600
Subject: [PATCH] Update AirVisual to use DataUpdateCoordinator (#34796)

* Update AirVisual to use DataUpdateCoordinator

* Empty commit to re-trigger build

* Don't include history or trends in config flow

* Code review
---
 .../components/airvisual/__init__.py          | 171 +++++++-----------
 .../components/airvisual/air_quality.py       |  51 +++---
 .../components/airvisual/config_flow.py       |   5 +-
 homeassistant/components/airvisual/const.py   |   4 +-
 homeassistant/components/airvisual/sensor.py  | 129 +++++++------
 5 files changed, 159 insertions(+), 201 deletions(-)

diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index 4079f739824..099fdfc5df7 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -19,30 +19,23 @@ from homeassistant.const import (
 )
 from homeassistant.core import callback
 from homeassistant.helpers import aiohttp_client, config_validation as cv
-from homeassistant.helpers.dispatcher import (
-    async_dispatcher_connect,
-    async_dispatcher_send,
-)
 from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
 from .const import (
     CONF_CITY,
     CONF_COUNTRY,
     CONF_GEOGRAPHIES,
     CONF_INTEGRATION_TYPE,
-    DATA_CLIENT,
+    DATA_COORDINATOR,
     DOMAIN,
     INTEGRATION_TYPE_GEOGRAPHY,
     INTEGRATION_TYPE_NODE_PRO,
     LOGGER,
-    TOPIC_UPDATE,
 )
 
 PLATFORMS = ["air_quality", "sensor"]
 
-DATA_LISTENER = "listener"
-
 DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
 DEFAULT_GEOGRAPHY_SCAN_INTERVAL = timedelta(minutes=10)
 DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1)
@@ -97,7 +90,7 @@ def async_get_geography_id(geography_dict):
 
 async def async_setup(hass, config):
     """Set up the AirVisual component."""
-    hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}
+    hass.data[DOMAIN] = {DATA_COORDINATOR: {}}
 
     if DOMAIN not in config:
         return True
@@ -167,35 +160,71 @@ async def async_setup_entry(hass, config_entry):
 
     if CONF_API_KEY in config_entry.data:
         _standardize_geography_config_entry(hass, config_entry)
-        airvisual = AirVisualGeographyData(
+
+        client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
+
+        async def async_update_data():
+            """Get new data from the API."""
+            if CONF_CITY in config_entry.data:
+                api_coro = client.api.city(
+                    config_entry.data[CONF_CITY],
+                    config_entry.data[CONF_STATE],
+                    config_entry.data[CONF_COUNTRY],
+                )
+            else:
+                api_coro = client.api.nearest_city(
+                    config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE],
+                )
+
+            try:
+                return await api_coro
+            except AirVisualError as err:
+                raise UpdateFailed(f"Error while retrieving data: {err}")
+
+        coordinator = DataUpdateCoordinator(
             hass,
-            Client(api_key=config_entry.data[CONF_API_KEY], session=websession),
-            config_entry,
+            LOGGER,
+            name="geography data",
+            update_interval=DEFAULT_GEOGRAPHY_SCAN_INTERVAL,
+            update_method=async_update_data,
         )
 
         # Only geography-based entries have options:
         config_entry.add_update_listener(async_update_options)
     else:
         _standardize_node_pro_config_entry(hass, config_entry)
-        airvisual = AirVisualNodeProData(hass, Client(session=websession), config_entry)
 
-    await airvisual.async_update()
+        client = Client(session=websession)
 
-    hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airvisual
+        async def async_update_data():
+            """Get new data from the API."""
+            try:
+                return await client.node.from_samba(
+                    config_entry.data[CONF_IP_ADDRESS],
+                    config_entry.data[CONF_PASSWORD],
+                    include_history=False,
+                    include_trends=False,
+                )
+            except NodeProError as err:
+                raise UpdateFailed(f"Error while retrieving data: {err}")
+
+        coordinator = DataUpdateCoordinator(
+            hass,
+            LOGGER,
+            name="Node/Pro data",
+            update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL,
+            update_method=async_update_data,
+        )
+
+    await coordinator.async_refresh()
+
+    hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
 
     for component in PLATFORMS:
         hass.async_create_task(
             hass.config_entries.async_forward_entry_setup(config_entry, component)
         )
 
-    async def refresh(event_time):
-        """Refresh data from AirVisual."""
-        await airvisual.async_update()
-
-    hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval(
-        hass, refresh, airvisual.scan_interval
-    )
-
     return True
 
 
@@ -248,28 +277,31 @@ async def async_unload_entry(hass, config_entry):
         )
     )
     if unload_ok:
-        hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
-        remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id)
-        remove_listener()
+        hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id)
 
     return unload_ok
 
 
 async def async_update_options(hass, config_entry):
     """Handle an options update."""
-    airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
-    airvisual.async_update_options(config_entry.options)
+    coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
+    await coordinator.async_request_refresh()
 
 
 class AirVisualEntity(Entity):
     """Define a generic AirVisual entity."""
 
-    def __init__(self, airvisual):
+    def __init__(self, coordinator):
         """Initialize."""
-        self._airvisual = airvisual
         self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
         self._icon = None
         self._unit = None
+        self.coordinator = coordinator
+
+    @property
+    def available(self):
+        """Return if entity is available."""
+        return self.coordinator.last_update_success
 
     @property
     def device_state_attributes(self):
@@ -295,9 +327,7 @@ class AirVisualEntity(Entity):
             self.update_from_latest_data()
             self.async_write_ha_state()
 
-        self.async_on_remove(
-            async_dispatcher_connect(self.hass, self._airvisual.topic_update, update)
-        )
+        self.async_on_remove(self.coordinator.async_add_listener(update))
 
         self.update_from_latest_data()
 
@@ -305,76 +335,3 @@ class AirVisualEntity(Entity):
     def update_from_latest_data(self):
         """Update the entity from the latest data."""
         raise NotImplementedError
-
-
-class AirVisualGeographyData:
-    """Define a class to manage data from the AirVisual cloud API."""
-
-    def __init__(self, hass, client, config_entry):
-        """Initialize."""
-        self._client = client
-        self._hass = hass
-        self.data = {}
-        self.geography_data = config_entry.data
-        self.geography_id = config_entry.unique_id
-        self.integration_type = INTEGRATION_TYPE_GEOGRAPHY
-        self.options = config_entry.options
-        self.scan_interval = DEFAULT_GEOGRAPHY_SCAN_INTERVAL
-        self.topic_update = TOPIC_UPDATE.format(config_entry.unique_id)
-
-    async def async_update(self):
-        """Get new data for all locations from the AirVisual cloud API."""
-        if CONF_CITY in self.geography_data:
-            api_coro = self._client.api.city(
-                self.geography_data[CONF_CITY],
-                self.geography_data[CONF_STATE],
-                self.geography_data[CONF_COUNTRY],
-            )
-        else:
-            api_coro = self._client.api.nearest_city(
-                self.geography_data[CONF_LATITUDE], self.geography_data[CONF_LONGITUDE],
-            )
-
-        try:
-            self.data[self.geography_id] = await api_coro
-        except AirVisualError as err:
-            LOGGER.error("Error while retrieving data: %s", err)
-            self.data[self.geography_id] = {}
-
-        LOGGER.debug("Received new geography data")
-        async_dispatcher_send(self._hass, self.topic_update)
-
-    @callback
-    def async_update_options(self, options):
-        """Update the data manager's options."""
-        self.options = options
-        async_dispatcher_send(self._hass, self.topic_update)
-
-
-class AirVisualNodeProData:
-    """Define a class to manage data from an AirVisual Node/Pro."""
-
-    def __init__(self, hass, client, config_entry):
-        """Initialize."""
-        self._client = client
-        self._hass = hass
-        self._password = config_entry.data[CONF_PASSWORD]
-        self.data = {}
-        self.integration_type = INTEGRATION_TYPE_NODE_PRO
-        self.ip_address = config_entry.data[CONF_IP_ADDRESS]
-        self.scan_interval = DEFAULT_NODE_PRO_SCAN_INTERVAL
-        self.topic_update = TOPIC_UPDATE.format(config_entry.data[CONF_IP_ADDRESS])
-
-    async def async_update(self):
-        """Get new data from the Node/Pro."""
-        try:
-            self.data = await self._client.node.from_samba(
-                self.ip_address, self._password, include_history=False
-            )
-        except NodeProError as err:
-            LOGGER.error("Error while retrieving Node/Pro data: %s", err)
-            self.data = {}
-            return
-
-        LOGGER.debug("Received new Node/Pro data")
-        async_dispatcher_send(self._hass, self.topic_update)
diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py
index 71f9f9d9fbe..bd1c10a9d84 100644
--- a/homeassistant/components/airvisual/air_quality.py
+++ b/homeassistant/components/airvisual/air_quality.py
@@ -4,7 +4,12 @@ from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
 from homeassistant.core import callback
 
 from . import AirVisualEntity
-from .const import DATA_CLIENT, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY
+from .const import (
+    CONF_INTEGRATION_TYPE,
+    DATA_COORDINATOR,
+    DOMAIN,
+    INTEGRATION_TYPE_GEOGRAPHY,
+)
 
 ATTR_HUMIDITY = "humidity"
 ATTR_SENSOR_LIFE = "{0}_sensor_life"
@@ -13,13 +18,13 @@ ATTR_VOC = "voc"
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     """Set up AirVisual air quality entities based on a config entry."""
-    airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
+    coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
 
     # Geography-based AirVisual integrations don't utilize this platform:
-    if airvisual.integration_type == INTEGRATION_TYPE_GEOGRAPHY:
+    if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
         return
 
-    async_add_entities([AirVisualNodeProSensor(airvisual)], True)
+    async_add_entities([AirVisualNodeProSensor(coordinator)], True)
 
 
 class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
@@ -35,69 +40,71 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
     @property
     def air_quality_index(self):
         """Return the Air Quality Index (AQI)."""
-        if self._airvisual.data["current"]["settings"]["is_aqi_usa"]:
-            return self._airvisual.data["current"]["measurements"]["aqi_us"]
-        return self._airvisual.data["current"]["measurements"]["aqi_cn"]
+        if self.coordinator.data["current"]["settings"]["is_aqi_usa"]:
+            return self.coordinator.data["current"]["measurements"]["aqi_us"]
+        return self.coordinator.data["current"]["measurements"]["aqi_cn"]
 
     @property
     def available(self):
         """Return True if entity is available."""
-        return bool(self._airvisual.data)
+        return bool(self.coordinator.data)
 
     @property
     def carbon_dioxide(self):
         """Return the CO2 (carbon dioxide) level."""
-        return self._airvisual.data["current"]["measurements"].get("co2_ppm")
+        return self.coordinator.data["current"]["measurements"].get("co2")
 
     @property
     def device_info(self):
         """Return device registry information for this entity."""
         return {
-            "identifiers": {(DOMAIN, self._airvisual.data["current"]["serial_number"])},
-            "name": self._airvisual.data["current"]["settings"]["node_name"],
+            "identifiers": {
+                (DOMAIN, self.coordinator.data["current"]["serial_number"])
+            },
+            "name": self.coordinator.data["current"]["settings"]["node_name"],
             "manufacturer": "AirVisual",
-            "model": f'{self._airvisual.data["current"]["status"]["model"]}',
+            "model": f'{self.coordinator.data["current"]["status"]["model"]}',
             "sw_version": (
-                f'Version {self._airvisual.data["current"]["status"]["system_version"]}'
-                f'{self._airvisual.data["current"]["status"]["app_version"]}'
+                f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
+                f'{self.coordinator.data["current"]["status"]["app_version"]}'
             ),
         }
 
     @property
     def name(self):
         """Return the name."""
-        node_name = self._airvisual.data["current"]["settings"]["node_name"]
+        node_name = self.coordinator.data["current"]["settings"]["node_name"]
         return f"{node_name} Node/Pro: Air Quality"
 
     @property
     def particulate_matter_2_5(self):
         """Return the particulate matter 2.5 level."""
-        return self._airvisual.data["current"]["measurements"].get("pm2_5")
+        return self.coordinator.data["current"]["measurements"].get("pm2_5")
 
     @property
     def particulate_matter_10(self):
         """Return the particulate matter 10 level."""
-        return self._airvisual.data["current"]["measurements"].get("pm1_0")
+        return self.coordinator.data["current"]["measurements"].get("pm1_0")
 
     @property
     def particulate_matter_0_1(self):
         """Return the particulate matter 0.1 level."""
-        return self._airvisual.data["current"]["measurements"].get("pm0_1")
+        return self.coordinator.data["current"]["measurements"].get("pm0_1")
 
     @property
     def unique_id(self):
         """Return a unique, Home Assistant friendly identifier for this entity."""
-        return self._airvisual.data["current"]["serial_number"]
+        return self.coordinator.data["current"]["serial_number"]
 
     @callback
     def update_from_latest_data(self):
-        """Update from the Node/Pro's data."""
+        """Update the entity from the latest data."""
         self._attrs.update(
             {
-                ATTR_VOC: self._airvisual.data["current"]["measurements"].get("voc"),
+                ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"),
                 **{
                     ATTR_SENSOR_LIFE.format(pollutant): lifespan
-                    for pollutant, lifespan in self._airvisual.data["current"][
+                    for pollutant, lifespan in self.coordinator.data["current"][
                         "status"
                     ]["sensor_life"].items()
                 },
diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py
index 691fa19504a..abbc2df9061 100644
--- a/homeassistant/components/airvisual/config_flow.py
+++ b/homeassistant/components/airvisual/config_flow.py
@@ -146,7 +146,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
         try:
             await client.node.from_samba(
-                user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]
+                user_input[CONF_IP_ADDRESS],
+                user_input[CONF_PASSWORD],
+                include_history=False,
+                include_trends=False,
             )
         except NodeProError as err:
             LOGGER.error("Error connecting to Node/Pro unit: %s", err)
diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py
index 0e0e62a9b0c..a98a899b762 100644
--- a/homeassistant/components/airvisual/const.py
+++ b/homeassistant/components/airvisual/const.py
@@ -12,6 +12,4 @@ CONF_COUNTRY = "country"
 CONF_GEOGRAPHIES = "geographies"
 CONF_INTEGRATION_TYPE = "integration_type"
 
-DATA_CLIENT = "client"
-
-TOPIC_UPDATE = f"airvisual_update_{0}"
+DATA_COORDINATOR = "coordinator"
diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py
index 5009788e6fa..b122f3c27b4 100644
--- a/homeassistant/components/airvisual/sensor.py
+++ b/homeassistant/components/airvisual/sensor.py
@@ -24,7 +24,8 @@ from . import AirVisualEntity
 from .const import (
     CONF_CITY,
     CONF_COUNTRY,
-    DATA_CLIENT,
+    CONF_INTEGRATION_TYPE,
+    DATA_COORDINATOR,
     DOMAIN,
     INTEGRATION_TYPE_GEOGRAPHY,
 )
@@ -92,68 +93,53 @@ POLLUTANT_MAPPING = {
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     """Set up AirVisual sensors based on a config entry."""
-    airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
+    coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
 
-    if airvisual.integration_type == INTEGRATION_TYPE_GEOGRAPHY:
+    if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
         sensors = [
             AirVisualGeographySensor(
-                airvisual, kind, name, icon, unit, locale, geography_id,
+                coordinator, config_entry, kind, name, icon, unit, locale,
             )
-            for geography_id in airvisual.data
             for locale in GEOGRAPHY_SENSOR_LOCALES
             for kind, name, icon, unit in GEOGRAPHY_SENSORS
         ]
     else:
         sensors = [
-            AirVisualNodeProSensor(airvisual, kind, name, device_class, unit)
+            AirVisualNodeProSensor(coordinator, kind, name, device_class, unit)
             for kind, name, device_class, unit in NODE_PRO_SENSORS
         ]
 
     async_add_entities(sensors, True)
 
 
-class AirVisualSensor(AirVisualEntity):
-    """Define a generic AirVisual sensor."""
+class AirVisualGeographySensor(AirVisualEntity):
+    """Define an AirVisual sensor related to geography data via the Cloud API."""
 
-    def __init__(self, airvisual, kind, name, unit):
+    def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale):
         """Initialize."""
-        super().__init__(airvisual)
+        super().__init__(coordinator)
 
+        self._attrs.update(
+            {
+                ATTR_CITY: config_entry.data.get(CONF_CITY),
+                ATTR_STATE: config_entry.data.get(CONF_STATE),
+                ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY),
+            }
+        )
+        self._config_entry = config_entry
+        self._icon = icon
         self._kind = kind
+        self._locale = locale
         self._name = name
         self._state = None
         self._unit = unit
 
-    @property
-    def state(self):
-        """Return the state."""
-        return self._state
-
-
-class AirVisualGeographySensor(AirVisualSensor):
-    """Define an AirVisual sensor related to geography data via the Cloud API."""
-
-    def __init__(self, airvisual, kind, name, icon, unit, locale, geography_id):
-        """Initialize."""
-        super().__init__(airvisual, kind, name, unit)
-
-        self._attrs.update(
-            {
-                ATTR_CITY: airvisual.data[geography_id].get(CONF_CITY),
-                ATTR_STATE: airvisual.data[geography_id].get(CONF_STATE),
-                ATTR_COUNTRY: airvisual.data[geography_id].get(CONF_COUNTRY),
-            }
-        )
-        self._geography_id = geography_id
-        self._icon = icon
-        self._locale = locale
-
     @property
     def available(self):
         """Return True if entity is available."""
         try:
-            return bool(
-                self._airvisual.data[self._geography_id]["current"]["pollution"]
+            return self.coordinator.last_update_success and bool(
+                self.coordinator.data["current"]["pollution"]
             )
         except KeyError:
             return False
@@ -163,16 +149,21 @@ class AirVisualGeographySensor(AirVisualSensor):
         """Return the name."""
         return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}"
 
+    @property
+    def state(self):
+        """Return the state."""
+        return self._state
+
     @property
     def unique_id(self):
         """Return a unique, Home Assistant friendly identifier for this entity."""
-        return f"{self._geography_id}_{self._locale}_{self._kind}"
+        return f"{self._config_entry.unique_id}_{self._locale}_{self._kind}"
 
     @callback
     def update_from_latest_data(self):
-        """Update the sensor."""
+        """Update the entity from the latest data."""
         try:
-            data = self._airvisual.data[self._geography_id]["current"]["pollution"]
+            data = self.coordinator.data["current"]["pollution"]
         except KeyError:
             return
 
@@ -197,36 +188,31 @@ class AirVisualGeographySensor(AirVisualSensor):
                 }
             )
 
-        if CONF_LATITUDE in self._airvisual.geography_data:
-            if self._airvisual.options[CONF_SHOW_ON_MAP]:
-                self._attrs[ATTR_LATITUDE] = self._airvisual.geography_data[
-                    CONF_LATITUDE
-                ]
-                self._attrs[ATTR_LONGITUDE] = self._airvisual.geography_data[
-                    CONF_LONGITUDE
-                ]
+        if CONF_LATITUDE in self._config_entry.data:
+            if self._config_entry.options[CONF_SHOW_ON_MAP]:
+                self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE]
+                self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE]
                 self._attrs.pop("lati", None)
                 self._attrs.pop("long", None)
             else:
-                self._attrs["lati"] = self._airvisual.geography_data[CONF_LATITUDE]
-                self._attrs["long"] = self._airvisual.geography_data[CONF_LONGITUDE]
+                self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE]
+                self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE]
                 self._attrs.pop(ATTR_LATITUDE, None)
                 self._attrs.pop(ATTR_LONGITUDE, None)
 
 
-class AirVisualNodeProSensor(AirVisualSensor):
+class AirVisualNodeProSensor(AirVisualEntity):
     """Define an AirVisual sensor related to a Node/Pro unit."""
 
-    def __init__(self, airvisual, kind, name, device_class, unit):
+    def __init__(self, coordinator, kind, name, device_class, unit):
         """Initialize."""
-        super().__init__(airvisual, kind, name, unit)
+        super().__init__(coordinator)
 
         self._device_class = device_class
-
-    @property
-    def available(self):
-        """Return True if entity is available."""
-        return bool(self._airvisual.data)
+        self._kind = kind
+        self._name = name
+        self._state = None
+        self._unit = unit
 
     @property
     def device_class(self):
@@ -237,37 +223,44 @@ class AirVisualNodeProSensor(AirVisualSensor):
     def device_info(self):
         """Return device registry information for this entity."""
         return {
-            "identifiers": {(DOMAIN, self._airvisual.data["current"]["serial_number"])},
-            "name": self._airvisual.data["current"]["settings"]["node_name"],
+            "identifiers": {
+                (DOMAIN, self.coordinator.data["current"]["serial_number"])
+            },
+            "name": self.coordinator.data["current"]["settings"]["node_name"],
             "manufacturer": "AirVisual",
-            "model": f'{self._airvisual.data["current"]["status"]["model"]}',
+            "model": f'{self.coordinator.data["current"]["status"]["model"]}',
             "sw_version": (
-                f'Version {self._airvisual.data["current"]["status"]["system_version"]}'
-                f'{self._airvisual.data["current"]["status"]["app_version"]}'
+                f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
+                f'{self.coordinator.data["current"]["status"]["app_version"]}'
             ),
         }
 
     @property
     def name(self):
         """Return the name."""
-        node_name = self._airvisual.data["current"]["settings"]["node_name"]
+        node_name = self.coordinator.data["current"]["settings"]["node_name"]
         return f"{node_name} Node/Pro: {self._name}"
 
+    @property
+    def state(self):
+        """Return the state."""
+        return self._state
+
     @property
     def unique_id(self):
         """Return a unique, Home Assistant friendly identifier for this entity."""
-        return f"{self._airvisual.data['current']['serial_number']}_{self._kind}"
+        return f"{self.coordinator.data['current']['serial_number']}_{self._kind}"
 
     @callback
     def update_from_latest_data(self):
-        """Update from the Node/Pro's data."""
+        """Update the entity from the latest data."""
         if self._kind == SENSOR_KIND_BATTERY_LEVEL:
-            self._state = self._airvisual.data["current"]["status"]["battery"]
+            self._state = self.coordinator.data["current"]["status"]["battery"]
         elif self._kind == SENSOR_KIND_HUMIDITY:
-            self._state = self._airvisual.data["current"]["measurements"].get(
+            self._state = self.coordinator.data["current"]["measurements"].get(
                 "humidity"
             )
         elif self._kind == SENSOR_KIND_TEMPERATURE:
-            self._state = self._airvisual.data["current"]["measurements"].get(
+            self._state = self.coordinator.data["current"]["measurements"].get(
                 "temperature_C"
             )