mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Huum - Introduce coordinator to support multiple platforms (#148889)
Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
parent
d72fb021c1
commit
9373bb287c
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@ -684,8 +684,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/husqvarna_automower/ @Thomas55555
|
/tests/components/husqvarna_automower/ @Thomas55555
|
||||||
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
||||||
/tests/components/husqvarna_automower_ble/ @alistair23
|
/tests/components/husqvarna_automower_ble/ @alistair23
|
||||||
/homeassistant/components/huum/ @frwickst
|
/homeassistant/components/huum/ @frwickst @vincentwolsink
|
||||||
/tests/components/huum/ @frwickst
|
/tests/components/huum/ @frwickst @vincentwolsink
|
||||||
/homeassistant/components/hvv_departures/ @vigonotion
|
/homeassistant/components/hvv_departures/ @vigonotion
|
||||||
/tests/components/hvv_departures/ @vigonotion
|
/tests/components/hvv_departures/ @vigonotion
|
||||||
/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
||||||
|
@ -2,46 +2,28 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from huum.exceptions import Forbidden, NotAuthenticated
|
|
||||||
from huum.huum import Huum
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN, PLATFORMS
|
from .const import PLATFORMS
|
||||||
|
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, config_entry: HuumConfigEntry) -> bool:
|
||||||
"""Set up Huum from a config entry."""
|
"""Set up Huum from a config entry."""
|
||||||
username = entry.data[CONF_USERNAME]
|
coordinator = HuumDataUpdateCoordinator(
|
||||||
password = entry.data[CONF_PASSWORD]
|
hass=hass,
|
||||||
|
config_entry=config_entry,
|
||||||
|
)
|
||||||
|
|
||||||
huum = Huum(username, password, session=async_get_clientsession(hass))
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
config_entry.runtime_data = coordinator
|
||||||
|
|
||||||
try:
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||||
await huum.status()
|
|
||||||
except (Forbidden, NotAuthenticated) as err:
|
|
||||||
_LOGGER.error("Could not log in to Huum with given credentials")
|
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
"Could not log in to Huum with given credentials"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = huum
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, config_entry: HuumConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -7,38 +7,35 @@ from typing import Any
|
|||||||
|
|
||||||
from huum.const import SaunaStatus
|
from huum.const import SaunaStatus
|
||||||
from huum.exceptions import SafetyException
|
from huum.exceptions import SafetyException
|
||||||
from huum.huum import Huum
|
|
||||||
from huum.schemas import HuumStatusResponse
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: HuumConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Huum sauna with config flow."""
|
"""Set up the Huum sauna with config flow."""
|
||||||
huum_handler = hass.data.setdefault(DOMAIN, {})[entry.entry_id]
|
async_add_entities([HuumDevice(entry.runtime_data)])
|
||||||
|
|
||||||
async_add_entities([HuumDevice(huum_handler, entry.entry_id)], True)
|
|
||||||
|
|
||||||
|
|
||||||
class HuumDevice(ClimateEntity):
|
class HuumDevice(CoordinatorEntity[HuumDataUpdateCoordinator], ClimateEntity):
|
||||||
"""Representation of a heater."""
|
"""Representation of a heater."""
|
||||||
|
|
||||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||||
@ -54,24 +51,22 @@ class HuumDevice(ClimateEntity):
|
|||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
_target_temperature: int | None = None
|
def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None:
|
||||||
_status: HuumStatusResponse | None = None
|
|
||||||
|
|
||||||
def __init__(self, huum_handler: Huum, unique_id: str) -> None:
|
|
||||||
"""Initialize the heater."""
|
"""Initialize the heater."""
|
||||||
self._attr_unique_id = unique_id
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self._attr_unique_id = coordinator.config_entry.entry_id
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, unique_id)},
|
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||||
name="Huum sauna",
|
name="Huum sauna",
|
||||||
manufacturer="Huum",
|
manufacturer="Huum",
|
||||||
|
model="UKU WiFi",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._huum_handler = huum_handler
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> HVACMode:
|
def hvac_mode(self) -> HVACMode:
|
||||||
"""Return hvac operation ie. heat, cool mode."""
|
"""Return hvac operation ie. heat, cool mode."""
|
||||||
if self._status and self._status.status == SaunaStatus.ONLINE_HEATING:
|
if self.coordinator.data.status == SaunaStatus.ONLINE_HEATING:
|
||||||
return HVACMode.HEAT
|
return HVACMode.HEAT
|
||||||
return HVACMode.OFF
|
return HVACMode.OFF
|
||||||
|
|
||||||
@ -85,41 +80,33 @@ class HuumDevice(ClimateEntity):
|
|||||||
@property
|
@property
|
||||||
def current_temperature(self) -> int | None:
|
def current_temperature(self) -> int | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
if (status := self._status) is not None:
|
return self.coordinator.data.temperature
|
||||||
return status.temperature
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> int:
|
def target_temperature(self) -> int:
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
return self._target_temperature or int(self.min_temp)
|
return self.coordinator.data.target_temperature or int(self.min_temp)
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set hvac mode."""
|
"""Set hvac mode."""
|
||||||
if hvac_mode == HVACMode.HEAT:
|
if hvac_mode == HVACMode.HEAT:
|
||||||
await self._turn_on(self.target_temperature)
|
await self._turn_on(self.target_temperature)
|
||||||
elif hvac_mode == HVACMode.OFF:
|
elif hvac_mode == HVACMode.OFF:
|
||||||
await self._huum_handler.turn_off()
|
await self.coordinator.huum.turn_off()
|
||||||
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
if temperature is None:
|
if temperature is None or self.hvac_mode != HVACMode.HEAT:
|
||||||
return
|
return
|
||||||
self._target_temperature = temperature
|
|
||||||
|
|
||||||
if self.hvac_mode == HVACMode.HEAT:
|
await self._turn_on(temperature)
|
||||||
await self._turn_on(temperature)
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
|
||||||
"""Get the latest status data."""
|
|
||||||
self._status = await self._huum_handler.status()
|
|
||||||
if self._target_temperature is None or self.hvac_mode == HVACMode.HEAT:
|
|
||||||
self._target_temperature = self._status.target_temperature
|
|
||||||
|
|
||||||
async def _turn_on(self, temperature: int) -> None:
|
async def _turn_on(self, temperature: int) -> None:
|
||||||
try:
|
try:
|
||||||
await self._huum_handler.turn_on(temperature)
|
await self.coordinator.huum.turn_on(temperature)
|
||||||
except (ValueError, SafetyException) as err:
|
except (ValueError, SafetyException) as err:
|
||||||
_LOGGER.error(str(err))
|
_LOGGER.error(str(err))
|
||||||
raise HomeAssistantError(f"Unable to turn on sauna: {err}") from err
|
raise HomeAssistantError(f"Unable to turn on sauna: {err}") from err
|
||||||
|
@ -37,12 +37,12 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
huum_handler = Huum(
|
huum = Huum(
|
||||||
user_input[CONF_USERNAME],
|
user_input[CONF_USERNAME],
|
||||||
user_input[CONF_PASSWORD],
|
user_input[CONF_PASSWORD],
|
||||||
session=async_get_clientsession(self.hass),
|
session=async_get_clientsession(self.hass),
|
||||||
)
|
)
|
||||||
await huum_handler.status()
|
await huum.status()
|
||||||
except (Forbidden, NotAuthenticated):
|
except (Forbidden, NotAuthenticated):
|
||||||
# Most likely Forbidden as that is what is returned from `.status()` with bad creds
|
# Most likely Forbidden as that is what is returned from `.status()` with bad creds
|
||||||
_LOGGER.error("Could not log in to Huum with given credentials")
|
_LOGGER.error("Could not log in to Huum with given credentials")
|
||||||
|
60
homeassistant/components/huum/coordinator.py
Normal file
60
homeassistant/components/huum/coordinator.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""DataUpdateCoordinator for Huum."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from huum.exceptions import Forbidden, NotAuthenticated
|
||||||
|
from huum.huum import Huum
|
||||||
|
from huum.schemas import HuumStatusResponse
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
type HuumConfigEntry = ConfigEntry[HuumDataUpdateCoordinator]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
|
||||||
|
class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]):
|
||||||
|
"""Class to manage fetching data from the API."""
|
||||||
|
|
||||||
|
config_entry: HuumConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: HuumConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(
|
||||||
|
hass=hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
config_entry=config_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.huum = Huum(
|
||||||
|
config_entry.data[CONF_USERNAME],
|
||||||
|
config_entry.data[CONF_PASSWORD],
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> HuumStatusResponse:
|
||||||
|
"""Get the latest status data."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await self.huum.status()
|
||||||
|
except (Forbidden, NotAuthenticated) as err:
|
||||||
|
_LOGGER.error("Could not log in to Huum with given credentials")
|
||||||
|
raise UpdateFailed(
|
||||||
|
"Could not log in to Huum with given credentials"
|
||||||
|
) from err
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "huum",
|
"domain": "huum",
|
||||||
"name": "Huum",
|
"name": "Huum",
|
||||||
"codeowners": ["@frwickst"],
|
"codeowners": ["@frwickst", "@vincentwolsink"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/huum",
|
"documentation": "https://www.home-assistant.io/integrations/huum",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
|
@ -1 +1,18 @@
|
|||||||
"""Tests for the huum integration."""
|
"""Tests for the huum integration."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_with_selected_platforms(
|
||||||
|
hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform]
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Huum integration with the selected platforms."""
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
with patch("homeassistant.components.huum.PLATFORMS", platforms):
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
72
tests/components/huum/conftest.py
Normal file
72
tests/components/huum/conftest.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"""Configuration for Huum tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from huum.const import SaunaStatus
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.huum.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_huum() -> Generator[AsyncMock]:
|
||||||
|
"""Mock data from the API."""
|
||||||
|
huum = AsyncMock()
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.huum.config_flow.Huum.status",
|
||||||
|
return_value=huum,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.huum.coordinator.Huum.status",
|
||||||
|
return_value=huum,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.huum.coordinator.Huum.turn_on",
|
||||||
|
return_value=huum,
|
||||||
|
) as turn_on,
|
||||||
|
):
|
||||||
|
huum.status = SaunaStatus.ONLINE_NOT_HEATING
|
||||||
|
huum.door_closed = True
|
||||||
|
huum.temperature = 30
|
||||||
|
huum.sauna_name = 123456
|
||||||
|
huum.target_temperature = 80
|
||||||
|
huum.light = 1
|
||||||
|
huum.humidity = 5
|
||||||
|
huum.sauna_config.child_lock = "OFF"
|
||||||
|
huum.sauna_config.max_heating_time = 3
|
||||||
|
huum.sauna_config.min_heating_time = 0
|
||||||
|
huum.sauna_config.max_temp = 110
|
||||||
|
huum.sauna_config.min_temp = 40
|
||||||
|
huum.sauna_config.max_timer = 0
|
||||||
|
huum.sauna_config.min_timer = 0
|
||||||
|
huum.turn_on = turn_on
|
||||||
|
|
||||||
|
yield huum
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Mock setting up a config entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.huum.async_setup_entry", return_value=True
|
||||||
|
) as setup_entry_mock:
|
||||||
|
yield setup_entry_mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Mock a config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_USERNAME: "huum@sauna.org",
|
||||||
|
CONF_PASSWORD: "ukuuku",
|
||||||
|
},
|
||||||
|
unique_id="123456",
|
||||||
|
entry_id="AABBCC112233",
|
||||||
|
)
|
68
tests/components/huum/snapshots/test_climate.ambr
Normal file
68
tests/components/huum/snapshots/test_climate.ambr
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_climate_entity[climate.huum_sauna-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'hvac_modes': list([
|
||||||
|
<HVACMode.HEAT: 'heat'>,
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
|
]),
|
||||||
|
'max_temp': 110,
|
||||||
|
'min_temp': 40,
|
||||||
|
'target_temp_step': 1,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'climate',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'climate.huum_sauna',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': 'mdi:radiator-off',
|
||||||
|
'original_name': None,
|
||||||
|
'platform': 'huum',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': <ClimateEntityFeature: 385>,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': 'AABBCC112233',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_climate_entity[climate.huum_sauna-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'current_temperature': 30,
|
||||||
|
'friendly_name': 'Huum sauna',
|
||||||
|
'hvac_modes': list([
|
||||||
|
<HVACMode.HEAT: 'heat'>,
|
||||||
|
<HVACMode.OFF: 'off'>,
|
||||||
|
]),
|
||||||
|
'icon': 'mdi:radiator-off',
|
||||||
|
'max_temp': 110,
|
||||||
|
'min_temp': 40,
|
||||||
|
'supported_features': <ClimateEntityFeature: 385>,
|
||||||
|
'target_temp_step': 1,
|
||||||
|
'temperature': 80,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'climate.huum_sauna',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'off',
|
||||||
|
})
|
||||||
|
# ---
|
78
tests/components/huum/test_climate.py
Normal file
78
tests/components/huum/test_climate.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"""Tests for the Huum climate entity."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from huum.const import SaunaStatus
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
ATTR_HVAC_MODE,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
HVACMode,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_with_selected_platforms
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, snapshot_platform
|
||||||
|
|
||||||
|
ENTITY_ID = "climate.huum_sauna"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_entity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_huum: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test the initial parameters."""
|
||||||
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_hvac_mode(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_huum: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting HVAC mode."""
|
||||||
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||||
|
|
||||||
|
mock_huum.status = SaunaStatus.ONLINE_HEATING
|
||||||
|
await hass.services.async_call(
|
||||||
|
Platform.CLIMATE,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
assert state.state == HVACMode.HEAT
|
||||||
|
|
||||||
|
mock_huum.turn_on.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_temperature(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_huum: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting the temperature."""
|
||||||
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||||
|
|
||||||
|
mock_huum.status = SaunaStatus.ONLINE_HEATING
|
||||||
|
await hass.services.async_call(
|
||||||
|
Platform.CLIMATE,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: ENTITY_ID,
|
||||||
|
ATTR_TEMPERATURE: 60,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_huum.turn_on.assert_called_once_with(60)
|
@ -1,6 +1,6 @@
|
|||||||
"""Test the huum config flow."""
|
"""Test the huum config flow."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from huum.exceptions import Forbidden
|
from huum.exceptions import Forbidden
|
||||||
import pytest
|
import pytest
|
||||||
@ -13,11 +13,13 @@ from homeassistant.data_entry_flow import FlowResultType
|
|||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
TEST_USERNAME = "test-username"
|
TEST_USERNAME = "huum@sauna.org"
|
||||||
TEST_PASSWORD = "test-password"
|
TEST_PASSWORD = "ukuuku"
|
||||||
|
|
||||||
|
|
||||||
async def test_form(hass: HomeAssistant) -> None:
|
async def test_form(
|
||||||
|
hass: HomeAssistant, mock_huum: AsyncMock, mock_setup_entry: AsyncMock
|
||||||
|
) -> None:
|
||||||
"""Test we get the form."""
|
"""Test we get the form."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -26,24 +28,14 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
with (
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
patch(
|
result["flow_id"],
|
||||||
"homeassistant.components.huum.config_flow.Huum.status",
|
{
|
||||||
return_value=True,
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
),
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
patch(
|
},
|
||||||
"homeassistant.components.huum.async_setup_entry",
|
)
|
||||||
return_value=True,
|
await hass.async_block_till_done()
|
||||||
) as mock_setup_entry,
|
|
||||||
):
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
CONF_USERNAME: TEST_USERNAME,
|
|
||||||
CONF_PASSWORD: TEST_PASSWORD,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == TEST_USERNAME
|
assert result2["title"] == TEST_USERNAME
|
||||||
@ -54,42 +46,28 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_signup_flow_already_set_up(hass: HomeAssistant) -> None:
|
async def test_signup_flow_already_set_up(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_huum: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
"""Test that we handle already existing entities with same id."""
|
"""Test that we handle already existing entities with same id."""
|
||||||
mock_config_entry = MockConfigEntry(
|
|
||||||
title="Huum Sauna",
|
|
||||||
domain=DOMAIN,
|
|
||||||
unique_id=TEST_USERNAME,
|
|
||||||
data={
|
|
||||||
CONF_USERNAME: TEST_USERNAME,
|
|
||||||
CONF_PASSWORD: TEST_PASSWORD,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
with (
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
patch(
|
result["flow_id"],
|
||||||
"homeassistant.components.huum.config_flow.Huum.status",
|
{
|
||||||
return_value=True,
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
),
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
patch(
|
},
|
||||||
"homeassistant.components.huum.async_setup_entry",
|
)
|
||||||
return_value=True,
|
await hass.async_block_till_done()
|
||||||
),
|
assert result2["type"] is FlowResultType.ABORT
|
||||||
):
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
CONF_USERNAME: TEST_USERNAME,
|
|
||||||
CONF_PASSWORD: TEST_PASSWORD,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert result2["type"] is FlowResultType.ABORT
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -103,7 +81,11 @@ async def test_signup_flow_already_set_up(hass: HomeAssistant) -> None:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_huum_errors(
|
async def test_huum_errors(
|
||||||
hass: HomeAssistant, raises: Exception, error_base: str
|
hass: HomeAssistant,
|
||||||
|
mock_huum: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
raises: Exception,
|
||||||
|
error_base: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we handle cannot connect error."""
|
"""Test we handle cannot connect error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -125,21 +107,11 @@ async def test_huum_errors(
|
|||||||
assert result2["type"] is FlowResultType.FORM
|
assert result2["type"] is FlowResultType.FORM
|
||||||
assert result2["errors"] == {"base": error_base}
|
assert result2["errors"] == {"base": error_base}
|
||||||
|
|
||||||
with (
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
patch(
|
result["flow_id"],
|
||||||
"homeassistant.components.huum.config_flow.Huum.status",
|
{
|
||||||
return_value=True,
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
),
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
patch(
|
},
|
||||||
"homeassistant.components.huum.async_setup_entry",
|
)
|
||||||
return_value=True,
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
),
|
|
||||||
):
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
CONF_USERNAME: TEST_USERNAME,
|
|
||||||
CONF_PASSWORD: TEST_PASSWORD,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
|
||||||
|
27
tests/components/huum/test_init.py
Normal file
27
tests/components/huum/test_init.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""Tests for the Huum __init__."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from homeassistant.components.huum.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_with_selected_platforms
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_loading_and_unloading_config_entry(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_huum: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test loading and unloading a config entry."""
|
||||||
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
Loading…
x
Reference in New Issue
Block a user