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/calendar.py
|
||||||
homeassistant/components/todoist/const.py
|
homeassistant/components/todoist/const.py
|
||||||
homeassistant/components/tof/sensor.py
|
homeassistant/components/tof/sensor.py
|
||||||
|
homeassistant/components/tolo/__init__.py
|
||||||
|
homeassistant/components/tolo/climate.py
|
||||||
homeassistant/components/tomato/device_tracker.py
|
homeassistant/components/tomato/device_tracker.py
|
||||||
homeassistant/components/toon/__init__.py
|
homeassistant/components/toon/__init__.py
|
||||||
homeassistant/components/toon/binary_sensor.py
|
homeassistant/components/toon/binary_sensor.py
|
||||||
|
|
|
@ -131,6 +131,7 @@ homeassistant.components.tautulli.*
|
||||||
homeassistant.components.tcp.*
|
homeassistant.components.tcp.*
|
||||||
homeassistant.components.tile.*
|
homeassistant.components.tile.*
|
||||||
homeassistant.components.tplink.*
|
homeassistant.components.tplink.*
|
||||||
|
homeassistant.components.tolo.*
|
||||||
homeassistant.components.tractive.*
|
homeassistant.components.tractive.*
|
||||||
homeassistant.components.tradfri.*
|
homeassistant.components.tradfri.*
|
||||||
homeassistant.components.tts.*
|
homeassistant.components.tts.*
|
||||||
|
|
|
@ -541,6 +541,7 @@ homeassistant/components/tile/* @bachya
|
||||||
homeassistant/components/time_date/* @fabaff
|
homeassistant/components/time_date/* @fabaff
|
||||||
homeassistant/components/tmb/* @alemuro
|
homeassistant/components/tmb/* @alemuro
|
||||||
homeassistant/components/todoist/* @boralyl
|
homeassistant/components/todoist/* @boralyl
|
||||||
|
homeassistant/components/tolo/* @MatthiasLohr
|
||||||
homeassistant/components/totalconnect/* @austinmroczek
|
homeassistant/components/totalconnect/* @austinmroczek
|
||||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||||
homeassistant/components/traccar/* @ludeeus
|
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",
|
"tellduslive",
|
||||||
"tibber",
|
"tibber",
|
||||||
"tile",
|
"tile",
|
||||||
|
"tolo",
|
||||||
"toon",
|
"toon",
|
||||||
"totalconnect",
|
"totalconnect",
|
||||||
"tplink",
|
"tplink",
|
||||||
|
|
|
@ -361,6 +361,10 @@ DHCP = [
|
||||||
"domain": "tado",
|
"domain": "tado",
|
||||||
"hostname": "tado*"
|
"hostname": "tado*"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"domain": "tolo",
|
||||||
|
"hostname": "usr-tcp232-ed2"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"domain": "toon",
|
"domain": "toon",
|
||||||
"hostname": "eneco-*",
|
"hostname": "eneco-*",
|
||||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -1452,6 +1452,17 @@ no_implicit_optional = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.tractive.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -2316,6 +2316,9 @@ tmb==0.0.4
|
||||||
# homeassistant.components.todoist
|
# homeassistant.components.todoist
|
||||||
todoist-python==8.0.0
|
todoist-python==8.0.0
|
||||||
|
|
||||||
|
# homeassistant.components.tolo
|
||||||
|
tololib==0.1.0b2
|
||||||
|
|
||||||
# homeassistant.components.toon
|
# homeassistant.components.toon
|
||||||
toonapi==0.2.1
|
toonapi==0.2.1
|
||||||
|
|
||||||
|
|
|
@ -1350,6 +1350,9 @@ tellduslive==0.10.11
|
||||||
# homeassistant.components.powerwall
|
# homeassistant.components.powerwall
|
||||||
tesla-powerwall==0.3.12
|
tesla-powerwall==0.3.12
|
||||||
|
|
||||||
|
# homeassistant.components.tolo
|
||||||
|
tololib==0.1.0b2
|
||||||
|
|
||||||
# homeassistant.components.toon
|
# homeassistant.components.toon
|
||||||
toonapi==0.2.1
|
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