From b8fdebd05c277d123edfce1ca865e1022825e757 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@cpanel.net>
Date: Mon, 23 Mar 2020 11:01:48 -0500
Subject: [PATCH] Add aircleaner and humidify service to nexia climate (#33078)

* Add aircleaner and humidify service to nexia climate

* These were removed from the original merge to reduce review scope

* Additional tests for binary_sensor, sensor, and climate states

* Switch to signals for services

Get rid of everywhere we call device and change to zone or thermostat
as it was too confusing

Renames to make it clear that zone and thermostat are tightly coupled

* Make scene activation responsive

* no need to use update for only one key/value

* stray comma

* use async_call_later

* its async, need ()s

* cleaner

* merge entity platform services testing branch
---
 homeassistant/components/nexia/__init__.py    |   5 +-
 .../components/nexia/binary_sensor.py         |  59 +---
 homeassistant/components/nexia/climate.py     | 277 ++++++++++--------
 homeassistant/components/nexia/const.py       |   7 +-
 homeassistant/components/nexia/entity.py      | 105 ++++++-
 homeassistant/components/nexia/scene.py       |  48 ++-
 homeassistant/components/nexia/sensor.py      | 133 +++------
 homeassistant/components/nexia/services.yaml  |  19 ++
 homeassistant/components/nexia/util.py        |   6 +
 tests/components/nexia/test_binary_sensor.py  |  35 +++
 tests/components/nexia/test_climate.py        |  35 +++
 tests/components/nexia/test_scene.py          |   6 +-
 tests/components/nexia/test_sensor.py         | 133 +++++++++
 13 files changed, 576 insertions(+), 292 deletions(-)
 create mode 100644 homeassistant/components/nexia/services.yaml
 create mode 100644 homeassistant/components/nexia/util.py
 create mode 100644 tests/components/nexia/test_binary_sensor.py
 create mode 100644 tests/components/nexia/test_sensor.py

diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py
index 41ecf6f1045..5c317794c2a 100644
--- a/homeassistant/components/nexia/__init__.py
+++ b/homeassistant/components/nexia/__init__.py
@@ -15,7 +15,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
 
-from .const import DATA_NEXIA, DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR
+from .const import DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -94,8 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
         update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE),
     )
 
-    hass.data[DOMAIN][entry.entry_id] = {}
-    hass.data[DOMAIN][entry.entry_id][DATA_NEXIA] = {
+    hass.data[DOMAIN][entry.entry_id] = {
         NEXIA_DEVICE: nexia_home,
         UPDATE_COORDINATOR: coordinator,
     }
diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py
index 2802c3d7bd4..5c33412c647 100644
--- a/homeassistant/components/nexia/binary_sensor.py
+++ b/homeassistant/components/nexia/binary_sensor.py
@@ -1,23 +1,15 @@
 """Support for Nexia / Trane XL Thermostats."""
 
 from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.const import ATTR_ATTRIBUTION
 
-from .const import (
-    ATTRIBUTION,
-    DATA_NEXIA,
-    DOMAIN,
-    MANUFACTURER,
-    NEXIA_DEVICE,
-    UPDATE_COORDINATOR,
-)
-from .entity import NexiaEntity
+from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR
+from .entity import NexiaThermostatEntity
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     """Set up sensors for a Nexia device."""
 
-    nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA]
+    nexia_data = hass.data[DOMAIN][config_entry.entry_id]
     nexia_home = nexia_data[NEXIA_DEVICE]
     coordinator = nexia_data[UPDATE_COORDINATOR]
 
@@ -42,48 +34,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
     async_add_entities(entities, True)
 
 
-class NexiaBinarySensor(NexiaEntity, BinarySensorDevice):
+class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorDevice):
     """Provices Nexia BinarySensor support."""
 
-    def __init__(self, coordinator, device, sensor_call, sensor_name):
+    def __init__(self, coordinator, thermostat, sensor_call, sensor_name):
         """Initialize the nexia sensor."""
-        super().__init__(coordinator)
-        self._coordinator = coordinator
-        self._device = device
-        self._name = f"{self._device.get_name()} {sensor_name}"
+        super().__init__(
+            coordinator,
+            thermostat,
+            name=f"{thermostat.get_name()} {sensor_name}",
+            unique_id=f"{thermostat.thermostat_id}_{sensor_call}",
+        )
         self._call = sensor_call
-        self._unique_id = f"{self._device.thermostat_id}_{sensor_call}"
         self._state = None
 
-    @property
-    def unique_id(self):
-        """Return the unique id of the binary sensor."""
-        return self._unique_id
-
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def device_info(self):
-        """Return the device_info of the device."""
-        return {
-            "identifiers": {(DOMAIN, self._device.thermostat_id)},
-            "name": self._device.get_name(),
-            "model": self._device.get_model(),
-            "sw_version": self._device.get_firmware(),
-            "manufacturer": MANUFACTURER,
-        }
-
-    @property
-    def device_state_attributes(self):
-        """Return the device specific state attributes."""
-        return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
-        }
-
     @property
     def is_on(self):
         """Return the status of the sensor."""
-        return getattr(self._device, self._call)()
+        return getattr(self._thermostat, self._call)()
diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py
index 7231f2b8ba9..8af1be20b1e 100644
--- a/homeassistant/components/nexia/climate.py
+++ b/homeassistant/components/nexia/climate.py
@@ -12,9 +12,11 @@ from nexia.const import (
     SYSTEM_STATUS_IDLE,
     UNIT_FAHRENHEIT,
 )
+import voluptuous as vol
 
 from homeassistant.components.climate import ClimateDevice
 from homeassistant.components.climate.const import (
+    ATTR_HUMIDITY,
     ATTR_MAX_HUMIDITY,
     ATTR_MIN_HUMIDITY,
     ATTR_TARGET_TEMP_HIGH,
@@ -36,26 +38,50 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_TEMPERATURE_RANGE,
 )
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
+    ATTR_ENTITY_ID,
     ATTR_TEMPERATURE,
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
 )
+from homeassistant.helpers import entity_platform
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import dispatcher_send
 
 from .const import (
+    ATTR_AIRCLEANER_MODE,
     ATTR_DEHUMIDIFY_SETPOINT,
     ATTR_DEHUMIDIFY_SUPPORTED,
     ATTR_HUMIDIFY_SETPOINT,
     ATTR_HUMIDIFY_SUPPORTED,
     ATTR_ZONE_STATUS,
-    ATTRIBUTION,
-    DATA_NEXIA,
     DOMAIN,
-    MANUFACTURER,
     NEXIA_DEVICE,
+    SIGNAL_THERMOSTAT_UPDATE,
+    SIGNAL_ZONE_UPDATE,
     UPDATE_COORDINATOR,
 )
-from .entity import NexiaEntity
+from .entity import NexiaThermostatZoneEntity
+from .util import percent_conv
+
+SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode"
+SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint"
+
+SET_AIRCLEANER_SCHEMA = vol.Schema(
+    {
+        vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+        vol.Required(ATTR_AIRCLEANER_MODE): cv.string,
+    }
+)
+
+SET_HUMIDITY_SCHEMA = vol.Schema(
+    {
+        vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+        vol.Required(ATTR_HUMIDITY): vol.All(
+            vol.Coerce(int), vol.Range(min=35, max=65)
+        ),
+    }
+)
+
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -83,10 +109,21 @@ NEXIA_TO_HA_HVAC_MODE_MAP = {
 async def async_setup_entry(hass, config_entry, async_add_entities):
     """Set up climate for a Nexia device."""
 
-    nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA]
+    nexia_data = hass.data[DOMAIN][config_entry.entry_id]
     nexia_home = nexia_data[NEXIA_DEVICE]
     coordinator = nexia_data[UPDATE_COORDINATOR]
 
+    platform = entity_platform.current_platform.get()
+
+    platform.async_register_entity_service(
+        SERVICE_SET_HUMIDIFY_SETPOINT,
+        SET_HUMIDITY_SCHEMA,
+        SERVICE_SET_HUMIDIFY_SETPOINT,
+    )
+    platform.async_register_entity_service(
+        SERVICE_SET_AIRCLEANER_MODE, SET_AIRCLEANER_SCHEMA, SERVICE_SET_AIRCLEANER_MODE,
+    )
+
     entities = []
     for thermostat_id in nexia_home.get_thermostat_ids():
         thermostat = nexia_home.get_thermostat_by_id(thermostat_id)
@@ -97,26 +134,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
     async_add_entities(entities, True)
 
 
-class NexiaZone(NexiaEntity, ClimateDevice):
+class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice):
     """Provides Nexia Climate support."""
 
-    def __init__(self, coordinator, device):
+    def __init__(self, coordinator, zone):
         """Initialize the thermostat."""
-        super().__init__(coordinator)
-        self.thermostat = device.thermostat
-        self._device = device
-        self._coordinator = coordinator
+        super().__init__(
+            coordinator, zone, name=zone.get_name(), unique_id=zone.zone_id
+        )
+        self._undo_humidfy_dispatcher = None
+        self._undo_aircleaner_dispatcher = None
         # The has_* calls are stable for the life of the device
         # and do not do I/O
-        self._has_relative_humidity = self.thermostat.has_relative_humidity()
-        self._has_emergency_heat = self.thermostat.has_emergency_heat()
-        self._has_humidify_support = self.thermostat.has_humidify_support()
-        self._has_dehumidify_support = self.thermostat.has_dehumidify_support()
-
-    @property
-    def unique_id(self):
-        """Device Uniqueid."""
-        return self._device.zone_id
+        self._has_relative_humidity = self._thermostat.has_relative_humidity()
+        self._has_emergency_heat = self._thermostat.has_emergency_heat()
+        self._has_humidify_support = self._thermostat.has_humidify_support()
+        self._has_dehumidify_support = self._thermostat.has_dehumidify_support()
 
     @property
     def supported_features(self):
@@ -139,27 +172,22 @@ class NexiaZone(NexiaEntity, ClimateDevice):
     @property
     def is_fan_on(self):
         """Blower is on."""
-        return self.thermostat.is_blower_active()
-
-    @property
-    def name(self):
-        """Name of the zone."""
-        return self._device.get_name()
+        return self._thermostat.is_blower_active()
 
     @property
     def temperature_unit(self):
         """Return the unit of measurement."""
-        return TEMP_CELSIUS if self.thermostat.get_unit() == "C" else TEMP_FAHRENHEIT
+        return TEMP_CELSIUS if self._thermostat.get_unit() == "C" else TEMP_FAHRENHEIT
 
     @property
     def current_temperature(self):
         """Return the current temperature."""
-        return self._device.get_temperature()
+        return self._zone.get_temperature()
 
     @property
     def fan_mode(self):
         """Return the fan setting."""
-        return self.thermostat.get_fan_mode()
+        return self._thermostat.get_fan_mode()
 
     @property
     def fan_modes(self):
@@ -169,92 +197,92 @@ class NexiaZone(NexiaEntity, ClimateDevice):
     @property
     def min_temp(self):
         """Minimum temp for the current setting."""
-        return (self._device.thermostat.get_setpoint_limits())[0]
+        return (self._thermostat.get_setpoint_limits())[0]
 
     @property
     def max_temp(self):
         """Maximum temp for the current setting."""
-        return (self._device.thermostat.get_setpoint_limits())[1]
+        return (self._thermostat.get_setpoint_limits())[1]
 
     def set_fan_mode(self, fan_mode):
         """Set new target fan mode."""
-        self.thermostat.set_fan_mode(fan_mode)
-        self.schedule_update_ha_state()
+        self._thermostat.set_fan_mode(fan_mode)
+        self._signal_thermostat_update()
 
     @property
     def preset_mode(self):
         """Preset that is active."""
-        return self._device.get_preset()
+        return self._zone.get_preset()
 
     @property
     def preset_modes(self):
         """All presets."""
-        return self._device.get_presets()
+        return self._zone.get_presets()
 
     def set_humidity(self, humidity):
         """Dehumidify target."""
-        self.thermostat.set_dehumidify_setpoint(humidity / 100.0)
-        self.schedule_update_ha_state()
+        self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
+        self._signal_thermostat_update()
 
     @property
     def target_humidity(self):
         """Humidity indoors setpoint."""
         if self._has_dehumidify_support:
-            return round(self.thermostat.get_dehumidify_setpoint() * 100.0, 1)
+            return percent_conv(self._thermostat.get_dehumidify_setpoint())
         if self._has_humidify_support:
-            return round(self.thermostat.get_humidify_setpoint() * 100.0, 1)
+            return percent_conv(self._thermostat.get_humidify_setpoint())
         return None
 
     @property
     def current_humidity(self):
         """Humidity indoors."""
         if self._has_relative_humidity:
-            return round(self.thermostat.get_relative_humidity() * 100.0, 1)
+            return percent_conv(self._thermostat.get_relative_humidity())
         return None
 
     @property
     def target_temperature(self):
         """Temperature we try to reach."""
-        current_mode = self._device.get_current_mode()
+        current_mode = self._zone.get_current_mode()
 
         if current_mode == OPERATION_MODE_COOL:
-            return self._device.get_cooling_setpoint()
+            return self._zone.get_cooling_setpoint()
         if current_mode == OPERATION_MODE_HEAT:
-            return self._device.get_heating_setpoint()
+            return self._zone.get_heating_setpoint()
         return None
 
     @property
     def target_temperature_step(self):
         """Step size of temperature units."""
-        if self._device.thermostat.get_unit() == UNIT_FAHRENHEIT:
+        if self._thermostat.get_unit() == UNIT_FAHRENHEIT:
             return 1.0
         return 0.5
 
     @property
     def target_temperature_high(self):
         """Highest temperature we are trying to reach."""
-        current_mode = self._device.get_current_mode()
+        current_mode = self._zone.get_current_mode()
 
         if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
             return None
-        return self._device.get_cooling_setpoint()
+        return self._zone.get_cooling_setpoint()
 
     @property
     def target_temperature_low(self):
         """Lowest temperature we are trying to reach."""
-        current_mode = self._device.get_current_mode()
+        current_mode = self._zone.get_current_mode()
 
         if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
             return None
-        return self._device.get_heating_setpoint()
+        return self._zone.get_heating_setpoint()
 
     @property
     def hvac_action(self) -> str:
         """Operation ie. heat, cool, idle."""
-        system_status = self.thermostat.get_system_status()
-        zone_called = self._device.is_calling()
+        system_status = self._thermostat.get_system_status()
+        zone_called = self._zone.is_calling()
 
-        if self._device.get_requested_mode() == OPERATION_MODE_OFF:
+        if self._zone.get_requested_mode() == OPERATION_MODE_OFF:
             return CURRENT_HVAC_OFF
         if not zone_called:
             return CURRENT_HVAC_IDLE
@@ -269,8 +297,8 @@ class NexiaZone(NexiaEntity, ClimateDevice):
     @property
     def hvac_mode(self):
         """Return current mode, as the user-visible name."""
-        mode = self._device.get_requested_mode()
-        hold = self._device.is_in_permanent_hold()
+        mode = self._zone.get_requested_mode()
+        hold = self._zone.is_in_permanent_hold()
 
         # If the device is in hold mode with
         # OPERATION_MODE_AUTO
@@ -299,10 +327,10 @@ class NexiaZone(NexiaEntity, ClimateDevice):
         new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH, None)
         set_temp = kwargs.get(ATTR_TEMPERATURE, None)
 
-        deadband = self.thermostat.get_deadband()
-        cur_cool_temp = self._device.get_cooling_setpoint()
-        cur_heat_temp = self._device.get_heating_setpoint()
-        (min_temp, max_temp) = self.thermostat.get_setpoint_limits()
+        deadband = self._thermostat.get_deadband()
+        cur_cool_temp = self._zone.get_cooling_setpoint()
+        cur_heat_temp = self._zone.get_heating_setpoint()
+        (min_temp, max_temp) = self._thermostat.get_setpoint_limits()
 
         # Check that we're not going to hit any minimum or maximum values
         if new_heat_temp and new_heat_temp + deadband > max_temp:
@@ -318,114 +346,119 @@ class NexiaZone(NexiaEntity, ClimateDevice):
             if new_cool_temp - new_heat_temp < deadband:
                 new_heat_temp = new_cool_temp - deadband
 
-        self._device.set_heat_cool_temp(
+        self._zone.set_heat_cool_temp(
             heat_temperature=new_heat_temp,
             cool_temperature=new_cool_temp,
             set_temperature=set_temp,
         )
-        self.schedule_update_ha_state()
+        self._signal_zone_update()
 
     @property
     def is_aux_heat(self):
         """Emergency heat state."""
-        return self.thermostat.is_emergency_heat_active()
-
-    @property
-    def device_info(self):
-        """Return the device_info of the device."""
-        return {
-            "identifiers": {(DOMAIN, self._device.zone_id)},
-            "name": self._device.get_name(),
-            "model": self.thermostat.get_model(),
-            "sw_version": self.thermostat.get_firmware(),
-            "manufacturer": MANUFACTURER,
-            "via_device": (DOMAIN, self.thermostat.thermostat_id),
-        }
+        return self._thermostat.is_emergency_heat_active()
 
     @property
     def device_state_attributes(self):
         """Return the device specific state attributes."""
-        data = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
-            ATTR_ZONE_STATUS: self._device.get_status(),
-        }
+        data = super().device_state_attributes
+
+        data[ATTR_ZONE_STATUS] = self._zone.get_status()
+
+        if not self._has_relative_humidity:
+            return data
+
+        min_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[0])
+        max_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[1])
+        data.update(
+            {
+                ATTR_MIN_HUMIDITY: min_humidity,
+                ATTR_MAX_HUMIDITY: max_humidity,
+                ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support,
+                ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support,
+            }
+        )
 
-        if self._has_relative_humidity:
-            data.update(
-                {
-                    ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support,
-                    ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support,
-                    ATTR_MIN_HUMIDITY: round(
-                        self.thermostat.get_humidity_setpoint_limits()[0] * 100.0, 1,
-                    ),
-                    ATTR_MAX_HUMIDITY: round(
-                        self.thermostat.get_humidity_setpoint_limits()[1] * 100.0, 1,
-                    ),
-                }
+        if self._has_dehumidify_support:
+            dehumdify_setpoint = percent_conv(
+                self._thermostat.get_dehumidify_setpoint()
             )
-            if self._has_dehumidify_support:
-                data.update(
-                    {
-                        ATTR_DEHUMIDIFY_SETPOINT: round(
-                            self.thermostat.get_dehumidify_setpoint() * 100.0, 1
-                        ),
-                    }
-                )
-            if self._has_humidify_support:
-                data.update(
-                    {
-                        ATTR_HUMIDIFY_SETPOINT: round(
-                            self.thermostat.get_humidify_setpoint() * 100.0, 1
-                        )
-                    }
-                )
+            data[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint
+
+        if self._has_humidify_support:
+            humdify_setpoint = percent_conv(self._thermostat.get_humidify_setpoint())
+            data[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint
+
         return data
 
     def set_preset_mode(self, preset_mode: str):
         """Set the preset mode."""
-        self._device.set_preset(preset_mode)
-        self.schedule_update_ha_state()
+        self._zone.set_preset(preset_mode)
+        self._signal_zone_update()
 
     def turn_aux_heat_off(self):
         """Turn. Aux Heat off."""
-        self.thermostat.set_emergency_heat(False)
-        self.schedule_update_ha_state()
+        self._thermostat.set_emergency_heat(False)
+        self._signal_thermostat_update()
 
     def turn_aux_heat_on(self):
         """Turn. Aux Heat on."""
-        self.thermostat.set_emergency_heat(True)
-        self.schedule_update_ha_state()
+        self._thermostat.set_emergency_heat(True)
+        self._signal_thermostat_update()
 
     def turn_off(self):
         """Turn. off the zone."""
         self.set_hvac_mode(OPERATION_MODE_OFF)
-        self.schedule_update_ha_state()
+        self._signal_zone_update()
 
     def turn_on(self):
         """Turn. on the zone."""
         self.set_hvac_mode(OPERATION_MODE_AUTO)
-        self.schedule_update_ha_state()
+        self._signal_zone_update()
 
     def set_hvac_mode(self, hvac_mode: str) -> None:
         """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc)."""
         if hvac_mode == HVAC_MODE_AUTO:
-            self._device.call_return_to_schedule()
-            self._device.set_mode(mode=OPERATION_MODE_AUTO)
+            self._zone.call_return_to_schedule()
+            self._zone.set_mode(mode=OPERATION_MODE_AUTO)
         else:
-            self._device.call_permanent_hold()
-            self._device.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode])
+            self._zone.call_permanent_hold()
+            self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode])
 
         self.schedule_update_ha_state()
 
     def set_aircleaner_mode(self, aircleaner_mode):
         """Set the aircleaner mode."""
-        self.thermostat.set_air_cleaner(aircleaner_mode)
-        self.schedule_update_ha_state()
+        self._thermostat.set_air_cleaner(aircleaner_mode)
+        self._signal_thermostat_update()
 
-    def set_humidify_setpoint(self, humidify_setpoint):
+    def set_humidify_setpoint(self, humidity):
         """Set the humidify setpoint."""
-        self.thermostat.set_humidify_setpoint(humidify_setpoint / 100.0)
-        self.schedule_update_ha_state()
+        self._thermostat.set_humidify_setpoint(humidity / 100.0)
+        self._signal_thermostat_update()
+
+    def _signal_thermostat_update(self):
+        """Signal a thermostat update.
+
+        Whenever the underlying library does an action against
+        a thermostat, the data for the thermostat and all
+        connected zone is updated.
+
+        Update all the zones on the thermostat.
+        """
+        dispatcher_send(
+            self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}"
+        )
+
+    def _signal_zone_update(self):
+        """Signal a zone update.
+
+        Whenever the underlying library does an action against
+        a zone, the data for the zone is updated.
+
+        Update a single zone.
+        """
+        dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}")
 
     async def async_update(self):
         """Update the entity.
diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py
index 384c3aad1b6..dbe7b71705c 100644
--- a/homeassistant/components/nexia/const.py
+++ b/homeassistant/components/nexia/const.py
@@ -7,7 +7,6 @@ ATTRIBUTION = "Data provided by mynexia.com"
 NOTIFICATION_ID = "nexia_notification"
 NOTIFICATION_TITLE = "Nexia Setup"
 
-DATA_NEXIA = "nexia"
 NEXIA_DEVICE = "device"
 NEXIA_SCAN_INTERVAL = "scan_interval"
 
@@ -16,6 +15,8 @@ DEFAULT_ENTITY_NAMESPACE = "nexia"
 
 ATTR_DESCRIPTION = "description"
 
+ATTR_AIRCLEANER_MODE = "aircleaner_mode"
+
 ATTR_ZONE_STATUS = "zone_status"
 ATTR_HUMIDIFY_SUPPORTED = "humidify_supported"
 ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported"
@@ -24,5 +25,7 @@ ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint"
 
 UPDATE_COORDINATOR = "update_coordinator"
 
-
 MANUFACTURER = "Trane"
+
+SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE"
+SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE"
diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py
index ec02a7e5f21..60675cc5888 100644
--- a/homeassistant/components/nexia/entity.py
+++ b/homeassistant/components/nexia/entity.py
@@ -1,14 +1,26 @@
 """The nexia integration base entity."""
 
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity import Entity
 
+from .const import (
+    ATTRIBUTION,
+    DOMAIN,
+    MANUFACTURER,
+    SIGNAL_THERMOSTAT_UPDATE,
+    SIGNAL_ZONE_UPDATE,
+)
+
 
 class NexiaEntity(Entity):
     """Base class for nexia entities."""
 
-    def __init__(self, coordinator):
+    def __init__(self, coordinator, name, unique_id):
         """Initialize the entity."""
         super().__init__()
+        self._unique_id = unique_id
+        self._name = name
         self._coordinator = coordinator
 
     @property
@@ -16,6 +28,23 @@ class NexiaEntity(Entity):
         """Return True if entity is available."""
         return self._coordinator.last_update_success
 
+    @property
+    def unique_id(self):
+        """Return the unique id."""
+        return self._unique_id
+
+    @property
+    def name(self):
+        """Return the name."""
+        return self._name
+
+    @property
+    def device_state_attributes(self):
+        """Return the device specific state attributes."""
+        return {
+            ATTR_ATTRIBUTION: ATTRIBUTION,
+        }
+
     @property
     def should_poll(self):
         """Return False, updates are controlled via coordinator."""
@@ -28,3 +57,77 @@ class NexiaEntity(Entity):
     async def async_will_remove_from_hass(self):
         """Undo subscription."""
         self._coordinator.async_remove_listener(self.async_write_ha_state)
+
+
+class NexiaThermostatEntity(NexiaEntity):
+    """Base class for nexia devices attached to a thermostat."""
+
+    def __init__(self, coordinator, thermostat, name, unique_id):
+        """Initialize the entity."""
+        super().__init__(coordinator, name, unique_id)
+        self._thermostat = thermostat
+        self._thermostat_update_subscription = None
+
+    @property
+    def device_info(self):
+        """Return the device_info of the device."""
+        return {
+            "identifiers": {(DOMAIN, self._thermostat.thermostat_id)},
+            "name": self._thermostat.get_name(),
+            "model": self._thermostat.get_model(),
+            "sw_version": self._thermostat.get_firmware(),
+            "manufacturer": MANUFACTURER,
+        }
+
+    async def async_added_to_hass(self):
+        """Listen for signals for services."""
+        await super().async_added_to_hass()
+        self._thermostat_update_subscription = async_dispatcher_connect(
+            self.hass,
+            f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}",
+            self.async_write_ha_state,
+        )
+
+    async def async_will_remove_from_hass(self):
+        """Unsub from signals for services."""
+        await super().async_will_remove_from_hass()
+        if self._thermostat_update_subscription:
+            self._thermostat_update_subscription()
+
+
+class NexiaThermostatZoneEntity(NexiaThermostatEntity):
+    """Base class for nexia devices attached to a thermostat."""
+
+    def __init__(self, coordinator, zone, name, unique_id):
+        """Initialize the entity."""
+        super().__init__(coordinator, zone.thermostat, name, unique_id)
+        self._zone = zone
+        self._zone_update_subscription = None
+
+    @property
+    def device_info(self):
+        """Return the device_info of the device."""
+        data = super().device_info
+        data.update(
+            {
+                "identifiers": {(DOMAIN, self._zone.zone_id)},
+                "name": self._zone.get_name(),
+                "via_device": (DOMAIN, self._zone.thermostat.thermostat_id),
+            }
+        )
+        return data
+
+    async def async_added_to_hass(self):
+        """Listen for signals for services."""
+        await super().async_added_to_hass()
+        self._zone_update_subscription = async_dispatcher_connect(
+            self.hass,
+            f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}",
+            self.async_write_ha_state,
+        )
+
+    async def async_will_remove_from_hass(self):
+        """Unsub from signals for services."""
+        await super().async_will_remove_from_hass()
+        if self._zone_update_subscription:
+            self._zone_update_subscription()
diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py
index 4489a4de274..fb851618aec 100644
--- a/homeassistant/components/nexia/scene.py
+++ b/homeassistant/components/nexia/scene.py
@@ -1,23 +1,18 @@
 """Support for Nexia Automations."""
 
 from homeassistant.components.scene import Scene
-from homeassistant.const import ATTR_ATTRIBUTION
-
-from .const import (
-    ATTR_DESCRIPTION,
-    ATTRIBUTION,
-    DATA_NEXIA,
-    DOMAIN,
-    NEXIA_DEVICE,
-    UPDATE_COORDINATOR,
-)
+from homeassistant.helpers.event import async_call_later
+
+from .const import ATTR_DESCRIPTION, DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR
 from .entity import NexiaEntity
 
+SCENE_ACTIVATION_TIME = 5
+
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     """Set up automations for a Nexia device."""
 
-    nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA]
+    nexia_data = hass.data[DOMAIN][config_entry.entry_id]
     nexia_home = nexia_data[NEXIA_DEVICE]
     coordinator = nexia_data[UPDATE_COORDINATOR]
     entities = []
@@ -36,33 +31,28 @@ class NexiaAutomationScene(NexiaEntity, Scene):
 
     def __init__(self, coordinator, automation):
         """Initialize the automation scene."""
-        super().__init__(coordinator)
+        super().__init__(
+            coordinator, name=automation.name, unique_id=automation.automation_id,
+        )
         self._automation = automation
 
-    @property
-    def unique_id(self):
-        """Return the unique id of the automation scene."""
-        # This is the automation unique_id
-        return self._automation.automation_id
-
-    @property
-    def name(self):
-        """Return the name of the automation scene."""
-        return self._automation.name
-
     @property
     def device_state_attributes(self):
         """Return the scene specific state attributes."""
-        return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
-            ATTR_DESCRIPTION: self._automation.description,
-        }
+        data = super().device_state_attributes
+        data[ATTR_DESCRIPTION] = self._automation.description
+        return data
 
     @property
     def icon(self):
         """Return the icon of the automation scene."""
         return "mdi:script-text-outline"
 
-    def activate(self):
+    async def async_activate(self):
         """Activate an automation scene."""
-        self._automation.activate()
+        await self.hass.async_add_executor_job(self._automation.activate)
+
+        async def refresh_callback(_):
+            await self._coordinator.async_refresh()
+
+        async_call_later(self.hass, SCENE_ACTIVATION_TIME, refresh_callback)
diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py
index 251101ccb1e..abbffa2b844 100644
--- a/homeassistant/components/nexia/sensor.py
+++ b/homeassistant/components/nexia/sensor.py
@@ -3,7 +3,6 @@
 from nexia.const import UNIT_CELSIUS
 
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     TEMP_CELSIUS,
@@ -11,21 +10,15 @@ from homeassistant.const import (
     UNIT_PERCENTAGE,
 )
 
-from .const import (
-    ATTRIBUTION,
-    DATA_NEXIA,
-    DOMAIN,
-    MANUFACTURER,
-    NEXIA_DEVICE,
-    UPDATE_COORDINATOR,
-)
-from .entity import NexiaEntity
+from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR
+from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity
+from .util import percent_conv
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     """Set up sensors for a Nexia device."""
 
-    nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA]
+    nexia_data = hass.data[DOMAIN][config_entry.entry_id]
     nexia_home = nexia_data[NEXIA_DEVICE]
     coordinator = nexia_data[UPDATE_COORDINATOR]
     entities = []
@@ -35,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
         thermostat = nexia_home.get_thermostat_by_id(thermostat_id)
 
         entities.append(
-            NexiaSensor(
+            NexiaThermostatSensor(
                 coordinator,
                 thermostat,
                 "get_system_status",
@@ -46,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
         )
         # Air cleaner
         entities.append(
-            NexiaSensor(
+            NexiaThermostatSensor(
                 coordinator,
                 thermostat,
                 "get_air_cleaner_mode",
@@ -58,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
         # Compressor Speed
         if thermostat.has_variable_speed_compressor():
             entities.append(
-                NexiaSensor(
+                NexiaThermostatSensor(
                     coordinator,
                     thermostat,
                     "get_current_compressor_speed",
@@ -69,7 +62,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
                 )
             )
             entities.append(
-                NexiaSensor(
+                NexiaThermostatSensor(
                     coordinator,
                     thermostat,
                     "get_requested_compressor_speed",
@@ -87,7 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
                 else TEMP_FAHRENHEIT
             )
             entities.append(
-                NexiaSensor(
+                NexiaThermostatSensor(
                     coordinator,
                     thermostat,
                     "get_outdoor_temperature",
@@ -99,7 +92,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
         # Relative Humidity
         if thermostat.has_relative_humidity():
             entities.append(
-                NexiaSensor(
+                NexiaThermostatSensor(
                     coordinator,
                     thermostat,
                     "get_relative_humidity",
@@ -120,7 +113,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
             )
             # Temperature
             entities.append(
-                NexiaZoneSensor(
+                NexiaThermostatZoneSensor(
                     coordinator,
                     zone,
                     "get_temperature",
@@ -132,13 +125,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
             )
             # Zone Status
             entities.append(
-                NexiaZoneSensor(
+                NexiaThermostatZoneSensor(
                     coordinator, zone, "get_status", "Zone Status", None, None,
                 )
             )
             # Setpoint Status
             entities.append(
-                NexiaZoneSensor(
+                NexiaThermostatZoneSensor(
                     coordinator,
                     zone,
                     "get_setpoint_status",
@@ -151,18 +144,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
     async_add_entities(entities, True)
 
 
-def percent_conv(val):
-    """Convert an actual percentage (0.0-1.0) to 0-100 scale."""
-    return val * 100.0
-
-
-class NexiaSensor(NexiaEntity):
+class NexiaThermostatSensor(NexiaThermostatEntity):
     """Provides Nexia thermostat sensor support."""
 
     def __init__(
         self,
         coordinator,
-        device,
+        thermostat,
         sensor_call,
         sensor_name,
         sensor_class,
@@ -170,35 +158,18 @@ class NexiaSensor(NexiaEntity):
         modifier=None,
     ):
         """Initialize the sensor."""
-        super().__init__(coordinator)
-        self._coordinator = coordinator
-        self._device = device
+        super().__init__(
+            coordinator,
+            thermostat,
+            name=f"{thermostat.get_name()} {sensor_name}",
+            unique_id=f"{thermostat.thermostat_id}_{sensor_call}",
+        )
         self._call = sensor_call
-        self._sensor_name = sensor_name
         self._class = sensor_class
         self._state = None
-        self._name = f"{self._device.get_name()} {self._sensor_name}"
         self._unit_of_measurement = sensor_unit
         self._modifier = modifier
 
-    @property
-    def unique_id(self):
-        """Return the unique id of the sensor."""
-        # This is the thermostat unique_id
-        return f"{self._device.thermostat_id}_{self._call}"
-
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def device_state_attributes(self):
-        """Return the device specific state attributes."""
-        return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
-        }
-
     @property
     def device_class(self):
         """Return the device class of the sensor."""
@@ -207,7 +178,7 @@ class NexiaSensor(NexiaEntity):
     @property
     def state(self):
         """Return the state of the sensor."""
-        val = getattr(self._device, self._call)()
+        val = getattr(self._thermostat, self._call)()
         if self._modifier:
             val = self._modifier(val)
         if isinstance(val, float):
@@ -219,25 +190,14 @@ class NexiaSensor(NexiaEntity):
         """Return the unit of measurement this sensor expresses itself in."""
         return self._unit_of_measurement
 
-    @property
-    def device_info(self):
-        """Return the device_info of the device."""
-        return {
-            "identifiers": {(DOMAIN, self._device.thermostat_id)},
-            "name": self._device.get_name(),
-            "model": self._device.get_model(),
-            "sw_version": self._device.get_firmware(),
-            "manufacturer": MANUFACTURER,
-        }
-
 
-class NexiaZoneSensor(NexiaSensor):
+class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity):
     """Nexia Zone Sensor Support."""
 
     def __init__(
         self,
         coordinator,
-        device,
+        zone,
         sensor_call,
         sensor_name,
         sensor_class,
@@ -248,29 +208,32 @@ class NexiaZoneSensor(NexiaSensor):
 
         super().__init__(
             coordinator,
-            device,
-            sensor_call,
-            sensor_name,
-            sensor_class,
-            sensor_unit,
-            modifier,
+            zone,
+            name=f"{zone.get_name()} {sensor_name}",
+            unique_id=f"{zone.zone_id}_{sensor_call}",
         )
-        self._device = device
+        self._call = sensor_call
+        self._class = sensor_class
+        self._state = None
+        self._unit_of_measurement = sensor_unit
+        self._modifier = modifier
 
     @property
-    def unique_id(self):
-        """Return the unique id of the sensor."""
-        # This is the zone unique_id
-        return f"{self._device.zone_id}_{self._call}"
+    def device_class(self):
+        """Return the device class of the sensor."""
+        return self._class
 
     @property
-    def device_info(self):
-        """Return the device_info of the device."""
-        return {
-            "identifiers": {(DOMAIN, self._device.zone_id)},
-            "name": self._device.get_name(),
-            "model": self._device.thermostat.get_model(),
-            "sw_version": self._device.thermostat.get_firmware(),
-            "manufacturer": MANUFACTURER,
-            "via_device": (DOMAIN, self._device.thermostat.thermostat_id),
-        }
+    def state(self):
+        """Return the state of the sensor."""
+        val = getattr(self._zone, self._call)()
+        if self._modifier:
+            val = self._modifier(val)
+        if isinstance(val, float):
+            val = round(val, 1)
+        return val
+
+    @property
+    def unit_of_measurement(self):
+        """Return the unit of measurement this sensor expresses itself in."""
+        return self._unit_of_measurement
diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml
new file mode 100644
index 00000000000..725b215da5a
--- /dev/null
+++ b/homeassistant/components/nexia/services.yaml
@@ -0,0 +1,19 @@
+set_aircleaner_mode:
+  description: "The air cleaner mode."
+  fields:
+    entity_id:
+      description: "This setting will affect all zones connected to the thermostat."
+      example: climate.master_bedroom
+    aircleaner_mode:
+      description: "The air cleaner mode to set. Options include \"auto\", \"quick\", or \"allergy\"."
+      example: allergy
+
+set_humidify_setpoint:
+  description: "The humidification set point."
+  fields:
+    entity_id:
+      description: "This setting will affect all zones connected to the thermostat."
+      example: climate.master_bedroom
+    humidity:
+      description: "The humidification setpoint as an int, range 35-65."
+      example: 45
\ No newline at end of file
diff --git a/homeassistant/components/nexia/util.py b/homeassistant/components/nexia/util.py
new file mode 100644
index 00000000000..d2ff10c8d34
--- /dev/null
+++ b/homeassistant/components/nexia/util.py
@@ -0,0 +1,6 @@
+"""Utils for Nexia / Trane XL Thermostats."""
+
+
+def percent_conv(val):
+    """Convert an actual percentage (0.0-1.0) to 0-100 scale."""
+    return round(val * 100.0, 1)
diff --git a/tests/components/nexia/test_binary_sensor.py b/tests/components/nexia/test_binary_sensor.py
new file mode 100644
index 00000000000..64b2946ee2f
--- /dev/null
+++ b/tests/components/nexia/test_binary_sensor.py
@@ -0,0 +1,35 @@
+"""The binary_sensor tests for the nexia platform."""
+
+from homeassistant.const import STATE_OFF, STATE_ON
+
+from .util import async_init_integration
+
+
+async def test_create_binary_sensors(hass):
+    """Test creation of binary sensors."""
+
+    await async_init_integration(hass)
+
+    state = hass.states.get("binary_sensor.master_suite_blower_active")
+    assert state.state == STATE_ON
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Master Suite Blower Active",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("binary_sensor.downstairs_east_wing_blower_active")
+    assert state.state == STATE_OFF
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Downstairs East Wing Blower Active",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py
index 327c611d277..e7675ff68b1 100644
--- a/tests/components/nexia/test_climate.py
+++ b/tests/components/nexia/test_climate.py
@@ -43,3 +43,38 @@ async def test_climate_zones(hass):
     assert all(
         state.attributes[key] == expected_attributes[key] for key in expected_attributes
     )
+
+    state = hass.states.get("climate.kitchen")
+    assert state.state == HVAC_MODE_HEAT_COOL
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "current_humidity": 36.0,
+        "current_temperature": 25.0,
+        "dehumidify_setpoint": 50.0,
+        "dehumidify_supported": True,
+        "fan_mode": "auto",
+        "fan_modes": ["auto", "on", "circulate"],
+        "friendly_name": "Kitchen",
+        "humidify_supported": False,
+        "humidity": 50.0,
+        "hvac_action": "idle",
+        "hvac_modes": ["off", "auto", "heat_cool", "heat", "cool"],
+        "max_humidity": 65.0,
+        "max_temp": 37.2,
+        "min_humidity": 35.0,
+        "min_temp": 12.8,
+        "preset_mode": "None",
+        "preset_modes": ["None", "Home", "Away", "Sleep"],
+        "supported_features": 31,
+        "target_temp_high": 26.1,
+        "target_temp_low": 17.2,
+        "target_temp_step": 1.0,
+        "temperature": None,
+        "zone_status": "",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
diff --git a/tests/components/nexia/test_scene.py b/tests/components/nexia/test_scene.py
index e6a5e94f083..4a325552e80 100644
--- a/tests/components/nexia/test_scene.py
+++ b/tests/components/nexia/test_scene.py
@@ -1,10 +1,10 @@
-"""The lock tests for the august platform."""
+"""The scene tests for the nexia platform."""
 
 from .util import async_init_integration
 
 
-async def test_automation_scenees(hass):
-    """Test creation automation scenees."""
+async def test_automation_scenes(hass):
+    """Test creation automation scenes."""
 
     await async_init_integration(hass)
 
diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py
new file mode 100644
index 00000000000..6e258d0ad55
--- /dev/null
+++ b/tests/components/nexia/test_sensor.py
@@ -0,0 +1,133 @@
+"""The sensor tests for the nexia platform."""
+
+from .util import async_init_integration
+
+
+async def test_create_sensors(hass):
+    """Test creation of sensors."""
+
+    await async_init_integration(hass)
+
+    state = hass.states.get("sensor.nick_office_temperature")
+    assert state.state == "23"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "device_class": "temperature",
+        "friendly_name": "Nick Office Temperature",
+        "unit_of_measurement": "°C",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.nick_office_zone_setpoint_status")
+    assert state.state == "Permanent Hold"
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Nick Office Zone Setpoint Status",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.nick_office_zone_status")
+    assert state.state == "Relieving Air"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Nick Office Zone Status",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.master_suite_air_cleaner_mode")
+    assert state.state == "auto"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Master Suite Air Cleaner Mode",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.master_suite_current_compressor_speed")
+    assert state.state == "69.0"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Master Suite Current Compressor Speed",
+        "unit_of_measurement": "%",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.master_suite_outdoor_temperature")
+    assert state.state == "30.6"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "device_class": "temperature",
+        "friendly_name": "Master Suite Outdoor Temperature",
+        "unit_of_measurement": "°C",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.master_suite_relative_humidity")
+    assert state.state == "52.0"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "device_class": "humidity",
+        "friendly_name": "Master Suite Relative Humidity",
+        "unit_of_measurement": "%",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.master_suite_requested_compressor_speed")
+    assert state.state == "69.0"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Master Suite Requested Compressor Speed",
+        "unit_of_measurement": "%",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
+
+    state = hass.states.get("sensor.master_suite_system_status")
+    assert state.state == "Cooling"
+
+    expected_attributes = {
+        "attribution": "Data provided by mynexia.com",
+        "friendly_name": "Master Suite System Status",
+    }
+    # Only test for a subset of attributes in case
+    # HA changes the implementation and a new one appears
+    assert all(
+        state.attributes[key] == expected_attributes[key] for key in expected_attributes
+    )
-- 
GitLab