Add TOLO Sauna (tolo) integration (#55619)
This commit is contained in:
parent
2439f6b562
commit
a399037a46
17 changed files with 560 additions and 0 deletions
|
@ -1091,6 +1091,8 @@ omit =
|
|||
homeassistant/components/todoist/calendar.py
|
||||
homeassistant/components/todoist/const.py
|
||||
homeassistant/components/tof/sensor.py
|
||||
homeassistant/components/tolo/__init__.py
|
||||
homeassistant/components/tolo/climate.py
|
||||
homeassistant/components/tomato/device_tracker.py
|
||||
homeassistant/components/toon/__init__.py
|
||||
homeassistant/components/toon/binary_sensor.py
|
||||
|
|
|
@ -131,6 +131,7 @@ homeassistant.components.tautulli.*
|
|||
homeassistant.components.tcp.*
|
||||
homeassistant.components.tile.*
|
||||
homeassistant.components.tplink.*
|
||||
homeassistant.components.tolo.*
|
||||
homeassistant.components.tractive.*
|
||||
homeassistant.components.tradfri.*
|
||||
homeassistant.components.tts.*
|
||||
|
|
|
@ -541,6 +541,7 @@ homeassistant/components/tile/* @bachya
|
|||
homeassistant/components/time_date/* @fabaff
|
||||
homeassistant/components/tmb/* @alemuro
|
||||
homeassistant/components/todoist/* @boralyl
|
||||
homeassistant/components/tolo/* @MatthiasLohr
|
||||
homeassistant/components/totalconnect/* @austinmroczek
|
||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
|
|
101
homeassistant/components/tolo/__init__.py
Normal file
101
homeassistant/components/tolo/__init__.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
"""Component to control TOLO Sauna/Steam Bath."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import NamedTuple
|
||||
|
||||
from tololib import ToloClient
|
||||
from tololib.errors import ResponseTimedOutError
|
||||
from tololib.message_info import SettingsInfo, StatusInfo
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN
|
||||
|
||||
PLATFORMS = ["climate"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up tolo from a config entry."""
|
||||
coordinator = ToloSaunaUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class ToloSaunaData(NamedTuple):
|
||||
"""Compound class for reflecting full state (status and info) of a TOLO Sauna."""
|
||||
|
||||
status: StatusInfo
|
||||
settings: SettingsInfo
|
||||
|
||||
|
||||
class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]):
|
||||
"""DataUpdateCoordinator for TOLO Sauna."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize ToloSaunaUpdateCoordinator."""
|
||||
self.client = ToloClient(entry.data[CONF_HOST])
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name=f"{entry.title} ({entry.data[CONF_HOST]}) Data Update Coordinator",
|
||||
update_interval=timedelta(seconds=3),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> ToloSaunaData:
|
||||
return await self.hass.async_add_executor_job(self._get_tolo_sauna_data)
|
||||
|
||||
def _get_tolo_sauna_data(self) -> ToloSaunaData:
|
||||
try:
|
||||
status = self.client.get_status_info(
|
||||
resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT
|
||||
)
|
||||
settings = self.client.get_settings_info(
|
||||
resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT
|
||||
)
|
||||
return ToloSaunaData(status, settings)
|
||||
except ResponseTimedOutError as error:
|
||||
raise UpdateFailed("communication timeout") from error
|
||||
|
||||
|
||||
class ToloSaunaCoordinatorEntity(CoordinatorEntity):
|
||||
"""CoordinatorEntity for TOLO Sauna."""
|
||||
|
||||
coordinator: ToloSaunaUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Initialize ToloSaunaCoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name="TOLO Sauna",
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
manufacturer="SteamTec",
|
||||
model=self.coordinator.data.status.model.name.capitalize(),
|
||||
)
|
156
homeassistant/components/tolo/climate.py
Normal file
156
homeassistant/components/tolo/climate.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
"""TOLO Sauna climate controls (main sauna control)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from tololib.const import Calefaction
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
ClimateEntity,
|
||||
)
|
||||
from homeassistant.components.climate.const import (
|
||||
CURRENT_HVAC_DRY,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
CURRENT_HVAC_OFF,
|
||||
FAN_OFF,
|
||||
FAN_ON,
|
||||
HVAC_MODE_DRY,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator
|
||||
from .const import (
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up climate controls for TOLO Sauna."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([SaunaClimate(coordinator, entry)])
|
||||
|
||||
|
||||
class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity):
|
||||
"""Sauna climate control."""
|
||||
|
||||
_attr_fan_modes = [FAN_ON, FAN_OFF]
|
||||
_attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_DRY]
|
||||
_attr_max_humidity = DEFAULT_MAX_HUMIDITY
|
||||
_attr_max_temp = DEFAULT_MAX_TEMP
|
||||
_attr_min_humidity = DEFAULT_MIN_HUMIDITY
|
||||
_attr_min_temp = DEFAULT_MIN_TEMP
|
||||
_attr_name = "Sauna Climate"
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
_attr_supported_features = (
|
||||
SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | SUPPORT_FAN_MODE
|
||||
)
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
def __init__(
|
||||
self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Initialize TOLO Sauna Climate entity."""
|
||||
super().__init__(coordinator, entry)
|
||||
|
||||
self._attr_unique_id = f"{entry.entry_id}_climate"
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> int:
|
||||
"""Return current temperature."""
|
||||
return self.coordinator.data.status.current_temperature
|
||||
|
||||
@property
|
||||
def current_humidity(self) -> int:
|
||||
"""Return current humidity."""
|
||||
return self.coordinator.data.status.current_humidity
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> int:
|
||||
"""Return target temperature."""
|
||||
return self.coordinator.data.settings.target_temperature
|
||||
|
||||
@property
|
||||
def target_humidity(self) -> int:
|
||||
"""Return target humidity."""
|
||||
return self.coordinator.data.settings.target_humidity
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""Get current HVAC mode."""
|
||||
if self.coordinator.data.status.power_on:
|
||||
return HVAC_MODE_HEAT
|
||||
if (
|
||||
not self.coordinator.data.status.power_on
|
||||
and self.coordinator.data.status.fan_on
|
||||
):
|
||||
return HVAC_MODE_DRY
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> str | None:
|
||||
"""Execute HVAC action."""
|
||||
if self.coordinator.data.status.calefaction == Calefaction.HEAT:
|
||||
return CURRENT_HVAC_HEAT
|
||||
if self.coordinator.data.status.calefaction == Calefaction.KEEP:
|
||||
return CURRENT_HVAC_IDLE
|
||||
if self.coordinator.data.status.calefaction == Calefaction.INACTIVE:
|
||||
if self.coordinator.data.status.fan_on:
|
||||
return CURRENT_HVAC_DRY
|
||||
return CURRENT_HVAC_OFF
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str:
|
||||
"""Return current fan mode."""
|
||||
if self.coordinator.data.status.fan_on:
|
||||
return FAN_ON
|
||||
return FAN_OFF
|
||||
|
||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set HVAC mode."""
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
self._set_power_and_fan(False, False)
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
self._set_power_and_fan(True, False)
|
||||
if hvac_mode == HVAC_MODE_DRY:
|
||||
self._set_power_and_fan(False, True)
|
||||
|
||||
def set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set fan mode."""
|
||||
self.coordinator.client.set_fan_on(fan_mode == FAN_ON)
|
||||
|
||||
def set_humidity(self, humidity: float) -> None:
|
||||
"""Set desired target humidity."""
|
||||
self.coordinator.client.set_target_humidity(round(humidity))
|
||||
|
||||
def set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set desired target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
self.coordinator.client.set_target_temperature(round(temperature))
|
||||
|
||||
def _set_power_and_fan(self, power_on: bool, fan_on: bool) -> None:
|
||||
"""Shortcut for setting power and fan of TOLO device on one method."""
|
||||
self.coordinator.client.set_power_on(power_on)
|
||||
self.coordinator.client.set_fan_on(fan_on)
|
96
homeassistant/components/tolo/config_flow.py
Normal file
96
homeassistant/components/tolo/config_flow.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
"""Config flow for tolo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from tololib import ToloClient
|
||||
from tololib.errors import ResponseTimedOutError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import dhcp
|
||||
from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DEFAULT_NAME, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""ConfigFlow for TOLO Sauna."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_discovered_host: str | None = None
|
||||
|
||||
@staticmethod
|
||||
def _check_device_availability(host: str) -> bool:
|
||||
client = ToloClient(host)
|
||||
try:
|
||||
result = client.get_status_info(
|
||||
resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT
|
||||
)
|
||||
return result is not None
|
||||
except ResponseTimedOutError:
|
||||
return False
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
device_available = await self.hass.async_add_executor_job(
|
||||
self._check_device_availability, user_input[CONF_HOST]
|
||||
)
|
||||
|
||||
if not device_available:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_NAME, data={CONF_HOST: user_input[CONF_HOST]}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS]))
|
||||
self._abort_if_unique_id_configured({CONF_HOST: discovery_info[IP_ADDRESS]})
|
||||
self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]})
|
||||
|
||||
device_available = await self.hass.async_add_executor_job(
|
||||
self._check_device_availability, discovery_info[IP_ADDRESS]
|
||||
)
|
||||
|
||||
if device_available:
|
||||
self._discovered_host = discovery_info[IP_ADDRESS]
|
||||
return await self.async_step_confirm()
|
||||
return self.async_abort(reason="not_tolo_device")
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: self._discovered_host})
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_NAME, data={CONF_HOST: self._discovered_host}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
description_placeholders={CONF_HOST: self._discovered_host},
|
||||
)
|
13
homeassistant/components/tolo/const.py
Normal file
13
homeassistant/components/tolo/const.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
"""Constants for the tolo integration."""
|
||||
|
||||
DOMAIN = "tolo"
|
||||
DEFAULT_NAME = "TOLO Sauna"
|
||||
|
||||
DEFAULT_RETRY_TIMEOUT = 1
|
||||
DEFAULT_RETRY_COUNT = 3
|
||||
|
||||
DEFAULT_MAX_TEMP = 60
|
||||
DEFAULT_MIN_TEMP = 20
|
||||
|
||||
DEFAULT_MAX_HUMIDITY = 99
|
||||
DEFAULT_MIN_HUMIDITY = 60
|
14
homeassistant/components/tolo/manifest.json
Normal file
14
homeassistant/components/tolo/manifest.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"domain": "tolo",
|
||||
"name": "TOLO Sauna",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tolo",
|
||||
"requirements": [
|
||||
"tololib==0.1.0b2"
|
||||
],
|
||||
"codeowners": [
|
||||
"@MatthiasLohr"
|
||||
],
|
||||
"iot_class": "local_polling",
|
||||
"dhcp": [{"hostname": "usr-tcp232-ed2"}]
|
||||
}
|
23
homeassistant/components/tolo/strings.json
Normal file
23
homeassistant/components/tolo/strings.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Enter the hostname or IP address of your TOLO Sauna device.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
23
homeassistant/components/tolo/translations/en.json
Normal file
23
homeassistant/components/tolo/translations/en.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to start set up?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
},
|
||||
"description": "Enter the hostname or IP address of your TOLO Sauna device."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -298,6 +298,7 @@ FLOWS = [
|
|||
"tellduslive",
|
||||
"tibber",
|
||||
"tile",
|
||||
"tolo",
|
||||
"toon",
|
||||
"totalconnect",
|
||||
"tplink",
|
||||
|
|
|
@ -361,6 +361,10 @@ DHCP = [
|
|||
"domain": "tado",
|
||||
"hostname": "tado*"
|
||||
},
|
||||
{
|
||||
"domain": "tolo",
|
||||
"hostname": "usr-tcp232-ed2"
|
||||
},
|
||||
{
|
||||
"domain": "toon",
|
||||
"hostname": "eneco-*",
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -1452,6 +1452,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.tolo.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.tractive.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -2316,6 +2316,9 @@ tmb==0.0.4
|
|||
# homeassistant.components.todoist
|
||||
todoist-python==8.0.0
|
||||
|
||||
# homeassistant.components.tolo
|
||||
tololib==0.1.0b2
|
||||
|
||||
# homeassistant.components.toon
|
||||
toonapi==0.2.1
|
||||
|
||||
|
|
|
@ -1350,6 +1350,9 @@ tellduslive==0.10.11
|
|||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.3.12
|
||||
|
||||
# homeassistant.components.tolo
|
||||
tololib==0.1.0b2
|
||||
|
||||
# homeassistant.components.toon
|
||||
toonapi==0.2.1
|
||||
|
||||
|
|
1
tests/components/tolo/__init__.py
Normal file
1
tests/components/tolo/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the TOLO Sauna integration."""
|
107
tests/components/tolo/test_config_flow.py
Normal file
107
tests/components/tolo/test_config_flow.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
"""Tests for the TOLO Sauna config flow."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from tololib.errors import ResponseTimedOutError
|
||||
|
||||
from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS
|
||||
from homeassistant.components.tolo.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
RESULT_TYPE_CREATE_ENTRY,
|
||||
RESULT_TYPE_FORM,
|
||||
)
|
||||
|
||||
MOCK_DHCP_DATA = {IP_ADDRESS: "127.0.0.2", MAC_ADDRESS: "00:11:22:33:44:55"}
|
||||
|
||||
|
||||
@pytest.fixture(name="toloclient")
|
||||
def toloclient_fixture() -> Mock:
|
||||
"""Patch libraries."""
|
||||
with patch("homeassistant.components.tolo.config_flow.ToloClient") as toloclient:
|
||||
yield toloclient
|
||||
|
||||
|
||||
async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock):
|
||||
"""Test a user initiated config flow with provided host which times out."""
|
||||
toloclient().get_status_info.side_effect = lambda *args, **kwargs: (
|
||||
_ for _ in ()
|
||||
).throw(ResponseTimedOutError())
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: "127.0.0.1"},
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock):
|
||||
"""Test complete user flow with first wrong and then correct host."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
assert "flow_id" in result
|
||||
|
||||
toloclient().get_status_info.side_effect = lambda *args, **kwargs: None
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "127.0.0.2"},
|
||||
)
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == SOURCE_USER
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
assert "flow_id" in result2
|
||||
|
||||
toloclient().get_status_info.side_effect = lambda *args, **kwargs: object()
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "127.0.0.1"},
|
||||
)
|
||||
|
||||
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result3["title"] == "TOLO Sauna"
|
||||
assert result3["data"][CONF_HOST] == "127.0.0.1"
|
||||
|
||||
|
||||
async def test_dhcp(hass: HomeAssistant, toloclient: Mock):
|
||||
"""Test starting a flow from discovery."""
|
||||
toloclient().get_status_info.side_effect = lambda *args, **kwargs: object()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "TOLO Sauna"
|
||||
assert result["data"][CONF_HOST] == "127.0.0.2"
|
||||
assert result["result"].unique_id == "00:11:22:33:44:55"
|
||||
|
||||
|
||||
async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock):
|
||||
"""Test starting a flow from discovery."""
|
||||
toloclient().get_status_info.side_effect = lambda *args, **kwargs: None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
Loading…
Add table
Add a link
Reference in a new issue