Convert nuheat to use DataUpdateCoordinator (#42034)
* Convert nuheat to use DataUpdateCoordinator * coverage * Update homeassistant/components/nuheat/climate.py
This commit is contained in:
parent
37df7bf4c5
commit
8c27a99386
5 changed files with 81 additions and 28 deletions
|
@ -1,5 +1,6 @@
|
|||
"""Support for NuHeat thermostats."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import nuheat
|
||||
|
@ -17,6 +18,7 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS
|
||||
|
||||
|
@ -68,6 +70,12 @@ async def async_setup(hass: HomeAssistant, config: dict):
|
|||
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):
|
||||
"""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)
|
||||
|
||||
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:
|
||||
raise ConfigEntryNotReady from 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)
|
||||
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:
|
||||
hass.async_create_task(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Support for NuHeat thermostats."""
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
@ -22,8 +22,9 @@ from homeassistant.components.climate.const import (
|
|||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
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.util import Throttle
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
|
@ -38,7 +39,6 @@ from .const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
# The device does not have an off function.
|
||||
# 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):
|
||||
"""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
|
||||
thermostat = await hass.async_add_executor_job(api.get_thermostat, serial_number)
|
||||
entity = NuHeatThermostat(thermostat, temperature_unit)
|
||||
entity = NuHeatThermostat(coordinator, thermostat, temperature_unit)
|
||||
|
||||
# No longer need a service as set_hvac_mode to auto does this
|
||||
# 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)
|
||||
|
||||
|
||||
class NuHeatThermostat(ClimateEntity):
|
||||
class NuHeatThermostat(CoordinatorEntity, ClimateEntity):
|
||||
"""Representation of a NuHeat Thermostat."""
|
||||
|
||||
def __init__(self, thermostat, temperature_unit):
|
||||
def __init__(self, coordinator, thermostat, temperature_unit):
|
||||
"""Initialize the thermostat."""
|
||||
super().__init__(coordinator)
|
||||
self._thermostat = thermostat
|
||||
self._temperature_unit = temperature_unit
|
||||
self._schedule_mode = None
|
||||
self._target_temperature = None
|
||||
self._force_update = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -122,7 +121,7 @@ class NuHeatThermostat(ClimateEntity):
|
|||
@property
|
||||
def available(self):
|
||||
"""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):
|
||||
"""Set the system mode."""
|
||||
|
@ -260,28 +259,30 @@ class NuHeatThermostat(ClimateEntity):
|
|||
# in the future to make sure the change actually
|
||||
# took effect
|
||||
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, _):
|
||||
self._force_update = True
|
||||
self.schedule_update_ha_state(True)
|
||||
async def _forced_refresh(self, *_) -> None:
|
||||
"""Force a refresh."""
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self._force_update:
|
||||
self._throttled_update(no_throttle=True)
|
||||
self._force_update = False
|
||||
else:
|
||||
self._throttled_update()
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._update_internal_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _throttled_update(self, **kwargs):
|
||||
"""Get the latest state from the thermostat with a throttle."""
|
||||
self._thermostat.get_data()
|
||||
@callback
|
||||
def _update_internal_state(self):
|
||||
"""Update our internal state from the last api response."""
|
||||
self._schedule_mode = self._thermostat.schedule_mode
|
||||
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
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
|
|
|
@ -8,7 +8,7 @@ CONF_SERIAL_NUMBER = "serial_number"
|
|||
|
||||
MANUFACTURER = "NuHeat"
|
||||
|
||||
NUHEAT_API_STATE_SHIFT_DELAY = 1
|
||||
NUHEAT_API_STATE_SHIFT_DELAY = 2
|
||||
|
||||
TEMP_HOLD_TIME_SEC = 43200
|
||||
|
||||
|
|
|
@ -103,6 +103,8 @@ def _get_mock_thermostat_schedule_temporary_hold():
|
|||
target_celsius=43,
|
||||
target_fahrenheit=99,
|
||||
target_temperature=3729,
|
||||
max_temperature=5000,
|
||||
min_temperature=1,
|
||||
)
|
||||
|
||||
thermostat.get_data = Mock()
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"""The test for the NuHeat thermostat module."""
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.nuheat.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .mocks import (
|
||||
_get_mock_nuheat,
|
||||
|
@ -12,6 +16,7 @@ from .mocks import (
|
|||
)
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
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
|
||||
# HA changes the implementation and a new one appears
|
||||
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
|
||||
|
|
Loading…
Add table
Reference in a new issue