mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add Balboa Spa integration (#59234)
This commit is contained in:
parent
78305ac6ae
commit
03d1efab46
@ -90,6 +90,8 @@ omit =
|
||||
homeassistant/components/azure_devops/sensor.py
|
||||
homeassistant/components/azure_service_bus/*
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/balboa/__init__.py
|
||||
homeassistant/components/balboa/entity.py
|
||||
homeassistant/components/beewi_smartclim/sensor.py
|
||||
homeassistant/components/bbb_gpio/*
|
||||
homeassistant/components/bbox/device_tracker.py
|
||||
|
@ -67,6 +67,7 @@ homeassistant/components/axis/* @Kane610
|
||||
homeassistant/components/azure_devops/* @timmo001
|
||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||
homeassistant/components/azure_service_bus/* @hfurubotten
|
||||
homeassistant/components/balboa/* @garbled1
|
||||
homeassistant/components/beewi_smartclim/* @alemuro
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
|
102
homeassistant/components/balboa/__init__.py
Normal file
102
homeassistant/components/balboa/__init__.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""The Balboa Spa Client integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import time
|
||||
|
||||
from pybalboa import BalboaSpaWifi
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
CONF_SYNC_TIME,
|
||||
DEFAULT_SYNC_TIME,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SIGNAL_UPDATE,
|
||||
)
|
||||
|
||||
SYNC_TIME_INTERVAL = timedelta(days=1)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Balboa Spa from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
_LOGGER.debug("Attempting to connect to %s", host)
|
||||
spa = BalboaSpaWifi(host)
|
||||
|
||||
connected = await spa.connect()
|
||||
if not connected:
|
||||
_LOGGER.error("Failed to connect to spa at %s", host)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa
|
||||
|
||||
# send config requests, and then listen until we are configured.
|
||||
await spa.send_mod_ident_req()
|
||||
await spa.send_panel_req(0, 1)
|
||||
|
||||
async def _async_balboa_update_cb():
|
||||
"""Primary update callback called from pybalboa."""
|
||||
_LOGGER.debug("Primary update callback triggered")
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(entry.entry_id))
|
||||
|
||||
# set the callback so we know we have new data
|
||||
spa.new_data_cb = _async_balboa_update_cb
|
||||
|
||||
_LOGGER.debug("Starting listener and monitor tasks")
|
||||
hass.loop.create_task(spa.listen())
|
||||
await spa.spa_configured()
|
||||
asyncio.create_task(spa.check_connection_status())
|
||||
|
||||
# At this point we have a configured spa.
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
# call update_listener on startup and for options change as well.
|
||||
await async_setup_time_sync(hass, entry)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
_LOGGER.debug("Disconnecting from spa")
|
||||
spa = hass.data[DOMAIN][entry.entry_id]
|
||||
await spa.disconnect()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Set up the time sync."""
|
||||
if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME):
|
||||
return
|
||||
|
||||
_LOGGER.debug("Setting up daily time sync")
|
||||
spa = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async def sync_time():
|
||||
_LOGGER.debug("Syncing time with Home Assistant")
|
||||
await spa.set_time(time.strptime(str(dt_util.now()), "%Y-%m-%d %H:%M:%S.%f%z"))
|
||||
|
||||
await sync_time()
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval(hass, sync_time, SYNC_TIME_INTERVAL)
|
||||
)
|
161
homeassistant/components/balboa/climate.py
Normal file
161
homeassistant/components/balboa/climate.py
Normal file
@ -0,0 +1,161 @@
|
||||
"""Support for Balboa Spa Wifi adaptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
PRECISION_WHOLE,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import CLIMATE, CLIMATE_SUPPORTED_FANSTATES, CLIMATE_SUPPORTED_MODES, DOMAIN
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up the spa climate device."""
|
||||
async_add_entities(
|
||||
[
|
||||
BalboaSpaClimate(
|
||||
hass,
|
||||
entry,
|
||||
hass.data[DOMAIN][entry.entry_id],
|
||||
CLIMATE,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class BalboaSpaClimate(BalboaEntity, ClimateEntity):
|
||||
"""Representation of a Balboa Spa Climate device."""
|
||||
|
||||
_attr_icon = "mdi:hot-tub"
|
||||
_attr_fan_modes = CLIMATE_SUPPORTED_FANSTATES
|
||||
_attr_hvac_modes = CLIMATE_SUPPORTED_MODES
|
||||
|
||||
def __init__(self, hass, entry, client, devtype, num=None):
|
||||
"""Initialize the climate entity."""
|
||||
super().__init__(hass, entry, client, devtype, num)
|
||||
self._balboa_to_ha_blower_map = {
|
||||
self._client.BLOWER_OFF: FAN_OFF,
|
||||
self._client.BLOWER_LOW: FAN_LOW,
|
||||
self._client.BLOWER_MEDIUM: FAN_MEDIUM,
|
||||
self._client.BLOWER_HIGH: FAN_HIGH,
|
||||
}
|
||||
self._ha_to_balboa_blower_map = {
|
||||
value: key for key, value in self._balboa_to_ha_blower_map.items()
|
||||
}
|
||||
self._balboa_to_ha_heatmode_map = {
|
||||
self._client.HEATMODE_READY: HVAC_MODE_HEAT,
|
||||
self._client.HEATMODE_RNR: HVAC_MODE_AUTO,
|
||||
self._client.HEATMODE_REST: HVAC_MODE_OFF,
|
||||
}
|
||||
self._ha_heatmode_to_balboa_map = {
|
||||
value: key for key, value in self._balboa_to_ha_heatmode_map.items()
|
||||
}
|
||||
scale = self._client.get_tempscale()
|
||||
self._attr_preset_modes = self._client.get_heatmode_stringlist()
|
||||
self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
if self._client.have_blower():
|
||||
self._attr_supported_features |= SUPPORT_FAN_MODE
|
||||
self._attr_min_temp = self._client.tmin[self._client.TEMPRANGE_LOW][scale]
|
||||
self._attr_max_temp = self._client.tmax[self._client.TEMPRANGE_HIGH][scale]
|
||||
self._attr_temperature_unit = TEMP_FAHRENHEIT
|
||||
self._attr_precision = PRECISION_WHOLE
|
||||
if self._client.get_tempscale() == self._client.TSCALE_C:
|
||||
self._attr_temperature_unit = TEMP_CELSIUS
|
||||
self._attr_precision = PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return the current HVAC mode."""
|
||||
mode = self._client.get_heatmode()
|
||||
return self._balboa_to_ha_heatmode_map[mode]
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> str:
|
||||
"""Return the current operation mode."""
|
||||
state = self._client.get_heatstate()
|
||||
if state >= self._client.ON:
|
||||
return CURRENT_HVAC_HEAT
|
||||
return CURRENT_HVAC_IDLE
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str:
|
||||
"""Return the current fan mode."""
|
||||
fanmode = self._client.get_blower()
|
||||
return self._balboa_to_ha_blower_map.get(fanmode, FAN_OFF)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._client.get_curtemp()
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature we try to reach."""
|
||||
return self._client.get_settemp()
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return current preset mode."""
|
||||
return self._client.get_heatmode(True)
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set a new target temperature."""
|
||||
scale = self._client.get_tempscale()
|
||||
newtemp = kwargs[ATTR_TEMPERATURE]
|
||||
if newtemp > self._client.tmax[self._client.TEMPRANGE_LOW][scale]:
|
||||
await self._client.change_temprange(self._client.TEMPRANGE_HIGH)
|
||||
await asyncio.sleep(1)
|
||||
if newtemp < self._client.tmin[self._client.TEMPRANGE_HIGH][scale]:
|
||||
await self._client.change_temprange(self._client.TEMPRANGE_LOW)
|
||||
await asyncio.sleep(1)
|
||||
await self._client.send_temp_change(newtemp)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode) -> None:
|
||||
"""Set new preset mode."""
|
||||
modelist = self._client.get_heatmode_stringlist()
|
||||
self._async_validate_mode_or_raise(preset_mode)
|
||||
if preset_mode not in modelist:
|
||||
raise HomeAssistantError(f"{preset_mode} is not a valid preset mode")
|
||||
await self._client.change_heatmode(modelist.index(preset_mode))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new fan mode."""
|
||||
await self._client.change_blower(self._ha_to_balboa_blower_map[fan_mode])
|
||||
|
||||
def _async_validate_mode_or_raise(self, mode):
|
||||
"""Check that the mode can be set."""
|
||||
if mode == self._client.HEATMODE_RNR:
|
||||
raise HomeAssistantError(f"{mode} can only be reported but not set")
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode.
|
||||
|
||||
OFF = Rest
|
||||
AUTO = Ready in Rest (can't be set, only reported)
|
||||
HEAT = Ready
|
||||
"""
|
||||
mode = self._ha_heatmode_to_balboa_map[hvac_mode]
|
||||
self._async_validate_mode_or_raise(mode)
|
||||
await self._client.change_heatmode(self._ha_heatmode_to_balboa_map[hvac_mode])
|
99
homeassistant/components/balboa/config_flow.py
Normal file
99
homeassistant/components/balboa/config_flow.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""Config flow for Balboa Spa Client integration."""
|
||||
from pybalboa import BalboaSpaWifi
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import _LOGGER, CONF_SYNC_TIME, DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
_LOGGER.debug("Attempting to connect to %s", data[CONF_HOST])
|
||||
spa = BalboaSpaWifi(data[CONF_HOST])
|
||||
connected = await spa.connect()
|
||||
_LOGGER.debug("Got connected = %d", connected)
|
||||
if not connected:
|
||||
raise CannotConnect
|
||||
|
||||
# send config requests, and then listen until we are configured.
|
||||
await spa.send_mod_ident_req()
|
||||
await spa.send_panel_req(0, 1)
|
||||
|
||||
hass.loop.create_task(spa.listen())
|
||||
|
||||
await spa.spa_configured()
|
||||
|
||||
macaddr = format_mac(spa.get_macaddr())
|
||||
model = spa.get_model_name()
|
||||
await spa.disconnect()
|
||||
|
||||
return {"title": model, "formatted_mac": macaddr}
|
||||
|
||||
|
||||
class BalboaSpaClientFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Balboa Spa Client config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return BalboaSpaClientOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""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]})
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info["formatted_mac"])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class BalboaSpaClientOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Balboa Spa Client options."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize Balboa Spa Client options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage Balboa Spa Client options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_SYNC_TIME,
|
||||
default=self.config_entry.options.get(CONF_SYNC_TIME, False),
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
)
|
35
homeassistant/components/balboa/const.py
Normal file
35
homeassistant/components/balboa/const.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Constants for the Balboa Spa Client integration."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
)
|
||||
from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_OFF
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "balboa"
|
||||
|
||||
CLIMATE_SUPPORTED_FANSTATES = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
||||
CLIMATE_SUPPORTED_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
CONF_SYNC_TIME = "sync_time"
|
||||
DEFAULT_SYNC_TIME = False
|
||||
FAN_SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_HIGH]
|
||||
PLATFORMS = ["climate"]
|
||||
|
||||
AUX = "Aux"
|
||||
CIRC_PUMP = "Circ Pump"
|
||||
CLIMATE = "Climate"
|
||||
FILTER = "Filter"
|
||||
LIGHT = "Light"
|
||||
MISTER = "Mister"
|
||||
PUMP = "Pump"
|
||||
TEMP_RANGE = "Temp Range"
|
||||
|
||||
SIGNAL_UPDATE = "balboa_update_{}"
|
57
homeassistant/components/balboa/entity.py
Normal file
57
homeassistant/components/balboa/entity.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Base class for Balboa Spa Client integration."""
|
||||
import time
|
||||
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
from .const import SIGNAL_UPDATE
|
||||
|
||||
|
||||
class BalboaEntity(Entity):
|
||||
"""Abstract class for all Balboa platforms.
|
||||
|
||||
Once you connect to the spa's port, it continuously sends data (at a rate
|
||||
of about 5 per second!). The API updates the internal states of things
|
||||
from this stream, and all we have to do is read the values out of the
|
||||
accessors.
|
||||
"""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, hass, entry, client, devtype, num=None):
|
||||
"""Initialize the spa entity."""
|
||||
self._client = client
|
||||
self._device_name = self._client.get_model_name()
|
||||
self._type = devtype
|
||||
self._num = num
|
||||
self._entry = entry
|
||||
self._attr_unique_id = f'{self._device_name}-{self._type}{self._num or ""}-{self._client.get_macaddr().replace(":","")[-6:]}'
|
||||
self._attr_name = f'{self._device_name}: {self._type}{self._num or ""}'
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=self._device_name,
|
||||
manufacturer="Balboa Water Group",
|
||||
model=self._client.get_model_name(),
|
||||
sw_version=self._client.get_ssid(),
|
||||
connections={(CONNECTION_NETWORK_MAC, self._client.get_macaddr())},
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up a listener for the entity."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_UPDATE.format(self._entry.entry_id),
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return whether the state is based on actual reading from device."""
|
||||
return (self._client.lastupd + 5 * 60) < time.time()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return whether the entity is available or not."""
|
||||
return self._client.connected
|
13
homeassistant/components/balboa/manifest.json
Normal file
13
homeassistant/components/balboa/manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "balboa",
|
||||
"name": "Balboa Spa Client",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||
"requirements": [
|
||||
"pybalboa==0.13"
|
||||
],
|
||||
"codeowners": [
|
||||
"@garbled1"
|
||||
],
|
||||
"iot_class": "local_push"
|
||||
}
|
28
homeassistant/components/balboa/strings.json
Normal file
28
homeassistant/components/balboa/strings.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the Balboa Wi-Fi device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"sync_time": "Keep your Balboa Spa Client's time synchronized with Home Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
homeassistant/components/balboa/translations/en.json
Normal file
28
homeassistant/components/balboa/translations/en.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the Balboa Wi-Fi device",
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"sync_time": "Keep your Balboa Spa Client's time synchronized with Home Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@ FLOWS = [
|
||||
"awair",
|
||||
"axis",
|
||||
"azure_devops",
|
||||
"balboa",
|
||||
"blebox",
|
||||
"blink",
|
||||
"bmw_connected_drive",
|
||||
|
@ -1371,6 +1371,9 @@ pyatome==0.1.1
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.8.2
|
||||
|
||||
# homeassistant.components.balboa
|
||||
pybalboa==0.13
|
||||
|
||||
# homeassistant.components.bbox
|
||||
pybbox==0.0.5-alpha
|
||||
|
||||
|
@ -837,6 +837,9 @@ pyatmo==6.2.0
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.8.2
|
||||
|
||||
# homeassistant.components.balboa
|
||||
pybalboa==0.13
|
||||
|
||||
# homeassistant.components.blackbird
|
||||
pyblackbird==0.5
|
||||
|
||||
|
167
tests/components/balboa/__init__.py
Normal file
167
tests/components/balboa/__init__.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""Test the Balboa Spa Client integration."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
BALBOA_DEFAULT_PORT = 4257
|
||||
TEST_HOST = "balboatest.localdomain"
|
||||
|
||||
|
||||
async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Mock integration setup."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=BALBOA_DOMAIN,
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi",
|
||||
new=BalboaMock,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
async def init_integration_mocked(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Mock integration setup."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=BALBOA_DOMAIN,
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.connect",
|
||||
new=BalboaMock.connect,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.listen_until_configured",
|
||||
new=BalboaMock.listen_until_configured,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.listen",
|
||||
new=BalboaMock.listen,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.check_connection_status",
|
||||
new=BalboaMock.check_connection_status,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.send_panel_req",
|
||||
new=BalboaMock.send_panel_req,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.send_mod_ident_req",
|
||||
new=BalboaMock.send_mod_ident_req,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.spa_configured",
|
||||
new=BalboaMock.spa_configured,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_model_name",
|
||||
new=BalboaMock.get_model_name,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
class BalboaMock:
|
||||
"""Mock pybalboa library."""
|
||||
|
||||
def __init__(self, hostname, port=BALBOA_DEFAULT_PORT):
|
||||
"""Mock init."""
|
||||
self.host = hostname
|
||||
self.port = port
|
||||
self.connected = False
|
||||
self.new_data_cb = None
|
||||
self.lastupd = 0
|
||||
self.connected = False
|
||||
self.fake_action = False
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to the spa."""
|
||||
self.connected = True
|
||||
return True
|
||||
|
||||
async def broken_connect(self):
|
||||
"""Connect to the spa."""
|
||||
self.connected = False
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""Stop talking to the spa."""
|
||||
self.connected = False
|
||||
|
||||
async def send_panel_req(self, arg_ba, arg_bb):
|
||||
"""Send a panel request, 2 bytes of data."""
|
||||
self.fake_action = False
|
||||
return
|
||||
|
||||
async def send_mod_ident_req(self):
|
||||
"""Ask for the module identification."""
|
||||
self.fake_action = False
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_macaddr():
|
||||
"""Return the macaddr of the spa wifi."""
|
||||
return "ef:ef:ef:c0:ff:ee"
|
||||
|
||||
def get_model_name(self):
|
||||
"""Return the model name."""
|
||||
self.fake_action = False
|
||||
return "FakeSpa"
|
||||
|
||||
@staticmethod
|
||||
def get_ssid():
|
||||
"""Return the software version."""
|
||||
return "V0.0"
|
||||
|
||||
@staticmethod
|
||||
async def set_time(new_time, timescale=None):
|
||||
"""Set time on spa to new_time with optional timescale."""
|
||||
return
|
||||
|
||||
async def listen(self):
|
||||
"""Listen to the spa babble forever."""
|
||||
while True:
|
||||
if not self.connected:
|
||||
# sleep and hope the checker fixes us
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
||||
# fake it
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def check_connection_status(self):
|
||||
"""Set this up to periodically check the spa connection and fix."""
|
||||
self.fake_action = False
|
||||
while True:
|
||||
# fake it
|
||||
await asyncio.sleep(15)
|
||||
|
||||
async def spa_configured(self):
|
||||
"""Check if the spa has been configured."""
|
||||
self.fake_action = False
|
||||
return
|
||||
|
||||
async def int_new_data_cb(self):
|
||||
"""Call false internal data callback."""
|
||||
|
||||
if self.new_data_cb is None:
|
||||
return
|
||||
await self.new_data_cb() # pylint: disable=not-callable
|
||||
|
||||
async def listen_until_configured(self, maxiter=20):
|
||||
"""Listen to the spa babble until we are configured."""
|
||||
if not self.connected:
|
||||
return False
|
||||
return True
|
272
tests/components/balboa/test_climate.py
Normal file
272
tests/components/balboa/test_climate.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""Tests of the climate entity of the balboa integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN, SIGNAL_UPDATE
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_PRESET_MODES,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import init_integration_mocked
|
||||
|
||||
from tests.components.climate import common
|
||||
|
||||
FAN_SETTINGS = [
|
||||
FAN_OFF,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_HIGH,
|
||||
]
|
||||
|
||||
HVAC_SETTINGS = [
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
]
|
||||
|
||||
ENTITY_CLIMATE = "climate.fakespa_climate"
|
||||
|
||||
|
||||
async def test_spa_defaults(hass: HomeAssistant):
|
||||
"""Test supported features flags."""
|
||||
|
||||
await _setup_climate_test(hass)
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
assert (
|
||||
state.attributes["supported_features"]
|
||||
== SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
)
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 10.0
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 40.0
|
||||
assert state.attributes[ATTR_PRESET_MODE] == "Ready"
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
|
||||
|
||||
async def test_spa_defaults_fake_tscale(hass: HomeAssistant):
|
||||
"""Test supported features flags."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_tempscale", return_value=1
|
||||
):
|
||||
await _setup_climate_test(hass)
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
assert (
|
||||
state.attributes["supported_features"]
|
||||
== SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
)
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 10.0
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 40.0
|
||||
assert state.attributes[ATTR_PRESET_MODE] == "Ready"
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
|
||||
|
||||
async def test_spa_with_blower(hass: HomeAssistant):
|
||||
"""Test supported features flags."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.have_blower", return_value=True
|
||||
):
|
||||
config_entry = await _setup_climate_test(hass)
|
||||
|
||||
# force a refresh
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
assert (
|
||||
state.attributes["supported_features"]
|
||||
== SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE
|
||||
)
|
||||
|
||||
for fan_state in range(4):
|
||||
# set blower
|
||||
state = await _patch_blower(hass, config_entry, fan_state)
|
||||
assert state.attributes[ATTR_FAN_MODE] == FAN_SETTINGS[fan_state]
|
||||
|
||||
# test the nonsense checks
|
||||
for fan_state in (None, 70):
|
||||
state = await _patch_blower(hass, config_entry, fan_state)
|
||||
assert state.attributes[ATTR_FAN_MODE] == FAN_OFF
|
||||
|
||||
|
||||
async def test_spa_temperature(hass: HomeAssistant):
|
||||
"""Test spa temperature settings."""
|
||||
|
||||
config_entry = await _setup_climate_test(hass)
|
||||
|
||||
# flip the spa into F
|
||||
# set temp to a valid number
|
||||
state = await _patch_spa_settemp(hass, config_entry, 0, 100.0)
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 38.0
|
||||
|
||||
|
||||
async def test_spa_temperature_unit(hass: HomeAssistant):
|
||||
"""Test temperature unit conversions."""
|
||||
|
||||
with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT):
|
||||
config_entry = await _setup_climate_test(hass)
|
||||
|
||||
state = await _patch_spa_settemp(hass, config_entry, 0, 15.4)
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 15.0
|
||||
|
||||
|
||||
async def test_spa_hvac_modes(hass: HomeAssistant):
|
||||
"""Test hvac modes."""
|
||||
|
||||
config_entry = await _setup_climate_test(hass)
|
||||
|
||||
# try out the different heat modes
|
||||
for heat_mode in range(2):
|
||||
state = await _patch_spa_heatmode(hass, config_entry, heat_mode)
|
||||
modes = state.attributes.get(ATTR_HVAC_MODES)
|
||||
assert [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] == modes
|
||||
assert state.state == HVAC_SETTINGS[heat_mode]
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await _patch_spa_heatmode(hass, config_entry, 2)
|
||||
|
||||
|
||||
async def test_spa_hvac_action(hass: HomeAssistant):
|
||||
"""Test setting of the HVAC action."""
|
||||
|
||||
config_entry = await _setup_climate_test(hass)
|
||||
|
||||
# try out the different heat states
|
||||
state = await _patch_spa_heatstate(hass, config_entry, 1)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
|
||||
|
||||
state = await _patch_spa_heatstate(hass, config_entry, 0)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
|
||||
|
||||
async def test_spa_preset_modes(hass: HomeAssistant):
|
||||
"""Test the various preset modes."""
|
||||
|
||||
config_entry = await _setup_climate_test(hass)
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
modes = state.attributes.get(ATTR_PRESET_MODES)
|
||||
assert ["Ready", "Rest", "Ready in Rest"] == modes
|
||||
|
||||
# Put it in Ready and Rest
|
||||
modelist = ["Ready", "Rest"]
|
||||
for mode in modelist:
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_heatmode",
|
||||
return_value=modelist.index(mode),
|
||||
):
|
||||
await common.async_set_preset_mode(hass, mode, ENTITY_CLIMATE)
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes[ATTR_PRESET_MODE] == modelist.index(mode)
|
||||
|
||||
# put it in RNR and test assertion
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_heatmode",
|
||||
return_value=2,
|
||||
), pytest.raises(HomeAssistantError):
|
||||
await common.async_set_preset_mode(hass, 2, ENTITY_CLIMATE)
|
||||
|
||||
|
||||
# Helpers
|
||||
async def _patch_blower(hass, config_entry, fan_state):
|
||||
"""Patch the blower state."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_blower",
|
||||
return_value=fan_state,
|
||||
):
|
||||
if fan_state is not None and fan_state <= len(FAN_SETTINGS):
|
||||
await common.async_set_fan_mode(hass, FAN_SETTINGS[fan_state])
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
|
||||
async def _patch_spa_settemp(hass, config_entry, tscale, settemp):
|
||||
"""Patch the settemp."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_tempscale",
|
||||
return_value=tscale,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_settemp",
|
||||
return_value=settemp,
|
||||
):
|
||||
await common.async_set_temperature(
|
||||
hass, temperature=settemp, entity_id=ENTITY_CLIMATE
|
||||
)
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
|
||||
async def _patch_spa_heatmode(hass, config_entry, heat_mode):
|
||||
"""Patch the heatmode."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_heatmode",
|
||||
return_value=heat_mode,
|
||||
):
|
||||
await common.async_set_hvac_mode(hass, HVAC_SETTINGS[heat_mode], ENTITY_CLIMATE)
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
|
||||
async def _patch_spa_heatstate(hass, config_entry, heat_state):
|
||||
"""Patch the heatmode."""
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.get_heatstate",
|
||||
return_value=heat_state,
|
||||
):
|
||||
await common.async_set_hvac_mode(
|
||||
hass, HVAC_SETTINGS[heat_state], ENTITY_CLIMATE
|
||||
)
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return hass.states.get(ENTITY_CLIMATE)
|
||||
|
||||
|
||||
async def _setup_climate_test(hass):
|
||||
"""Prepare the test."""
|
||||
config_entry = await init_integration_mocked(hass)
|
||||
await async_setup_component(hass, BALBOA_DOMAIN, config_entry)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
167
tests/components/balboa/test_config_flow.py
Normal file
167
tests/components/balboa/test_config_flow.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""Test the Balboa Spa Client config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN
|
||||
from homeassistant.config_entries import 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,
|
||||
)
|
||||
|
||||
from . import BalboaMock
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_DATA = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
}
|
||||
TEST_ID = "FakeBalboa"
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect",
|
||||
new=BalboaMock.connect,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect",
|
||||
new=BalboaMock.disconnect,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.listen",
|
||||
new=BalboaMock.listen,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.send_mod_ident_req",
|
||||
new=BalboaMock.send_mod_ident_req,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.send_panel_req",
|
||||
new=BalboaMock.send_panel_req,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.spa_configured",
|
||||
new=BalboaMock.spa_configured,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
TEST_DATA,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["data"] == TEST_DATA
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect",
|
||||
new=BalboaMock.broken_connect,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect",
|
||||
new=BalboaMock.disconnect,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
TEST_DATA,
|
||||
)
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test we handle unknown error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
TEST_DATA,
|
||||
)
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_already_configured(hass: HomeAssistant) -> None:
|
||||
"""Test when provided credentials are already configured."""
|
||||
MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect",
|
||||
new=BalboaMock.connect,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect",
|
||||
new=BalboaMock.disconnect,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
TEST_DATA,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_options_flow(hass):
|
||||
"""Test specifying non default settings using options flow."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Rather than mocking out 15 or so functions, we just need to mock
|
||||
# the entire library, otherwise it will get stuck in a listener and
|
||||
# the various loops in pybalboa.
|
||||
with patch(
|
||||
"homeassistant.components.balboa.config_flow.BalboaSpaWifi",
|
||||
new=BalboaMock,
|
||||
), patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi",
|
||||
new=BalboaMock,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_SYNC_TIME: True},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {CONF_SYNC_TIME: True}
|
43
tests/components/balboa/test_init.py
Normal file
43
tests/components/balboa/test_init.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Tests of the initialization of the balboa integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import TEST_HOST, BalboaMock, init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_entry(hass: HomeAssistant):
|
||||
"""Validate that setup entry also configure the client."""
|
||||
config_entry = await init_integration(hass)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_setup_entry_fails(hass):
|
||||
"""Validate that setup entry also configure the client."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=BALBOA_DOMAIN,
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.balboa.BalboaSpaWifi.connect",
|
||||
new=BalboaMock.broken_connect,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
Loading…
x
Reference in New Issue
Block a user