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
This commit is contained in:
J. Nick Koston 2020-03-23 11:01:48 -05:00 committed by GitHub
parent 0e3dc7976c
commit b8fdebd05c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 575 additions and 291 deletions

View File

@ -15,7 +15,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 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__) _LOGGER = logging.getLogger(__name__)
@ -94,8 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE),
) )
hass.data[DOMAIN][entry.entry_id] = {} hass.data[DOMAIN][entry.entry_id] = {
hass.data[DOMAIN][entry.entry_id][DATA_NEXIA] = {
NEXIA_DEVICE: nexia_home, NEXIA_DEVICE: nexia_home,
UPDATE_COORDINATOR: coordinator, UPDATE_COORDINATOR: coordinator,
} }

View File

@ -1,23 +1,15 @@
"""Support for Nexia / Trane XL Thermostats.""" """Support for Nexia / Trane XL Thermostats."""
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_ATTRIBUTION
from .const import ( from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR
ATTRIBUTION, from .entity import NexiaThermostatEntity
DATA_NEXIA,
DOMAIN,
MANUFACTURER,
NEXIA_DEVICE,
UPDATE_COORDINATOR,
)
from .entity import NexiaEntity
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sensors for a Nexia device.""" """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] nexia_home = nexia_data[NEXIA_DEVICE]
coordinator = nexia_data[UPDATE_COORDINATOR] 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) async_add_entities(entities, True)
class NexiaBinarySensor(NexiaEntity, BinarySensorDevice): class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorDevice):
"""Provices Nexia BinarySensor support.""" """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.""" """Initialize the nexia sensor."""
super().__init__(coordinator) super().__init__(
self._coordinator = coordinator coordinator,
self._device = device thermostat,
self._name = f"{self._device.get_name()} {sensor_name}" name=f"{thermostat.get_name()} {sensor_name}",
unique_id=f"{thermostat.thermostat_id}_{sensor_call}",
)
self._call = sensor_call self._call = sensor_call
self._unique_id = f"{self._device.thermostat_id}_{sensor_call}"
self._state = None 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 @property
def is_on(self): def is_on(self):
"""Return the status of the sensor.""" """Return the status of the sensor."""
return getattr(self._device, self._call)() return getattr(self._thermostat, self._call)()

View File

@ -12,9 +12,11 @@ from nexia.const import (
SYSTEM_STATUS_IDLE, SYSTEM_STATUS_IDLE,
UNIT_FAHRENHEIT, UNIT_FAHRENHEIT,
) )
import voluptuous as vol
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY, ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY, ATTR_MIN_HUMIDITY,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
@ -36,26 +38,50 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE_RANGE,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ENTITY_ID,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, 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 ( from .const import (
ATTR_AIRCLEANER_MODE,
ATTR_DEHUMIDIFY_SETPOINT, ATTR_DEHUMIDIFY_SETPOINT,
ATTR_DEHUMIDIFY_SUPPORTED, ATTR_DEHUMIDIFY_SUPPORTED,
ATTR_HUMIDIFY_SETPOINT, ATTR_HUMIDIFY_SETPOINT,
ATTR_HUMIDIFY_SUPPORTED, ATTR_HUMIDIFY_SUPPORTED,
ATTR_ZONE_STATUS, ATTR_ZONE_STATUS,
ATTRIBUTION,
DATA_NEXIA,
DOMAIN, DOMAIN,
MANUFACTURER,
NEXIA_DEVICE, NEXIA_DEVICE,
SIGNAL_THERMOSTAT_UPDATE,
SIGNAL_ZONE_UPDATE,
UPDATE_COORDINATOR, 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__) _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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up climate for a Nexia device.""" """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] nexia_home = nexia_data[NEXIA_DEVICE]
coordinator = nexia_data[UPDATE_COORDINATOR] 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 = [] entities = []
for thermostat_id in nexia_home.get_thermostat_ids(): for thermostat_id in nexia_home.get_thermostat_ids():
thermostat = nexia_home.get_thermostat_by_id(thermostat_id) 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) async_add_entities(entities, True)
class NexiaZone(NexiaEntity, ClimateDevice): class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice):
"""Provides Nexia Climate support.""" """Provides Nexia Climate support."""
def __init__(self, coordinator, device): def __init__(self, coordinator, zone):
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__(coordinator) super().__init__(
self.thermostat = device.thermostat coordinator, zone, name=zone.get_name(), unique_id=zone.zone_id
self._device = device )
self._coordinator = coordinator self._undo_humidfy_dispatcher = None
self._undo_aircleaner_dispatcher = None
# The has_* calls are stable for the life of the device # The has_* calls are stable for the life of the device
# and do not do I/O # and do not do I/O
self._has_relative_humidity = self.thermostat.has_relative_humidity() self._has_relative_humidity = self._thermostat.has_relative_humidity()
self._has_emergency_heat = self.thermostat.has_emergency_heat() self._has_emergency_heat = self._thermostat.has_emergency_heat()
self._has_humidify_support = self.thermostat.has_humidify_support() self._has_humidify_support = self._thermostat.has_humidify_support()
self._has_dehumidify_support = self.thermostat.has_dehumidify_support() self._has_dehumidify_support = self._thermostat.has_dehumidify_support()
@property
def unique_id(self):
"""Device Uniqueid."""
return self._device.zone_id
@property @property
def supported_features(self): def supported_features(self):
@ -139,27 +172,22 @@ class NexiaZone(NexiaEntity, ClimateDevice):
@property @property
def is_fan_on(self): def is_fan_on(self):
"""Blower is on.""" """Blower is on."""
return self.thermostat.is_blower_active() return self._thermostat.is_blower_active()
@property
def name(self):
"""Name of the zone."""
return self._device.get_name()
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """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 @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self._device.get_temperature() return self._zone.get_temperature()
@property @property
def fan_mode(self): def fan_mode(self):
"""Return the fan setting.""" """Return the fan setting."""
return self.thermostat.get_fan_mode() return self._thermostat.get_fan_mode()
@property @property
def fan_modes(self): def fan_modes(self):
@ -169,92 +197,92 @@ class NexiaZone(NexiaEntity, ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Minimum temp for the current setting.""" """Minimum temp for the current setting."""
return (self._device.thermostat.get_setpoint_limits())[0] return (self._thermostat.get_setpoint_limits())[0]
@property @property
def max_temp(self): def max_temp(self):
"""Maximum temp for the current setting.""" """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): def set_fan_mode(self, fan_mode):
"""Set new target fan mode.""" """Set new target fan mode."""
self.thermostat.set_fan_mode(fan_mode) self._thermostat.set_fan_mode(fan_mode)
self.schedule_update_ha_state() self._signal_thermostat_update()
@property @property
def preset_mode(self): def preset_mode(self):
"""Preset that is active.""" """Preset that is active."""
return self._device.get_preset() return self._zone.get_preset()
@property @property
def preset_modes(self): def preset_modes(self):
"""All presets.""" """All presets."""
return self._device.get_presets() return self._zone.get_presets()
def set_humidity(self, humidity): def set_humidity(self, humidity):
"""Dehumidify target.""" """Dehumidify target."""
self.thermostat.set_dehumidify_setpoint(humidity / 100.0) self._thermostat.set_dehumidify_setpoint(humidity / 100.0)
self.schedule_update_ha_state() self._signal_thermostat_update()
@property @property
def target_humidity(self): def target_humidity(self):
"""Humidity indoors setpoint.""" """Humidity indoors setpoint."""
if self._has_dehumidify_support: 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: 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 return None
@property @property
def current_humidity(self): def current_humidity(self):
"""Humidity indoors.""" """Humidity indoors."""
if self._has_relative_humidity: 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 return None
@property @property
def target_temperature(self): def target_temperature(self):
"""Temperature we try to reach.""" """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: if current_mode == OPERATION_MODE_COOL:
return self._device.get_cooling_setpoint() return self._zone.get_cooling_setpoint()
if current_mode == OPERATION_MODE_HEAT: if current_mode == OPERATION_MODE_HEAT:
return self._device.get_heating_setpoint() return self._zone.get_heating_setpoint()
return None return None
@property @property
def target_temperature_step(self): def target_temperature_step(self):
"""Step size of temperature units.""" """Step size of temperature units."""
if self._device.thermostat.get_unit() == UNIT_FAHRENHEIT: if self._thermostat.get_unit() == UNIT_FAHRENHEIT:
return 1.0 return 1.0
return 0.5 return 0.5
@property @property
def target_temperature_high(self): def target_temperature_high(self):
"""Highest temperature we are trying to reach.""" """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): if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
return None return None
return self._device.get_cooling_setpoint() return self._zone.get_cooling_setpoint()
@property @property
def target_temperature_low(self): def target_temperature_low(self):
"""Lowest temperature we are trying to reach.""" """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): if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
return None return None
return self._device.get_heating_setpoint() return self._zone.get_heating_setpoint()
@property @property
def hvac_action(self) -> str: def hvac_action(self) -> str:
"""Operation ie. heat, cool, idle.""" """Operation ie. heat, cool, idle."""
system_status = self.thermostat.get_system_status() system_status = self._thermostat.get_system_status()
zone_called = self._device.is_calling() 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 return CURRENT_HVAC_OFF
if not zone_called: if not zone_called:
return CURRENT_HVAC_IDLE return CURRENT_HVAC_IDLE
@ -269,8 +297,8 @@ class NexiaZone(NexiaEntity, ClimateDevice):
@property @property
def hvac_mode(self): def hvac_mode(self):
"""Return current mode, as the user-visible name.""" """Return current mode, as the user-visible name."""
mode = self._device.get_requested_mode() mode = self._zone.get_requested_mode()
hold = self._device.is_in_permanent_hold() hold = self._zone.is_in_permanent_hold()
# If the device is in hold mode with # If the device is in hold mode with
# OPERATION_MODE_AUTO # OPERATION_MODE_AUTO
@ -299,10 +327,10 @@ class NexiaZone(NexiaEntity, ClimateDevice):
new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH, None) new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH, None)
set_temp = kwargs.get(ATTR_TEMPERATURE, None) set_temp = kwargs.get(ATTR_TEMPERATURE, None)
deadband = self.thermostat.get_deadband() deadband = self._thermostat.get_deadband()
cur_cool_temp = self._device.get_cooling_setpoint() cur_cool_temp = self._zone.get_cooling_setpoint()
cur_heat_temp = self._device.get_heating_setpoint() cur_heat_temp = self._zone.get_heating_setpoint()
(min_temp, max_temp) = self.thermostat.get_setpoint_limits() (min_temp, max_temp) = self._thermostat.get_setpoint_limits()
# Check that we're not going to hit any minimum or maximum values # Check that we're not going to hit any minimum or maximum values
if new_heat_temp and new_heat_temp + deadband > max_temp: 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: if new_cool_temp - new_heat_temp < deadband:
new_heat_temp = new_cool_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, heat_temperature=new_heat_temp,
cool_temperature=new_cool_temp, cool_temperature=new_cool_temp,
set_temperature=set_temp, set_temperature=set_temp,
) )
self.schedule_update_ha_state() self._signal_zone_update()
@property @property
def is_aux_heat(self): def is_aux_heat(self):
"""Emergency heat state.""" """Emergency heat state."""
return self.thermostat.is_emergency_heat_active() 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),
}
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
data = { data = super().device_state_attributes
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_ZONE_STATUS: self._device.get_status(),
}
if self._has_relative_humidity: data[ATTR_ZONE_STATUS] = self._zone.get_status()
data.update(
{ if not self._has_relative_humidity:
ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support, return data
ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support,
ATTR_MIN_HUMIDITY: round( min_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[0])
self.thermostat.get_humidity_setpoint_limits()[0] * 100.0, 1, max_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[1])
), data.update(
ATTR_MAX_HUMIDITY: round( {
self.thermostat.get_humidity_setpoint_limits()[1] * 100.0, 1, 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_dehumidify_support:
dehumdify_setpoint = percent_conv(
self._thermostat.get_dehumidify_setpoint()
) )
if self._has_dehumidify_support: data[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint
data.update(
{ if self._has_humidify_support:
ATTR_DEHUMIDIFY_SETPOINT: round( humdify_setpoint = percent_conv(self._thermostat.get_humidify_setpoint())
self.thermostat.get_dehumidify_setpoint() * 100.0, 1 data[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint
),
}
)
if self._has_humidify_support:
data.update(
{
ATTR_HUMIDIFY_SETPOINT: round(
self.thermostat.get_humidify_setpoint() * 100.0, 1
)
}
)
return data return data
def set_preset_mode(self, preset_mode: str): def set_preset_mode(self, preset_mode: str):
"""Set the preset mode.""" """Set the preset mode."""
self._device.set_preset(preset_mode) self._zone.set_preset(preset_mode)
self.schedule_update_ha_state() self._signal_zone_update()
def turn_aux_heat_off(self): def turn_aux_heat_off(self):
"""Turn. Aux Heat off.""" """Turn. Aux Heat off."""
self.thermostat.set_emergency_heat(False) self._thermostat.set_emergency_heat(False)
self.schedule_update_ha_state() self._signal_thermostat_update()
def turn_aux_heat_on(self): def turn_aux_heat_on(self):
"""Turn. Aux Heat on.""" """Turn. Aux Heat on."""
self.thermostat.set_emergency_heat(True) self._thermostat.set_emergency_heat(True)
self.schedule_update_ha_state() self._signal_thermostat_update()
def turn_off(self): def turn_off(self):
"""Turn. off the zone.""" """Turn. off the zone."""
self.set_hvac_mode(OPERATION_MODE_OFF) self.set_hvac_mode(OPERATION_MODE_OFF)
self.schedule_update_ha_state() self._signal_zone_update()
def turn_on(self): def turn_on(self):
"""Turn. on the zone.""" """Turn. on the zone."""
self.set_hvac_mode(OPERATION_MODE_AUTO) 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: def set_hvac_mode(self, hvac_mode: str) -> None:
"""Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc)."""
if hvac_mode == HVAC_MODE_AUTO: if hvac_mode == HVAC_MODE_AUTO:
self._device.call_return_to_schedule() self._zone.call_return_to_schedule()
self._device.set_mode(mode=OPERATION_MODE_AUTO) self._zone.set_mode(mode=OPERATION_MODE_AUTO)
else: else:
self._device.call_permanent_hold() self._zone.call_permanent_hold()
self._device.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode])
self.schedule_update_ha_state() self.schedule_update_ha_state()
def set_aircleaner_mode(self, aircleaner_mode): def set_aircleaner_mode(self, aircleaner_mode):
"""Set the aircleaner mode.""" """Set the aircleaner mode."""
self.thermostat.set_air_cleaner(aircleaner_mode) self._thermostat.set_air_cleaner(aircleaner_mode)
self.schedule_update_ha_state() self._signal_thermostat_update()
def set_humidify_setpoint(self, humidify_setpoint): def set_humidify_setpoint(self, humidity):
"""Set the humidify setpoint.""" """Set the humidify setpoint."""
self.thermostat.set_humidify_setpoint(humidify_setpoint / 100.0) self._thermostat.set_humidify_setpoint(humidity / 100.0)
self.schedule_update_ha_state() 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): async def async_update(self):
"""Update the entity. """Update the entity.

View File

@ -7,7 +7,6 @@ ATTRIBUTION = "Data provided by mynexia.com"
NOTIFICATION_ID = "nexia_notification" NOTIFICATION_ID = "nexia_notification"
NOTIFICATION_TITLE = "Nexia Setup" NOTIFICATION_TITLE = "Nexia Setup"
DATA_NEXIA = "nexia"
NEXIA_DEVICE = "device" NEXIA_DEVICE = "device"
NEXIA_SCAN_INTERVAL = "scan_interval" NEXIA_SCAN_INTERVAL = "scan_interval"
@ -16,6 +15,8 @@ DEFAULT_ENTITY_NAMESPACE = "nexia"
ATTR_DESCRIPTION = "description" ATTR_DESCRIPTION = "description"
ATTR_AIRCLEANER_MODE = "aircleaner_mode"
ATTR_ZONE_STATUS = "zone_status" ATTR_ZONE_STATUS = "zone_status"
ATTR_HUMIDIFY_SUPPORTED = "humidify_supported" ATTR_HUMIDIFY_SUPPORTED = "humidify_supported"
ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported"
@ -24,5 +25,7 @@ ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint"
UPDATE_COORDINATOR = "update_coordinator" UPDATE_COORDINATOR = "update_coordinator"
MANUFACTURER = "Trane" MANUFACTURER = "Trane"
SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE"
SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE"

View File

@ -1,14 +1,26 @@
"""The nexia integration base entity.""" """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 homeassistant.helpers.entity import Entity
from .const import (
ATTRIBUTION,
DOMAIN,
MANUFACTURER,
SIGNAL_THERMOSTAT_UPDATE,
SIGNAL_ZONE_UPDATE,
)
class NexiaEntity(Entity): class NexiaEntity(Entity):
"""Base class for nexia entities.""" """Base class for nexia entities."""
def __init__(self, coordinator): def __init__(self, coordinator, name, unique_id):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__() super().__init__()
self._unique_id = unique_id
self._name = name
self._coordinator = coordinator self._coordinator = coordinator
@property @property
@ -16,6 +28,23 @@ class NexiaEntity(Entity):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._coordinator.last_update_success 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 @property
def should_poll(self): def should_poll(self):
"""Return False, updates are controlled via coordinator.""" """Return False, updates are controlled via coordinator."""
@ -28,3 +57,77 @@ class NexiaEntity(Entity):
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Undo subscription.""" """Undo subscription."""
self._coordinator.async_remove_listener(self.async_write_ha_state) 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()

View File

@ -1,23 +1,18 @@
"""Support for Nexia Automations.""" """Support for Nexia Automations."""
from homeassistant.components.scene import Scene from homeassistant.components.scene import Scene
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.event import async_call_later
from .const import ( from .const import ATTR_DESCRIPTION, DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR
ATTR_DESCRIPTION,
ATTRIBUTION,
DATA_NEXIA,
DOMAIN,
NEXIA_DEVICE,
UPDATE_COORDINATOR,
)
from .entity import NexiaEntity from .entity import NexiaEntity
SCENE_ACTIVATION_TIME = 5
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up automations for a Nexia device.""" """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] nexia_home = nexia_data[NEXIA_DEVICE]
coordinator = nexia_data[UPDATE_COORDINATOR] coordinator = nexia_data[UPDATE_COORDINATOR]
entities = [] entities = []
@ -36,33 +31,28 @@ class NexiaAutomationScene(NexiaEntity, Scene):
def __init__(self, coordinator, automation): def __init__(self, coordinator, automation):
"""Initialize the automation scene.""" """Initialize the automation scene."""
super().__init__(coordinator) super().__init__(
coordinator, name=automation.name, unique_id=automation.automation_id,
)
self._automation = automation 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 @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the scene specific state attributes.""" """Return the scene specific state attributes."""
return { data = super().device_state_attributes
ATTR_ATTRIBUTION: ATTRIBUTION, data[ATTR_DESCRIPTION] = self._automation.description
ATTR_DESCRIPTION: self._automation.description, return data
}
@property @property
def icon(self): def icon(self):
"""Return the icon of the automation scene.""" """Return the icon of the automation scene."""
return "mdi:script-text-outline" return "mdi:script-text-outline"
def activate(self): async def async_activate(self):
"""Activate an automation scene.""" """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)

View File

@ -3,7 +3,6 @@
from nexia.const import UNIT_CELSIUS from nexia.const import UNIT_CELSIUS
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -11,21 +10,15 @@ from homeassistant.const import (
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
) )
from .const import ( from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR
ATTRIBUTION, from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity
DATA_NEXIA, from .util import percent_conv
DOMAIN,
MANUFACTURER,
NEXIA_DEVICE,
UPDATE_COORDINATOR,
)
from .entity import NexiaEntity
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sensors for a Nexia device.""" """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] nexia_home = nexia_data[NEXIA_DEVICE]
coordinator = nexia_data[UPDATE_COORDINATOR] coordinator = nexia_data[UPDATE_COORDINATOR]
entities = [] 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) thermostat = nexia_home.get_thermostat_by_id(thermostat_id)
entities.append( entities.append(
NexiaSensor( NexiaThermostatSensor(
coordinator, coordinator,
thermostat, thermostat,
"get_system_status", "get_system_status",
@ -46,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
# Air cleaner # Air cleaner
entities.append( entities.append(
NexiaSensor( NexiaThermostatSensor(
coordinator, coordinator,
thermostat, thermostat,
"get_air_cleaner_mode", "get_air_cleaner_mode",
@ -58,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
# Compressor Speed # Compressor Speed
if thermostat.has_variable_speed_compressor(): if thermostat.has_variable_speed_compressor():
entities.append( entities.append(
NexiaSensor( NexiaThermostatSensor(
coordinator, coordinator,
thermostat, thermostat,
"get_current_compressor_speed", "get_current_compressor_speed",
@ -69,7 +62,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
) )
entities.append( entities.append(
NexiaSensor( NexiaThermostatSensor(
coordinator, coordinator,
thermostat, thermostat,
"get_requested_compressor_speed", "get_requested_compressor_speed",
@ -87,7 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
else TEMP_FAHRENHEIT else TEMP_FAHRENHEIT
) )
entities.append( entities.append(
NexiaSensor( NexiaThermostatSensor(
coordinator, coordinator,
thermostat, thermostat,
"get_outdoor_temperature", "get_outdoor_temperature",
@ -99,7 +92,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
# Relative Humidity # Relative Humidity
if thermostat.has_relative_humidity(): if thermostat.has_relative_humidity():
entities.append( entities.append(
NexiaSensor( NexiaThermostatSensor(
coordinator, coordinator,
thermostat, thermostat,
"get_relative_humidity", "get_relative_humidity",
@ -120,7 +113,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
# Temperature # Temperature
entities.append( entities.append(
NexiaZoneSensor( NexiaThermostatZoneSensor(
coordinator, coordinator,
zone, zone,
"get_temperature", "get_temperature",
@ -132,13 +125,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
# Zone Status # Zone Status
entities.append( entities.append(
NexiaZoneSensor( NexiaThermostatZoneSensor(
coordinator, zone, "get_status", "Zone Status", None, None, coordinator, zone, "get_status", "Zone Status", None, None,
) )
) )
# Setpoint Status # Setpoint Status
entities.append( entities.append(
NexiaZoneSensor( NexiaThermostatZoneSensor(
coordinator, coordinator,
zone, zone,
"get_setpoint_status", "get_setpoint_status",
@ -151,18 +144,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True) async_add_entities(entities, True)
def percent_conv(val): class NexiaThermostatSensor(NexiaThermostatEntity):
"""Convert an actual percentage (0.0-1.0) to 0-100 scale."""
return val * 100.0
class NexiaSensor(NexiaEntity):
"""Provides Nexia thermostat sensor support.""" """Provides Nexia thermostat sensor support."""
def __init__( def __init__(
self, self,
coordinator, coordinator,
device, thermostat,
sensor_call, sensor_call,
sensor_name, sensor_name,
sensor_class, sensor_class,
@ -170,35 +158,18 @@ class NexiaSensor(NexiaEntity):
modifier=None, modifier=None,
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(
self._coordinator = coordinator coordinator,
self._device = device thermostat,
name=f"{thermostat.get_name()} {sensor_name}",
unique_id=f"{thermostat.thermostat_id}_{sensor_call}",
)
self._call = sensor_call self._call = sensor_call
self._sensor_name = sensor_name
self._class = sensor_class self._class = sensor_class
self._state = None self._state = None
self._name = f"{self._device.get_name()} {self._sensor_name}"
self._unit_of_measurement = sensor_unit self._unit_of_measurement = sensor_unit
self._modifier = modifier 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 @property
def device_class(self): def device_class(self):
"""Return the device class of the sensor.""" """Return the device class of the sensor."""
@ -207,7 +178,7 @@ class NexiaSensor(NexiaEntity):
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
val = getattr(self._device, self._call)() val = getattr(self._thermostat, self._call)()
if self._modifier: if self._modifier:
val = self._modifier(val) val = self._modifier(val)
if isinstance(val, float): if isinstance(val, float):
@ -219,25 +190,14 @@ class NexiaSensor(NexiaEntity):
"""Return the unit of measurement this sensor expresses itself in.""" """Return the unit of measurement this sensor expresses itself in."""
return self._unit_of_measurement 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 NexiaThermostatZoneSensor(NexiaThermostatZoneEntity):
class NexiaZoneSensor(NexiaSensor):
"""Nexia Zone Sensor Support.""" """Nexia Zone Sensor Support."""
def __init__( def __init__(
self, self,
coordinator, coordinator,
device, zone,
sensor_call, sensor_call,
sensor_name, sensor_name,
sensor_class, sensor_class,
@ -248,29 +208,32 @@ class NexiaZoneSensor(NexiaSensor):
super().__init__( super().__init__(
coordinator, coordinator,
device, zone,
sensor_call, name=f"{zone.get_name()} {sensor_name}",
sensor_name, unique_id=f"{zone.zone_id}_{sensor_call}",
sensor_class,
sensor_unit,
modifier,
) )
self._device = device self._call = sensor_call
self._class = sensor_class
self._state = None
self._unit_of_measurement = sensor_unit
self._modifier = modifier
@property @property
def unique_id(self): def device_class(self):
"""Return the unique id of the sensor.""" """Return the device class of the sensor."""
# This is the zone unique_id return self._class
return f"{self._device.zone_id}_{self._call}"
@property @property
def device_info(self): def state(self):
"""Return the device_info of the device.""" """Return the state of the sensor."""
return { val = getattr(self._zone, self._call)()
"identifiers": {(DOMAIN, self._device.zone_id)}, if self._modifier:
"name": self._device.get_name(), val = self._modifier(val)
"model": self._device.thermostat.get_model(), if isinstance(val, float):
"sw_version": self._device.thermostat.get_firmware(), val = round(val, 1)
"manufacturer": MANUFACTURER, return val
"via_device": (DOMAIN, self._device.thermostat.thermostat_id),
} @property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
return self._unit_of_measurement

View File

@ -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

View File

@ -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)

View File

@ -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
)

View File

@ -43,3 +43,38 @@ async def test_climate_zones(hass):
assert all( assert all(
state.attributes[key] == expected_attributes[key] for key in expected_attributes 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
)

View File

@ -1,10 +1,10 @@
"""The lock tests for the august platform.""" """The scene tests for the nexia platform."""
from .util import async_init_integration from .util import async_init_integration
async def test_automation_scenees(hass): async def test_automation_scenes(hass):
"""Test creation automation scenees.""" """Test creation automation scenes."""
await async_init_integration(hass) await async_init_integration(hass)

View File

@ -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
)