Convert nuheat to use DataUpdateCoordinator (#42034)

* Convert nuheat to use DataUpdateCoordinator

* coverage

* Update homeassistant/components/nuheat/climate.py
This commit is contained in:
J. Nick Koston 2020-10-18 13:45:47 -05:00 committed by GitHub
parent 37df7bf4c5
commit 8c27a99386
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 81 additions and 28 deletions

View file

@ -1,5 +1,6 @@
"""Support for NuHeat thermostats.""" """Support for NuHeat thermostats."""
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import nuheat import nuheat
@ -17,6 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS
@ -68,6 +70,12 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True return True
def _get_thermostat(api, serial_number):
"""Authenticate and create the thermostat object."""
api.authenticate()
return api.get_thermostat(serial_number)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up NuHeat from a config entry.""" """Set up NuHeat from a config entry."""
@ -80,7 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
api = nuheat.NuHeat(username, password) api = nuheat.NuHeat(username, password)
try: try:
await hass.async_add_executor_job(api.authenticate) thermostat = await hass.async_add_executor_job(
_get_thermostat, api, serial_number
)
except requests.exceptions.Timeout as ex: except requests.exceptions.Timeout as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
except requests.exceptions.HTTPError as ex: except requests.exceptions.HTTPError as ex:
@ -95,7 +105,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
_LOGGER.error("Failed to login to nuheat: %s", ex) _LOGGER.error("Failed to login to nuheat: %s", ex)
return False return False
hass.data[DOMAIN][entry.entry_id] = (api, serial_number) async def _async_update_data():
"""Fetch data from API endpoint."""
try:
await hass.async_add_executor_job(thermostat.get_data)
except Exception as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"nuheat {serial_number}",
update_method=_async_update_data,
update_interval=timedelta(minutes=5),
)
hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator)
for component in PLATFORMS: for component in PLATFORMS:
hass.async_create_task( hass.async_create_task(

View file

@ -1,5 +1,5 @@
"""Support for NuHeat thermostats.""" """Support for NuHeat thermostats."""
from datetime import datetime, timedelta from datetime import datetime
import logging import logging
import time import time
@ -22,8 +22,9 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.helpers import event as event_helper from homeassistant.helpers import event as event_helper
from homeassistant.util import Throttle from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
DOMAIN, DOMAIN,
@ -38,7 +39,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
# The device does not have an off function. # The device does not have an off function.
# To turn it off set to min_temp and PRESET_PERMANENT_HOLD # To turn it off set to min_temp and PRESET_PERMANENT_HOLD
@ -65,11 +65,10 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the NuHeat thermostat(s).""" """Set up the NuHeat thermostat(s)."""
api, serial_number = hass.data[DOMAIN][config_entry.entry_id] thermostat, coordinator = hass.data[DOMAIN][config_entry.entry_id]
temperature_unit = hass.config.units.temperature_unit temperature_unit = hass.config.units.temperature_unit
thermostat = await hass.async_add_executor_job(api.get_thermostat, serial_number) entity = NuHeatThermostat(coordinator, thermostat, temperature_unit)
entity = NuHeatThermostat(thermostat, temperature_unit)
# No longer need a service as set_hvac_mode to auto does this # No longer need a service as set_hvac_mode to auto does this
# since climate 1.0 has been implemented # since climate 1.0 has been implemented
@ -77,16 +76,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([entity], True) async_add_entities([entity], True)
class NuHeatThermostat(ClimateEntity): class NuHeatThermostat(CoordinatorEntity, ClimateEntity):
"""Representation of a NuHeat Thermostat.""" """Representation of a NuHeat Thermostat."""
def __init__(self, thermostat, temperature_unit): def __init__(self, coordinator, thermostat, temperature_unit):
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__(coordinator)
self._thermostat = thermostat self._thermostat = thermostat
self._temperature_unit = temperature_unit self._temperature_unit = temperature_unit
self._schedule_mode = None self._schedule_mode = None
self._target_temperature = None self._target_temperature = None
self._force_update = False
@property @property
def name(self): def name(self):
@ -122,7 +121,7 @@ class NuHeatThermostat(ClimateEntity):
@property @property
def available(self): def available(self):
"""Return the unique id.""" """Return the unique id."""
return self._thermostat.online return self.coordinator.last_update_success and self._thermostat.online
def set_hvac_mode(self, hvac_mode): def set_hvac_mode(self, hvac_mode):
"""Set the system mode.""" """Set the system mode."""
@ -260,28 +259,30 @@ class NuHeatThermostat(ClimateEntity):
# in the future to make sure the change actually # in the future to make sure the change actually
# took effect # took effect
event_helper.call_later( event_helper.call_later(
self.hass, NUHEAT_API_STATE_SHIFT_DELAY, self._schedule_force_refresh self.hass, NUHEAT_API_STATE_SHIFT_DELAY, self._forced_refresh
) )
def _schedule_force_refresh(self, _): async def _forced_refresh(self, *_) -> None:
self._force_update = True """Force a refresh."""
self.schedule_update_ha_state(True) await self.coordinator.async_refresh()
def update(self): async def async_added_to_hass(self) -> None:
"""Get the latest state from the thermostat.""" """When entity is added to hass."""
if self._force_update: await super().async_added_to_hass()
self._throttled_update(no_throttle=True) self._update_internal_state()
self._force_update = False
else:
self._throttled_update()
@Throttle(MIN_TIME_BETWEEN_UPDATES) @callback
def _throttled_update(self, **kwargs): def _update_internal_state(self):
"""Get the latest state from the thermostat with a throttle.""" """Update our internal state from the last api response."""
self._thermostat.get_data()
self._schedule_mode = self._thermostat.schedule_mode self._schedule_mode = self._thermostat.schedule_mode
self._target_temperature = self._thermostat.target_temperature self._target_temperature = self._thermostat.target_temperature
@callback
def _handle_coordinator_update(self):
"""Get the latest state from the thermostat."""
self._update_internal_state()
self.async_write_ha_state()
@property @property
def device_info(self): def device_info(self):
"""Return the device_info of the device.""" """Return the device_info of the device."""

View file

@ -8,7 +8,7 @@ CONF_SERIAL_NUMBER = "serial_number"
MANUFACTURER = "NuHeat" MANUFACTURER = "NuHeat"
NUHEAT_API_STATE_SHIFT_DELAY = 1 NUHEAT_API_STATE_SHIFT_DELAY = 2
TEMP_HOLD_TIME_SEC = 43200 TEMP_HOLD_TIME_SEC = 43200

View file

@ -103,6 +103,8 @@ def _get_mock_thermostat_schedule_temporary_hold():
target_celsius=43, target_celsius=43,
target_fahrenheit=99, target_fahrenheit=99,
target_temperature=3729, target_temperature=3729,
max_temperature=5000,
min_temperature=1,
) )
thermostat.get_data = Mock() thermostat.get_data = Mock()

View file

@ -1,6 +1,10 @@
"""The test for the NuHeat thermostat module.""" """The test for the NuHeat thermostat module."""
from datetime import timedelta
from homeassistant.components.nuheat.const import DOMAIN from homeassistant.components.nuheat.const import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from .mocks import ( from .mocks import (
_get_mock_nuheat, _get_mock_nuheat,
@ -12,6 +16,7 @@ from .mocks import (
) )
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import async_fire_time_changed
async def test_climate_thermostat_run(hass): async def test_climate_thermostat_run(hass):
@ -135,3 +140,23 @@ async def test_climate_thermostat_schedule_temporary_hold(hass):
# Only test for a subset of attributes in case # Only test for a subset of attributes in case
# HA changes the implementation and a new one appears # HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items()) assert all(item in state.attributes.items() for item in expected_attributes.items())
await hass.services.async_call(
"climate",
"set_temperature",
service_data={ATTR_ENTITY_ID: "climate.temp_bathroom", "temperature": 90},
blocking=True,
)
await hass.async_block_till_done()
# opportunistic set
state = hass.states.get("climate.temp_bathroom")
assert state.attributes["preset_mode"] == "Temporary Hold"
assert state.attributes["temperature"] == 50.0
# and the api poll returns it to the mock
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
await hass.async_block_till_done()
state = hass.states.get("climate.temp_bathroom")
assert state.attributes["preset_mode"] == "Run Schedule"
assert state.attributes["temperature"] == 37.2