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."""
|
"""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(
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue