Rewrite of Toon component (#21186)

* 🚜 Rewrite of Toon component

* 🔥 Removed manual state from list

* 👕 Addresses code review comments

* 🔥 Removes a log line that should not have been left behind

* 👕 Addresses linting warnings

* 👕 Addresses Hound CI warning

* 👕 Fixes small code styling issues

*  Sets an appropriate SCAN_INTERVAL

*  Sets min/max temperature for climate platform

* 👕 Makes imports more consistent with codebase

* 🚑 Fixes incorrect SCAN_INTERVAL value in climate platform

* 🚑 Uses OrderedDict for config_flow schema

* 👕 Adds return types for min/max temp

* 🚜 Refactors entities into their actual devices

* ⬆️ Updates toonapilib to 3.0.7

* 🚜 Refactors binary sensor state inversion

* 🚑 Fixes states of OpenTherm connection and Hot Tap Water

*  Adds Boiler Preheat binary sensor

*  Adds Toon Thermostat Program binary sensor

*  Adds Boiler Modulation Level sensor

*  Adds Daily Power Cost sensor

* 🔥 Cleanup of Toon Thermostat climate attributes

* 🚜 Adjusts config_flow with Tenant selection

* 🙋 Adds myself to codeowners file as maintainer

* ⬆️ Gen requirements

* ⬆️ Updates toonapilib to 3.0.9

*  Adds config_flow tests
This commit is contained in:
Franck Nijhof 2019-02-26 19:18:09 +01:00 committed by Paulus Schoutsen
parent f0268688be
commit f3c9327ccf
16 changed files with 943 additions and 389 deletions

View File

@ -265,6 +265,7 @@ homeassistant/components/tibber/* @danielhiversen
homeassistant/components/*/tibber.py @danielhiversen homeassistant/components/*/tibber.py @danielhiversen
homeassistant/components/tradfri/* @ggravlingen homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen
homeassistant/components/toon/* @frenck
# U # U
homeassistant/components/unifi/* @kane610 homeassistant/components/unifi/* @kane610

View File

@ -0,0 +1,34 @@
{
"config": {
"title": "Toon",
"step": {
"authenticate": {
"title": "Link your Toon account",
"description": "Authenticate with your Eneco Toon account (not the developer account).",
"data": {
"username": "Username",
"password": "Password",
"tenant": "Tenant"
}
},
"display": {
"title": "Select display",
"description": "Select the Toon display to connect with.",
"data": {
"display": "Choose display"
}
}
},
"error": {
"credentials": "The provided credentials are invalid.",
"display_exists": "The selected display is already configured."
},
"abort": {
"client_id": "The client ID from the configuration is invalid.",
"client_secret": "The client secret from the configuration is invalid.",
"unknown_auth_fail": "Unexpected error occured, while authenticating.",
"no_agreements": "This account has no Toon displays.",
"no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)."
}
}
}

View File

@ -1,142 +1,195 @@
"""Support for Toon van Eneco devices.""" """Support for Toon van Eneco devices."""
from datetime import datetime, timedelta
import logging import logging
from typing import Any, Dict
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import (config_validation as cv,
from homeassistant.helpers.discovery import load_platform device_registry as dr)
from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
REQUIREMENTS = ['toonlib==1.1.3'] from . import config_flow # noqa pylint_disable=unused-import
from .const import (
CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT,
DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN)
REQUIREMENTS = ['toonapilib==3.0.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_GAS = 'gas'
CONF_SOLAR = 'solar'
DEFAULT_GAS = True
DEFAULT_SOLAR = False
DOMAIN = 'toon'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
TOON_HANDLE = 'toon_handle'
# Validation of the user's configuration # Validation of the user's configuration
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_GAS, default=DEFAULT_GAS): cv.boolean,
vol.Optional(CONF_SOLAR, default=DEFAULT_SOLAR): cv.boolean,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
def setup(hass, config): async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the Toon component.""" """Set up the Toon components."""
from toonlib import InvalidCredentials if DOMAIN not in config:
gas = config[DOMAIN][CONF_GAS] return True
solar = config[DOMAIN][CONF_SOLAR]
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
try: conf = config[DOMAIN]
hass.data[TOON_HANDLE] = ToonDataStore(username, password, gas, solar)
except InvalidCredentials:
return False
for platform in ('climate', 'sensor', 'switch'): # Store config to be used during entry setup
load_platform(hass, platform, DOMAIN, {}, config) hass.data[DATA_TOON_CONFIG] = conf
return True return True
class ToonDataStore: async def async_setup_entry(hass: HomeAssistantType,
"""An object to store the Toon data.""" entry: ConfigType) -> bool:
"""Set up Toon from a config entry."""
from toonapilib import Toon
def __init__( conf = hass.data.get(DATA_TOON_CONFIG)
self, username, password, gas=DEFAULT_GAS, solar=DEFAULT_SOLAR):
"""Initialize Toon."""
from toonlib import Toon
toon = Toon(username, password) toon = Toon(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD],
conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET],
tenant_id=entry.data[CONF_TENANT],
display_common_name=entry.data[CONF_DISPLAY])
hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon
# Register device for the Meter Adapter, since it will have no entities.
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={
(DOMAIN, toon.agreement.id, 'meter_adapter'),
},
manufacturer='Eneco',
name="Meter Adapter",
via_hub=(DOMAIN, toon.agreement.id)
)
for component in 'binary_sensor', 'climate', 'sensor':
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component))
return True
class ToonEntity(Entity):
"""Defines a base Toon entity."""
def __init__(self, toon, name: str, icon: str) -> None:
"""Initialize the Toon entity."""
self._name = name
self._state = None
self._icon = icon
self.toon = toon self.toon = toon
self.gas = gas
self.solar = solar
self.data = {}
self.last_update = datetime.min @property
self.update() def name(self) -> str:
"""Return the name of the entity."""
return self._name
@Throttle(MIN_TIME_BETWEEN_UPDATES) @property
def update(self): def icon(self) -> str:
"""Update Toon data.""" """Return the mdi icon of the entity."""
self.last_update = datetime.now() return self._icon
self.data['power_current'] = self.toon.power.value
self.data['power_today'] = round(
(float(self.toon.power.daily_usage) +
float(self.toon.power.daily_usage_low)) / 1000, 2)
self.data['temp'] = self.toon.temperature
if self.toon.thermostat_state: class ToonDisplayDeviceEntity(ToonEntity):
self.data['state'] = self.toon.thermostat_state.name """Defines a Toon display device entity."""
else:
self.data['state'] = 'Manual'
self.data['setpoint'] = float( @property
self.toon.thermostat_info.current_set_point) / 100 def device_info(self) -> Dict[str, Any]:
self.data['gas_current'] = self.toon.gas.value """Return device information about this thermostat."""
self.data['gas_today'] = round(float(self.toon.gas.daily_usage) / agreement = self.toon.agreement
1000, 2) model = agreement.display_hardware_version.rpartition('/')[0]
sw_version = agreement.display_software_version.rpartition('/')[-1]
for plug in self.toon.smartplugs: return {
self.data[plug.name] = { 'identifiers': {
'current_power': plug.current_usage, (DOMAIN, agreement.id),
'today_energy': round(float(plug.daily_usage) / 1000, 2), },
'current_state': plug.current_state, 'name': 'Toon Display',
'is_connected': plug.is_connected, 'manufacturer': 'Eneco',
'model': model,
'sw_version': sw_version,
} }
self.data['solar_maximum'] = self.toon.solar.maximum
self.data['solar_produced'] = self.toon.solar.produced
self.data['solar_value'] = self.toon.solar.value
self.data['solar_average_produced'] = self.toon.solar.average_produced
self.data['solar_meter_reading_low_produced'] = \
self.toon.solar.meter_reading_low_produced
self.data['solar_meter_reading_produced'] = \
self.toon.solar.meter_reading_produced
self.data['solar_daily_cost_produced'] = \
self.toon.solar.daily_cost_produced
for detector in self.toon.smokedetectors: class ToonElectricityMeterDeviceEntity(ToonEntity):
value = '{}_smoke_detector'.format(detector.name) """Defines a Electricity Meter device entity."""
self.data[value] = {
'smoke_detector': detector.battery_level, @property
'device_type': detector.device_type, def device_info(self) -> Dict[str, Any]:
'is_connected': detector.is_connected, """Return device information about this entity."""
'last_connected_change': detector.last_connected_change, return {
'name': 'Electricity Meter',
'identifiers': {
(DOMAIN, self.toon.agreement.id, 'electricity'),
},
'via_hub': (DOMAIN, self.toon.agreement.id, 'meter_adapter'),
} }
def set_state(self, state):
"""Push a new state to the Toon unit."""
self.toon.thermostat_state = state
def set_temp(self, temp): class ToonGasMeterDeviceEntity(ToonEntity):
"""Push a new temperature to the Toon unit.""" """Defines a Gas Meter device entity."""
self.toon.thermostat = temp
def get_data(self, data_id, plug_name=None): @property
"""Get the cached data.""" def device_info(self) -> Dict[str, Any]:
data = {'error': 'no data'} """Return device information about this entity."""
if plug_name: via_hub = 'meter_adapter'
if data_id in self.data[plug_name]: if self.toon.gas.is_smart:
data = self.data[plug_name][data_id] via_hub = 'electricity'
else:
if data_id in self.data: return {
data = self.data[data_id] 'name': 'Gas Meter',
return data 'identifiers': {
(DOMAIN, self.toon.agreement.id, 'gas'),
},
'via_hub': (DOMAIN, self.toon.agreement.id, via_hub),
}
class ToonSolarDeviceEntity(ToonEntity):
"""Defines a Solar Device device entity."""
@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about this entity."""
return {
'name': 'Solar Panels',
'identifiers': {
(DOMAIN, self.toon.agreement.id, 'solar'),
},
'via_hub': (DOMAIN, self.toon.agreement.id, 'meter_adapter'),
}
class ToonBoilerModuleDeviceEntity(ToonEntity):
"""Defines a Boiler Module device entity."""
@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about this entity."""
return {
'name': 'Boiler Module',
'manufacturer': 'Eneco',
'identifiers': {
(DOMAIN, self.toon.agreement.id, 'boiler_module'),
},
'via_hub': (DOMAIN, self.toon.agreement.id),
}
class ToonBoilerDeviceEntity(ToonEntity):
"""Defines a Boiler device entity."""
@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about this entity."""
return {
'name': 'Boiler',
'identifiers': {
(DOMAIN, self.toon.agreement.id, 'boiler'),
},
'via_hub': (DOMAIN, self.toon.agreement.id, 'boiler_module'),
}

View File

@ -0,0 +1,127 @@
"""Support for Toon binary sensors."""
from datetime import timedelta
import logging
from typing import Any
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from . import (ToonEntity, ToonDisplayDeviceEntity, ToonBoilerDeviceEntity,
ToonBoilerModuleDeviceEntity)
from .const import DATA_TOON_CLIENT, DOMAIN
DEPENDENCIES = ['toon']
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
SCAN_INTERVAL = timedelta(seconds=30)
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry,
async_add_entities) -> None:
"""Set up a Toon binary sensor based on a config entry."""
toon = hass.data[DATA_TOON_CLIENT][entry.entry_id]
sensors = [
ToonBoilerModuleBinarySensor(toon, 'thermostat_info',
'boiler_connected', None,
'Boiler Module Connection',
'mdi:check-network-outline',
'connectivity'),
ToonDisplayBinarySensor(toon, 'thermostat_info', 'active_state', 4,
"Toon Holiday Mode", 'mdi:airport', None),
ToonDisplayBinarySensor(toon, 'thermostat_info', 'next_program', None,
"Toon Program", 'mdi:calendar-clock', None),
]
if toon.thermostat_info.have_ot_boiler:
sensors.extend([
ToonBoilerBinarySensor(toon, 'thermostat_info',
'ot_communication_error', '0',
"OpenTherm Connection",
'mdi:check-network-outline',
'connectivity'),
ToonBoilerBinarySensor(toon, 'thermostat_info', 'error_found', 255,
"Boiler Status", 'mdi:alert', 'problem',
inverted=True),
ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info',
None, "Boiler Burner", 'mdi:fire', None),
ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', '2',
"Hot Tap Water", 'mdi:water-pump', None),
ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', '3',
"Boiler Preheating", 'mdi:fire', None),
])
async_add_entities(sensors)
class ToonBinarySensor(ToonEntity, BinarySensorDevice):
"""Defines an Toon binary sensor."""
def __init__(self, toon, section: str, measurement: str, on_value: Any,
name: str, icon: str, device_class: str,
inverted: bool = False) -> None:
"""Initialize the Toon sensor."""
self._state = inverted
self._device_class = device_class
self.section = section
self.measurement = measurement
self.on_value = on_value
self.inverted = inverted
super().__init__(toon, name, icon)
@property
def unique_id(self) -> str:
"""Return the unique ID for this binary sensor."""
return '_'.join([DOMAIN, self.toon.agreement.id, 'binary_sensor',
self.section, self.measurement, str(self.on_value)])
@property
def device_class(self) -> str:
"""Return the device class."""
return self._device_class
@property
def is_on(self) -> bool:
"""Return the status of the binary sensor."""
if self.on_value is not None:
value = self._state == self.on_value
elif self._state is None:
value = False
else:
value = bool(max(0, int(self._state)))
if self.inverted:
return not value
return value
async def async_update(self) -> None:
"""Get the latest data from the binary sensor."""
section = getattr(self.toon, self.section)
self._state = getattr(section, self.measurement)
class ToonBoilerBinarySensor(ToonBinarySensor, ToonBoilerDeviceEntity):
"""Defines a Boiler binary sensor."""
pass
class ToonDisplayBinarySensor(ToonBinarySensor, ToonDisplayDeviceEntity):
"""Defines a Toon Display binary sensor."""
pass
class ToonBoilerModuleBinarySensor(ToonBinarySensor,
ToonBoilerModuleDeviceEntity):
"""Defines a Boiler module binary sensor."""
pass

View File

@ -1,90 +1,129 @@
"""Support for Toon van Eneco Thermostats.""" """Support for Toon thermostat."""
from datetime import timedelta
import logging
from typing import Any, Dict, List
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_OPERATION_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) SUPPORT_TARGET_TEMPERATURE)
import homeassistant.components.toon as toon_main from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.helpers.typing import HomeAssistantType
from . import ToonDisplayDeviceEntity
from .const import DATA_TOON_CLIENT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN
DEPENDENCIES = ['toon']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
SCAN_INTERVAL = timedelta(seconds=30)
HA_TOON = { HA_TOON = {
STATE_AUTO: 'Comfort', STATE_AUTO: 'Comfort',
STATE_HEAT: 'Home', STATE_HEAT: 'Home',
STATE_ECO: 'Away', STATE_ECO: 'Away',
STATE_COOL: 'Sleep', STATE_COOL: 'Sleep',
} }
TOON_HA = {value: key for key, value in HA_TOON.items()} TOON_HA = {value: key for key, value in HA_TOON.items()}
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry,
"""Set up the Toon climate device.""" async_add_entities) -> None:
add_entities([ThermostatDevice(hass)], True) """Set up a Toon binary sensors based on a config entry."""
toon = hass.data[DATA_TOON_CLIENT][entry.entry_id]
async_add_entities([ToonThermostatDevice(toon)], True)
class ThermostatDevice(ClimateDevice): class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice):
"""Representation of a Toon climate device.""" """Representation of a Toon climate device."""
def __init__(self, hass): def __init__(self, toon) -> None:
"""Initialize the Toon climate device.""" """Initialize the Toon climate device."""
self._name = 'Toon van Eneco'
self.hass = hass
self.thermos = hass.data[toon_main.TOON_HANDLE]
self._state = None self._state = None
self._temperature = None
self._setpoint = None self._current_temperature = None
self._operation_list = [ self._target_temperature = None
STATE_AUTO, self._next_target_temperature = None
STATE_HEAT,
STATE_ECO, self._heating_type = None
STATE_COOL,
] super().__init__(toon, "Toon Thermostat", 'mdi:thermostat')
@property @property
def supported_features(self): def unique_id(self) -> str:
"""Return the unique ID for this thermostat."""
return '_'.join([DOMAIN, self.toon.agreement.id, 'climate'])
@property
def supported_features(self) -> int:
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return SUPPORT_FLAGS
@property @property
def name(self): def temperature_unit(self) -> str:
"""Return the name of this thermostat.""" """Return the unit of measurement."""
return self._name
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
return TEMP_CELSIUS return TEMP_CELSIUS
@property @property
def current_operation(self): def current_operation(self) -> str:
"""Return current operation i.e. comfort, home, away.""" """Return current operation i.e. comfort, home, away."""
return TOON_HA.get(self.thermos.get_data('state')) return TOON_HA.get(self._state)
@property @property
def operation_list(self): def operation_list(self) -> List[str]:
"""Return a list of available operation modes.""" """Return a list of available operation modes."""
return self._operation_list return list(HA_TOON.keys())
@property @property
def current_temperature(self): def current_temperature(self) -> float:
"""Return the current temperature.""" """Return the current temperature."""
return self.thermos.get_data('temp') return self._current_temperature
@property @property
def target_temperature(self): def target_temperature(self) -> float:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self.thermos.get_data('setpoint') return self._target_temperature
def set_temperature(self, **kwargs): @property
def min_temp(self) -> float:
"""Return the minimum temperature."""
return DEFAULT_MIN_TEMP
@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
return DEFAULT_MAX_TEMP
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the current state of the burner."""
return {
'heating_type': self._heating_type,
}
def set_temperature(self, **kwargs) -> None:
"""Change the setpoint of the thermostat.""" """Change the setpoint of the thermostat."""
temp = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
self.thermos.set_temp(temp) self.toon.thermostat = temperature
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode: str) -> None:
"""Set new operation mode.""" """Set new operation mode."""
self.thermos.set_state(HA_TOON[operation_mode]) self.toon.thermostat_state = HA_TOON[operation_mode]
def update(self): async def async_update(self) -> None:
"""Update local state.""" """Update local state."""
self.thermos.update() if self.toon.thermostat_state is None:
self._state = None
else:
self._state = self.toon.thermostat_state.name
self._current_temperature = self.toon.temperature
self._target_temperature = self.toon.thermostat
self._heating_type = self.toon.agreement.heating_type

View File

@ -0,0 +1,158 @@
"""Config flow to configure the Toon component."""
from collections import OrderedDict
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from .const import (
CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT,
DATA_TOON_CONFIG, DOMAIN)
_LOGGER = logging.getLogger(__name__)
@callback
def configured_displays(hass):
"""Return a set of configured Toon displays."""
return set(
entry.data[CONF_DISPLAY]
for entry in hass.config_entries.async_entries(DOMAIN)
)
@config_entries.HANDLERS.register(DOMAIN)
class ToonFlowHandler(config_entries.ConfigFlow):
"""Handle a Toon config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the Toon flow."""
self.displays = None
self.username = None
self.password = None
self.tenant = None
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
app = self.hass.data.get(DATA_TOON_CONFIG, {})
if not app:
return self.async_abort(reason='no_app')
return await self.async_step_authenticate(user_input)
async def _show_authenticaticate_form(self, errors=None):
"""Show the authentication form to the user."""
fields = OrderedDict()
fields[vol.Required(CONF_USERNAME)] = str
fields[vol.Required(CONF_PASSWORD)] = str
fields[vol.Optional(CONF_TENANT)] = vol.In([
'eneco', 'electrabel', 'viesgo'
])
return self.async_show_form(
step_id='authenticate',
data_schema=vol.Schema(fields),
errors=errors if errors else {},
)
async def async_step_authenticate(self, user_input=None):
"""Attempt to authenticate with the Toon account."""
from toonapilib import Toon
from toonapilib.toonapilibexceptions import (InvalidConsumerSecret,
InvalidConsumerKey,
InvalidCredentials,
AgreementsRetrievalError)
if user_input is None:
return await self._show_authenticaticate_form()
app = self.hass.data.get(DATA_TOON_CONFIG, {})
try:
toon = Toon(user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
app[CONF_CLIENT_ID],
app[CONF_CLIENT_SECRET],
tenant_id=user_input[CONF_TENANT])
displays = toon.display_names
except InvalidConsumerKey:
return self.async_abort(reason='client_id')
except InvalidConsumerSecret:
return self.async_abort(reason='client_secret')
except InvalidCredentials:
return await self._show_authenticaticate_form({
'base': 'credentials'
})
except AgreementsRetrievalError:
return self.async_abort(reason='no_agreements')
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error while authenticating")
return self.async_abort(reason='unknown_auth_fail')
self.displays = displays
self.username = user_input[CONF_USERNAME]
self.password = user_input[CONF_PASSWORD]
self.tenant = user_input[CONF_TENANT]
return await self.async_step_display()
async def _show_display_form(self, errors=None):
"""Show the select display form to the user."""
fields = OrderedDict()
fields[vol.Required(CONF_DISPLAY)] = vol.In(self.displays)
return self.async_show_form(
step_id='display',
data_schema=vol.Schema(fields),
errors=errors if errors else {},
)
async def async_step_display(self, user_input=None):
"""Select Toon display to add."""
from toonapilib import Toon
if not self.displays:
return self.async_abort(reason='no_displays')
if user_input is None:
return await self._show_display_form()
if user_input[CONF_DISPLAY] in configured_displays(self.hass):
return await self._show_display_form({
'base': 'display_exists'
})
app = self.hass.data.get(DATA_TOON_CONFIG, {})
try:
Toon(self.username,
self.password,
app[CONF_CLIENT_ID],
app[CONF_CLIENT_SECRET],
tenant_id=self.tenant,
display_common_name=user_input[CONF_DISPLAY])
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error while authenticating")
return self.async_abort(reason='unknown_auth_fail')
return self.async_create_entry(
title=user_input[CONF_DISPLAY],
data={
CONF_USERNAME: self.username,
CONF_PASSWORD: self.password,
CONF_TENANT: self.tenant,
CONF_DISPLAY: user_input[CONF_DISPLAY]
}
)

View File

@ -0,0 +1,21 @@
"""Constants for the Toon integration."""
DOMAIN = 'toon'
DATA_TOON = 'toon'
DATA_TOON_CONFIG = 'toon_config'
DATA_TOON_CLIENT = 'toon_client'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_DISPLAY = 'display'
CONF_TENANT = 'tenant'
DEFAULT_MAX_TEMP = 30.0
DEFAULT_MIN_TEMP = 6.0
CURRENCY_EUR = 'EUR'
POWER_WATT = 'Watt'
POWER_KWH = 'kWh'
RATIO_PERCENT = '%'
VOLUME_CM3 = 'CM3'
VOLUME_M3 = 'M3'

View File

@ -1,217 +1,189 @@
"""Support for rebranded Quby thermostat as provided by Eneco.""" """Support for Toon sensors."""
from datetime import timedelta
import logging import logging
import datetime
from homeassistant.helpers.entity import Entity from homeassistant.config_entries import ConfigEntry
import homeassistant.components.toon as toon_main from homeassistant.helpers.typing import HomeAssistantType
from . import (ToonEntity, ToonElectricityMeterDeviceEntity,
ToonGasMeterDeviceEntity, ToonSolarDeviceEntity,
ToonBoilerDeviceEntity)
from .const import (CURRENCY_EUR, DATA_TOON_CLIENT, DOMAIN, POWER_KWH,
POWER_WATT, VOLUME_CM3, VOLUME_M3, RATIO_PERCENT)
DEPENDENCIES = ['toon']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATE_ATTR_DEVICE_TYPE = 'device_type' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
STATE_ATTR_LAST_CONNECTED_CHANGE = 'last_connected_change' SCAN_INTERVAL = timedelta(seconds=30)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry,
"""Set up the Toon sensors.""" async_add_entities) -> None:
_toon_main = hass.data[toon_main.TOON_HANDLE] """Set up Toon sensors based on a config entry."""
toon = hass.data[DATA_TOON_CLIENT][entry.entry_id]
sensor_items = [] sensors = [
sensor_items.extend([ ToonElectricityMeterDeviceSensor(toon, 'power', 'value',
ToonSensor(hass, 'Power_current', 'power-plug', 'Watt'), "Current Power Usage",
ToonSensor(hass, 'Power_today', 'power-plug', 'kWh'), 'mdi:power-plug', POWER_WATT),
ToonElectricityMeterDeviceSensor(toon, 'power', 'average',
"Average Power Usage",
'mdi:power-plug', POWER_WATT),
ToonElectricityMeterDeviceSensor(toon, 'power', 'daily_value',
"Power Usage Today",
'mdi:power-plug', POWER_KWH),
ToonElectricityMeterDeviceSensor(toon, 'power', 'daily_cost',
"Power Cost Today",
'mdi:power-plug', CURRENCY_EUR),
ToonElectricityMeterDeviceSensor(toon, 'power', 'average_daily',
"Average Daily Power Usage",
'mdi:power-plug', POWER_KWH),
ToonElectricityMeterDeviceSensor(toon, 'power', 'meter_reading',
"Power Meter Feed IN Tariff 1",
'mdi:power-plug', POWER_KWH),
ToonElectricityMeterDeviceSensor(toon, 'power', 'meter_reading_low',
"Power Meter Feed IN Tariff 2",
'mdi:power-plug', POWER_KWH),
]
if toon.gas:
sensors.extend([
ToonGasMeterDeviceSensor(toon, 'gas', 'value', "Current Gas Usage",
'mdi:gas-cylinder', VOLUME_CM3),
ToonGasMeterDeviceSensor(toon, 'gas', 'average',
"Average Gas Usage", 'mdi:gas-cylinder',
VOLUME_CM3),
ToonGasMeterDeviceSensor(toon, 'gas', 'daily_usage',
"Gas Usage Today", 'mdi:gas-cylinder',
VOLUME_M3),
ToonGasMeterDeviceSensor(toon, 'gas', 'average_daily',
"Average Daily Gas Usage",
'mdi:gas-cylinder', VOLUME_M3),
ToonGasMeterDeviceSensor(toon, 'gas', 'meter_reading', "Gas Meter",
'mdi:gas-cylinder', VOLUME_M3),
ToonGasMeterDeviceSensor(toon, 'gas', 'daily_cost',
"Gas Cost Today", 'mdi:gas-cylinder',
CURRENCY_EUR),
]) ])
if _toon_main.gas: if toon.solar:
sensor_items.extend([ sensors.extend([
ToonSensor(hass, 'Gas_current', 'gas-cylinder', 'CM3'), ToonSolarDeviceSensor(toon, 'solar', 'value',
ToonSensor(hass, 'Gas_today', 'gas-cylinder', 'M3'), "Current Solar Production",
'mdi:solar-power', POWER_WATT),
ToonSolarDeviceSensor(toon, 'solar', 'maximum',
"Max Solar Production", 'mdi:solar-power',
POWER_WATT),
ToonSolarDeviceSensor(toon, 'solar', 'produced',
"Solar Production to Grid",
'mdi:solar-power', POWER_WATT),
ToonSolarDeviceSensor(toon, 'solar', 'average_produced',
"Average Solar Production to Grid",
'mdi:solar-power', POWER_WATT),
ToonElectricityMeterDeviceSensor(toon, 'solar',
'meter_reading_produced',
"Power Meter Feed OUT Tariff 1",
'mdi:solar-power',
POWER_KWH),
ToonElectricityMeterDeviceSensor(toon, 'solar',
'meter_reading_low_produced',
"Power Meter Feed OUT Tariff 2",
'mdi:solar-power', POWER_KWH),
]) ])
for plug in _toon_main.toon.smartplugs: if toon.thermostat_info.have_ot_boiler:
sensor_items.extend([ sensors.extend([
FibaroSensor(hass, '{}_current_power'.format(plug.name), ToonBoilerDeviceSensor(toon, 'thermostat_info',
plug.name, 'power-socket-eu', 'Watt'), 'current_modulation_level',
FibaroSensor(hass, '{}_today_energy'.format(plug.name), "Boiler Modulation Level",
plug.name, 'power-socket-eu', 'kWh'), 'mdi:percent',
RATIO_PERCENT),
]) ])
if _toon_main.toon.solar.produced or _toon_main.solar: async_add_entities(sensors)
sensor_items.extend([
SolarSensor(hass, 'Solar_maximum', 'kWh'),
SolarSensor(hass, 'Solar_produced', 'kWh'),
SolarSensor(hass, 'Solar_value', 'Watt'),
SolarSensor(hass, 'Solar_average_produced', 'kWh'),
SolarSensor(hass, 'Solar_meter_reading_low_produced', 'kWh'),
SolarSensor(hass, 'Solar_meter_reading_produced', 'kWh'),
SolarSensor(hass, 'Solar_daily_cost_produced', 'Euro'),
])
for smokedetector in _toon_main.toon.smokedetectors:
sensor_items.append(
FibaroSmokeDetector(
hass, '{}_smoke_detector'.format(smokedetector.name),
smokedetector.device_uuid, 'alarm-bell', '%')
)
add_entities(sensor_items)
class ToonSensor(Entity): class ToonSensor(ToonEntity):
"""Representation of a Toon sensor.""" """Defines a Toon sensor."""
def __init__(self, hass, name, icon, unit_of_measurement): def __init__(self, toon, section: str, measurement: str,
name: str, icon: str, unit_of_measurement: str) -> None:
"""Initialize the Toon sensor.""" """Initialize the Toon sensor."""
self._name = name
self._state = None self._state = None
self._icon = 'mdi:{}'.format(icon)
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self.thermos = hass.data[toon_main.TOON_HANDLE] self.section = section
self.measurement = measurement
super().__init__(toon, name, icon)
@property @property
def name(self): def unique_id(self) -> str:
"""Return the name of the sensor.""" """Return the unique ID for this sensor."""
return self._name return '_'.join([DOMAIN, self.toon.agreement.id, 'sensor',
self.section, self.measurement])
@property
def icon(self):
"""Return the mdi icon of the sensor."""
return self._icon
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.thermos.get_data(self.name.lower()) return self._state
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str:
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
return self._unit_of_measurement return self._unit_of_measurement
def update(self): async def async_update(self) -> None:
"""Get the latest data from the sensor.""" """Get the latest data from the sensor."""
self.thermos.update() section = getattr(self.toon, self.section)
value = None
if self.section == 'power' and self.measurement == 'daily_value':
value = round((float(section.daily_usage)
+ float(section.daily_usage_low)) / 1000.0, 2)
if value is None:
value = getattr(section, self.measurement)
if self.section == 'power' and \
self.measurement in ['meter_reading', 'meter_reading_low',
'average_daily']:
value = round(float(value)/1000.0, 2)
if self.section == 'solar' and \
self.measurement in ['meter_reading_produced',
'meter_reading_low_produced']:
value = float(value)/1000.0
if self.section == 'gas' and \
self.measurement in ['average_daily', 'daily_usage',
'meter_reading']:
value = round(float(value)/1000.0, 2)
self._state = max(0, value)
class FibaroSensor(Entity): class ToonElectricityMeterDeviceSensor(ToonSensor,
"""Representation of a Fibaro sensor.""" ToonElectricityMeterDeviceEntity):
"""Defines a Eletricity Meter sensor."""
def __init__(self, hass, name, plug_name, icon, unit_of_measurement): pass
"""Initialize the Fibaro sensor."""
self._name = name
self._plug_name = plug_name
self._state = None
self._icon = 'mdi:{}'.format(icon)
self._unit_of_measurement = unit_of_measurement
self.toon = hass.data[toon_main.TOON_HANDLE]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def icon(self):
"""Return the mdi icon of the sensor."""
return self._icon
@property
def state(self):
"""Return the state of the sensor."""
value = '_'.join(self.name.lower().split('_')[1:])
return self.toon.get_data(value, self._plug_name)
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
def update(self):
"""Get the latest data from the sensor."""
self.toon.update()
class SolarSensor(Entity): class ToonGasMeterDeviceSensor(ToonSensor, ToonGasMeterDeviceEntity):
"""Representation of a Solar sensor.""" """Defines a Gas Meter sensor."""
def __init__(self, hass, name, unit_of_measurement): pass
"""Initialize the Solar sensor."""
self._name = name
self._state = None
self._icon = 'mdi:weather-sunny'
self._unit_of_measurement = unit_of_measurement
self.toon = hass.data[toon_main.TOON_HANDLE]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def icon(self):
"""Return the mdi icon of the sensor."""
return self._icon
@property
def state(self):
"""Return the state of the sensor."""
return self.toon.get_data(self.name.lower())
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
def update(self):
"""Get the latest data from the sensor."""
self.toon.update()
class FibaroSmokeDetector(Entity): class ToonSolarDeviceSensor(ToonSensor, ToonSolarDeviceEntity):
"""Representation of a Fibaro smoke detector.""" """Defines a Solar sensor."""
def __init__(self, hass, name, uid, icon, unit_of_measurement): pass
"""Initialize the Fibaro smoke sensor."""
self._name = name
self._uid = uid
self._state = None
self._icon = 'mdi:{}'.format(icon)
self._unit_of_measurement = unit_of_measurement
self.toon = hass.data[toon_main.TOON_HANDLE]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property class ToonBoilerDeviceSensor(ToonSensor, ToonBoilerDeviceEntity):
def icon(self): """Defines a Boiler sensor."""
"""Return the mdi icon of the sensor."""
return self._icon
@property pass
def state_attributes(self):
"""Return the state attributes of the smoke detectors."""
value = datetime.datetime.fromtimestamp(
int(self.toon.get_data('last_connected_change', self.name))
).strftime('%Y-%m-%d %H:%M:%S')
return {
STATE_ATTR_DEVICE_TYPE:
self.toon.get_data('device_type', self.name),
STATE_ATTR_LAST_CONNECTED_CHANGE: value,
}
@property
def state(self):
"""Return the state of the sensor."""
value = self.name.lower().split('_', 1)[1]
return self.toon.get_data(value, self.name)
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
def update(self):
"""Get the latest data from the sensor."""
self.toon.update()

View File

@ -0,0 +1,34 @@
{
"config": {
"title": "Toon",
"step": {
"authenticate": {
"title": "Link your Toon account",
"description": "Authenticate with your Eneco Toon account (not the developer account).",
"data": {
"username": "Username",
"password": "Password",
"tenant": "Tenant"
}
},
"display": {
"title": "Select display",
"description": "Select the Toon display to connect with.",
"data": {
"display": "Choose display"
}
}
},
"error": {
"credentials": "The provided credentials are invalid.",
"display_exists": "The selected display is already configured."
},
"abort": {
"client_id": "The client ID from the configuration is invalid.",
"client_secret": "The client secret from the configuration is invalid.",
"unknown_auth_fail": "Unexpected error occured, while authenticating.",
"no_agreements": "This account has no Toon displays.",
"no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)."
}
}
}

View File

@ -1,68 +0,0 @@
"""Support for Eneco Slimmer stekkers (Smart Plugs)."""
import logging
from homeassistant.components.switch import SwitchDevice
import homeassistant.components.toon as toon_main
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the discovered Toon Smart Plugs."""
_toon_main = hass.data[toon_main.TOON_HANDLE]
switch_items = []
for plug in _toon_main.toon.smartplugs:
switch_items.append(EnecoSmartPlug(hass, plug))
add_entities(switch_items)
class EnecoSmartPlug(SwitchDevice):
"""Representation of a Toon Smart Plug."""
def __init__(self, hass, plug):
"""Initialize the Smart Plug."""
self.smartplug = plug
self.toon_data_store = hass.data[toon_main.TOON_HANDLE]
@property
def unique_id(self):
"""Return the ID of this switch."""
return self.smartplug.device_uuid
@property
def name(self):
"""Return the name of the switch if any."""
return self.smartplug.name
@property
def current_power_w(self):
"""Return the current power usage in W."""
return self.toon_data_store.get_data('current_power', self.name)
@property
def today_energy_kwh(self):
"""Return the today total energy usage in kWh."""
return self.toon_data_store.get_data('today_energy', self.name)
@property
def is_on(self):
"""Return true if switch is on. Standby is on."""
return self.toon_data_store.get_data('current_state', self.name)
@property
def available(self):
"""Return true if switch is available."""
return self.smartplug.can_toggle
def turn_on(self, **kwargs):
"""Turn the switch on."""
return self.smartplug.turn_on()
def turn_off(self, **kwargs):
"""Turn the switch off."""
return self.smartplug.turn_off()
def update(self):
"""Update state."""
self.toon_data_store.update()

View File

@ -172,6 +172,7 @@ FLOWS = [
'smhi', 'smhi',
'sonos', 'sonos',
'tellduslive', 'tellduslive',
'toon',
'tplink', 'tplink',
'tradfri', 'tradfri',
'twilio', 'twilio',

View File

@ -1685,7 +1685,7 @@ tikteck==0.4
todoist-python==7.0.17 todoist-python==7.0.17
# homeassistant.components.toon # homeassistant.components.toon
toonlib==1.1.3 toonapilib==3.0.9
# homeassistant.components.alarm_control_panel.totalconnect # homeassistant.components.alarm_control_panel.totalconnect
total_connect_client==0.22 total_connect_client==0.22

View File

@ -290,6 +290,9 @@ srpenergy==1.0.5
# homeassistant.components.statsd # homeassistant.components.statsd
statsd==3.2.1 statsd==3.2.1
# homeassistant.components.toon
toonapilib==3.0.9
# homeassistant.components.camera.uvc # homeassistant.components.camera.uvc
uvcclient==0.11.0 uvcclient==0.11.0

View File

@ -118,6 +118,7 @@ TEST_REQUIREMENTS = (
'sqlalchemy', 'sqlalchemy',
'srpenergy', 'srpenergy',
'statsd', 'statsd',
'toonapilib',
'uvcclient', 'uvcclient',
'vsure', 'vsure',
'warrant', 'warrant',

View File

@ -0,0 +1 @@
"""Tests for the Toon component."""

View File

@ -0,0 +1,177 @@
"""Tests for the Toon config flow."""
from unittest.mock import patch
import pytest
from toonapilib.toonapilibexceptions import (
AgreementsRetrievalError, InvalidConsumerKey, InvalidConsumerSecret,
InvalidCredentials)
from homeassistant import data_entry_flow
from homeassistant.components.toon import config_flow
from homeassistant.components.toon.const import (
CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DOMAIN)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, MockDependency
FIXTURE_APP = {
DOMAIN: {
CONF_CLIENT_ID: '1234567890abcdef',
CONF_CLIENT_SECRET: '1234567890abcdef',
}
}
FIXTURE_CREDENTIALS = {
CONF_USERNAME: 'john.doe',
CONF_PASSWORD: 'secret',
CONF_TENANT: 'eneco'
}
FIXTURE_DISPLAY = {
CONF_DISPLAY: 'display1'
}
@pytest.fixture
def mock_toonapilib():
"""Mock toonapilib."""
with MockDependency('toonapilib') as mock_toonapilib_:
mock_toonapilib_.Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]]
yield mock_toonapilib_
async def setup_component(hass):
"""Set up Toon component."""
with patch('os.path.isfile', return_value=False):
assert await async_setup_component(hass, DOMAIN, FIXTURE_APP)
await hass.async_block_till_done()
async def test_abort_if_no_app_configured(hass):
"""Test abort if no app is configured."""
flow = config_flow.ToonFlowHandler()
flow.hass = hass
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_app'
async def test_show_authenticate_form(hass):
"""Test that the authentication form is served."""
await setup_component(hass)
flow = config_flow.ToonFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'authenticate'
@pytest.mark.parametrize('side_effect,reason',
[(InvalidConsumerKey, 'client_id'),
(InvalidConsumerSecret, 'client_secret'),
(AgreementsRetrievalError, 'no_agreements'),
(Exception, 'unknown_auth_fail')])
async def test_toon_abort(hass, mock_toonapilib, side_effect, reason):
"""Test we abort on Toon error."""
await setup_component(hass)
flow = config_flow.ToonFlowHandler()
flow.hass = hass
mock_toonapilib.Toon.side_effect = side_effect
result = await flow.async_step_authenticate(user_input=FIXTURE_CREDENTIALS)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == reason
async def test_invalid_credentials(hass, mock_toonapilib):
"""Test we show authentication form on Toon auth error."""
mock_toonapilib.Toon.side_effect = InvalidCredentials
await setup_component(hass)
flow = config_flow.ToonFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=FIXTURE_CREDENTIALS)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'authenticate'
assert result['errors'] == {'base': 'credentials'}
async def test_full_flow_implementation(hass, mock_toonapilib):
"""Test registering an integration and finishing flow works."""
await setup_component(hass)
flow = config_flow.ToonFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'authenticate'
result = await flow.async_step_user(user_input=FIXTURE_CREDENTIALS)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'display'
result = await flow.async_step_display(user_input=FIXTURE_DISPLAY)
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['title'] == FIXTURE_DISPLAY[CONF_DISPLAY]
assert result['data'][CONF_USERNAME] == FIXTURE_CREDENTIALS[CONF_USERNAME]
assert result['data'][CONF_PASSWORD] == FIXTURE_CREDENTIALS[CONF_PASSWORD]
assert result['data'][CONF_TENANT] == FIXTURE_CREDENTIALS[CONF_TENANT]
assert result['data'][CONF_DISPLAY] == FIXTURE_DISPLAY[CONF_DISPLAY]
async def test_no_displays(hass, mock_toonapilib):
"""Test abort when there are no displays."""
await setup_component(hass)
mock_toonapilib.Toon().display_names = []
flow = config_flow.ToonFlowHandler()
flow.hass = hass
await flow.async_step_user(user_input=FIXTURE_CREDENTIALS)
result = await flow.async_step_display(user_input=None)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_displays'
async def test_display_already_exists(hass, mock_toonapilib):
"""Test showing display form again if display already exists."""
await setup_component(hass)
flow = config_flow.ToonFlowHandler()
flow.hass = hass
await flow.async_step_user(user_input=FIXTURE_CREDENTIALS)
MockConfigEntry(domain=DOMAIN, data=FIXTURE_DISPLAY).add_to_hass(hass)
result = await flow.async_step_display(user_input=FIXTURE_DISPLAY)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'display'
assert result['errors'] == {'base': 'display_exists'}
async def test_abort_last_minute_fail(hass, mock_toonapilib):
"""Test we abort when API communication fails in the last step."""
await setup_component(hass)
flow = config_flow.ToonFlowHandler()
flow.hass = hass
await flow.async_step_user(user_input=FIXTURE_CREDENTIALS)
mock_toonapilib.Toon.side_effect = Exception
result = await flow.async_step_display(user_input=FIXTURE_DISPLAY)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'unknown_auth_fail'