Skip to content

Commit 9373bb2

Browse files
Huum - Introduce coordinator to support multiple platforms (home-assistant#148889)
Co-authored-by: Josef Zweck <[email protected]>
1 parent d72fb02 commit 9373bb2

File tree

12 files changed

+403
-140
lines changed

12 files changed

+403
-140
lines changed

CODEOWNERS

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

homeassistant/components/huum/__init__.py

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,28 @@
22

33
from __future__ import annotations
44

5-
import logging
6-
7-
from huum.exceptions import Forbidden, NotAuthenticated
8-
from huum.huum import Huum
9-
10-
from homeassistant.config_entries import ConfigEntry
11-
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
125
from homeassistant.core import HomeAssistant
13-
from homeassistant.exceptions import ConfigEntryNotReady
14-
from homeassistant.helpers.aiohttp_client import async_get_clientsession
15-
16-
from .const import DOMAIN, PLATFORMS
176

18-
_LOGGER = logging.getLogger(__name__)
7+
from .const import PLATFORMS
8+
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
199

2010

21-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
11+
async def async_setup_entry(hass: HomeAssistant, config_entry: HuumConfigEntry) -> bool:
2212
"""Set up Huum from a config entry."""
23-
username = entry.data[CONF_USERNAME]
24-
password = entry.data[CONF_PASSWORD]
13+
coordinator = HuumDataUpdateCoordinator(
14+
hass=hass,
15+
config_entry=config_entry,
16+
)
2517

26-
huum = Huum(username, password, session=async_get_clientsession(hass))
18+
await coordinator.async_config_entry_first_refresh()
19+
config_entry.runtime_data = coordinator
2720

28-
try:
29-
await huum.status()
30-
except (Forbidden, NotAuthenticated) as err:
31-
_LOGGER.error("Could not log in to Huum with given credentials")
32-
raise ConfigEntryNotReady(
33-
"Could not log in to Huum with given credentials"
34-
) from err
35-
36-
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = huum
37-
38-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
21+
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
3922
return True
4023

4124

42-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
25+
async def async_unload_entry(
26+
hass: HomeAssistant, config_entry: HuumConfigEntry
27+
) -> bool:
4328
"""Unload a config entry."""
44-
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
45-
hass.data[DOMAIN].pop(entry.entry_id)
46-
47-
return unload_ok
29+
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

homeassistant/components/huum/climate.py

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,35 @@
77

88
from huum.const import SaunaStatus
99
from huum.exceptions import SafetyException
10-
from huum.huum import Huum
11-
from huum.schemas import HuumStatusResponse
1210

1311
from homeassistant.components.climate import (
1412
ClimateEntity,
1513
ClimateEntityFeature,
1614
HVACMode,
1715
)
18-
from homeassistant.config_entries import ConfigEntry
1916
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
2017
from homeassistant.core import HomeAssistant
2118
from homeassistant.exceptions import HomeAssistantError
2219
from homeassistant.helpers.device_registry import DeviceInfo
2320
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
21+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
2422

2523
from .const import DOMAIN
24+
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
2625

2726
_LOGGER = logging.getLogger(__name__)
2827

2928

3029
async def async_setup_entry(
3130
hass: HomeAssistant,
32-
entry: ConfigEntry,
31+
entry: HuumConfigEntry,
3332
async_add_entities: AddConfigEntryEntitiesCallback,
3433
) -> None:
3534
"""Set up the Huum sauna with config flow."""
36-
huum_handler = hass.data.setdefault(DOMAIN, {})[entry.entry_id]
35+
async_add_entities([HuumDevice(entry.runtime_data)])
3736

38-
async_add_entities([HuumDevice(huum_handler, entry.entry_id)], True)
3937

40-
41-
class HuumDevice(ClimateEntity):
38+
class HuumDevice(CoordinatorEntity[HuumDataUpdateCoordinator], ClimateEntity):
4239
"""Representation of a heater."""
4340

4441
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
@@ -54,24 +51,22 @@ class HuumDevice(ClimateEntity):
5451
_attr_has_entity_name = True
5552
_attr_name = None
5653

57-
_target_temperature: int | None = None
58-
_status: HuumStatusResponse | None = None
59-
60-
def __init__(self, huum_handler: Huum, unique_id: str) -> None:
54+
def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None:
6155
"""Initialize the heater."""
62-
self._attr_unique_id = unique_id
56+
super().__init__(coordinator)
57+
58+
self._attr_unique_id = coordinator.config_entry.entry_id
6359
self._attr_device_info = DeviceInfo(
64-
identifiers={(DOMAIN, unique_id)},
60+
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
6561
name="Huum sauna",
6662
manufacturer="Huum",
63+
model="UKU WiFi",
6764
)
6865

69-
self._huum_handler = huum_handler
70-
7166
@property
7267
def hvac_mode(self) -> HVACMode:
7368
"""Return hvac operation ie. heat, cool mode."""
74-
if self._status and self._status.status == SaunaStatus.ONLINE_HEATING:
69+
if self.coordinator.data.status == SaunaStatus.ONLINE_HEATING:
7570
return HVACMode.HEAT
7671
return HVACMode.OFF
7772

@@ -85,41 +80,33 @@ def icon(self) -> str:
8580
@property
8681
def current_temperature(self) -> int | None:
8782
"""Return the current temperature."""
88-
if (status := self._status) is not None:
89-
return status.temperature
90-
return None
83+
return self.coordinator.data.temperature
9184

9285
@property
9386
def target_temperature(self) -> int:
9487
"""Return the temperature we try to reach."""
95-
return self._target_temperature or int(self.min_temp)
88+
return self.coordinator.data.target_temperature or int(self.min_temp)
9689

9790
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
9891
"""Set hvac mode."""
9992
if hvac_mode == HVACMode.HEAT:
10093
await self._turn_on(self.target_temperature)
10194
elif hvac_mode == HVACMode.OFF:
102-
await self._huum_handler.turn_off()
95+
await self.coordinator.huum.turn_off()
96+
await self.coordinator.async_refresh()
10397

10498
async def async_set_temperature(self, **kwargs: Any) -> None:
10599
"""Set new target temperature."""
106100
temperature = kwargs.get(ATTR_TEMPERATURE)
107-
if temperature is None:
101+
if temperature is None or self.hvac_mode != HVACMode.HEAT:
108102
return
109-
self._target_temperature = temperature
110-
111-
if self.hvac_mode == HVACMode.HEAT:
112-
await self._turn_on(temperature)
113103

114-
async def async_update(self) -> None:
115-
"""Get the latest status data."""
116-
self._status = await self._huum_handler.status()
117-
if self._target_temperature is None or self.hvac_mode == HVACMode.HEAT:
118-
self._target_temperature = self._status.target_temperature
104+
await self._turn_on(temperature)
105+
await self.coordinator.async_refresh()
119106

120107
async def _turn_on(self, temperature: int) -> None:
121108
try:
122-
await self._huum_handler.turn_on(temperature)
109+
await self.coordinator.huum.turn_on(temperature)
123110
except (ValueError, SafetyException) as err:
124111
_LOGGER.error(str(err))
125112
raise HomeAssistantError(f"Unable to turn on sauna: {err}") from err

homeassistant/components/huum/config_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ async def async_step_user(
3737
errors = {}
3838
if user_input is not None:
3939
try:
40-
huum_handler = Huum(
40+
huum = Huum(
4141
user_input[CONF_USERNAME],
4242
user_input[CONF_PASSWORD],
4343
session=async_get_clientsession(self.hass),
4444
)
45-
await huum_handler.status()
45+
await huum.status()
4646
except (Forbidden, NotAuthenticated):
4747
# Most likely Forbidden as that is what is returned from `.status()` with bad creds
4848
_LOGGER.error("Could not log in to Huum with given credentials")
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""DataUpdateCoordinator for Huum."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import timedelta
6+
import logging
7+
8+
from huum.exceptions import Forbidden, NotAuthenticated
9+
from huum.huum import Huum
10+
from huum.schemas import HuumStatusResponse
11+
12+
from homeassistant.config_entries import ConfigEntry
13+
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
14+
from homeassistant.core import HomeAssistant
15+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
16+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
17+
18+
from .const import DOMAIN
19+
20+
type HuumConfigEntry = ConfigEntry[HuumDataUpdateCoordinator]
21+
22+
_LOGGER = logging.getLogger(__name__)
23+
UPDATE_INTERVAL = timedelta(seconds=30)
24+
25+
26+
class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]):
27+
"""Class to manage fetching data from the API."""
28+
29+
config_entry: HuumConfigEntry
30+
31+
def __init__(
32+
self,
33+
hass: HomeAssistant,
34+
config_entry: HuumConfigEntry,
35+
) -> None:
36+
"""Initialize."""
37+
super().__init__(
38+
hass=hass,
39+
logger=_LOGGER,
40+
name=DOMAIN,
41+
update_interval=UPDATE_INTERVAL,
42+
config_entry=config_entry,
43+
)
44+
45+
self.huum = Huum(
46+
config_entry.data[CONF_USERNAME],
47+
config_entry.data[CONF_PASSWORD],
48+
session=async_get_clientsession(hass),
49+
)
50+
51+
async def _async_update_data(self) -> HuumStatusResponse:
52+
"""Get the latest status data."""
53+
54+
try:
55+
return await self.huum.status()
56+
except (Forbidden, NotAuthenticated) as err:
57+
_LOGGER.error("Could not log in to Huum with given credentials")
58+
raise UpdateFailed(
59+
"Could not log in to Huum with given credentials"
60+
) from err

homeassistant/components/huum/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "huum",
33
"name": "Huum",
4-
"codeowners": ["@frwickst"],
4+
"codeowners": ["@frwickst", "@vincentwolsink"],
55
"config_flow": true,
66
"documentation": "https://www.home-assistant.io/integrations/huum",
77
"iot_class": "cloud_polling",

tests/components/huum/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
11
"""Tests for the huum integration."""
2+
3+
from unittest.mock import patch
4+
5+
from homeassistant.const import Platform
6+
from homeassistant.core import HomeAssistant
7+
8+
from tests.common import MockConfigEntry
9+
10+
11+
async def setup_with_selected_platforms(
12+
hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform]
13+
) -> None:
14+
"""Set up the Huum integration with the selected platforms."""
15+
entry.add_to_hass(hass)
16+
with patch("homeassistant.components.huum.PLATFORMS", platforms):
17+
assert await hass.config_entries.async_setup(entry.entry_id)
18+
await hass.async_block_till_done()

tests/components/huum/conftest.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Configuration for Huum tests."""
2+
3+
from collections.abc import Generator
4+
from unittest.mock import AsyncMock, patch
5+
6+
from huum.const import SaunaStatus
7+
import pytest
8+
9+
from homeassistant.components.huum.const import DOMAIN
10+
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
11+
12+
from tests.common import MockConfigEntry
13+
14+
15+
@pytest.fixture
16+
def mock_huum() -> Generator[AsyncMock]:
17+
"""Mock data from the API."""
18+
huum = AsyncMock()
19+
with (
20+
patch(
21+
"homeassistant.components.huum.config_flow.Huum.status",
22+
return_value=huum,
23+
),
24+
patch(
25+
"homeassistant.components.huum.coordinator.Huum.status",
26+
return_value=huum,
27+
),
28+
patch(
29+
"homeassistant.components.huum.coordinator.Huum.turn_on",
30+
return_value=huum,
31+
) as turn_on,
32+
):
33+
huum.status = SaunaStatus.ONLINE_NOT_HEATING
34+
huum.door_closed = True
35+
huum.temperature = 30
36+
huum.sauna_name = 123456
37+
huum.target_temperature = 80
38+
huum.light = 1
39+
huum.humidity = 5
40+
huum.sauna_config.child_lock = "OFF"
41+
huum.sauna_config.max_heating_time = 3
42+
huum.sauna_config.min_heating_time = 0
43+
huum.sauna_config.max_temp = 110
44+
huum.sauna_config.min_temp = 40
45+
huum.sauna_config.max_timer = 0
46+
huum.sauna_config.min_timer = 0
47+
huum.turn_on = turn_on
48+
49+
yield huum
50+
51+
52+
@pytest.fixture
53+
def mock_setup_entry() -> Generator[AsyncMock]:
54+
"""Mock setting up a config entry."""
55+
with patch(
56+
"homeassistant.components.huum.async_setup_entry", return_value=True
57+
) as setup_entry_mock:
58+
yield setup_entry_mock
59+
60+
61+
@pytest.fixture
62+
def mock_config_entry() -> MockConfigEntry:
63+
"""Mock a config entry."""
64+
return MockConfigEntry(
65+
domain=DOMAIN,
66+
data={
67+
CONF_USERNAME: "[email protected]",
68+
CONF_PASSWORD: "ukuuku",
69+
},
70+
unique_id="123456",
71+
entry_id="AABBCC112233",
72+
)

0 commit comments

Comments
 (0)