mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
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:
parent
f0268688be
commit
f3c9327ccf
@ -265,6 +265,7 @@ homeassistant/components/tibber/* @danielhiversen
|
||||
homeassistant/components/*/tibber.py @danielhiversen
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
homeassistant/components/toon/* @frenck
|
||||
|
||||
# U
|
||||
homeassistant/components/unifi/* @kane610
|
||||
|
34
homeassistant/components/toon/.translations/en.json
Normal file
34
homeassistant/components/toon/.translations/en.json
Normal 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/)."
|
||||
}
|
||||
}
|
||||
}
|
@ -1,142 +1,195 @@
|
||||
"""Support for Toon van Eneco devices."""
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers import (config_validation as cv,
|
||||
device_registry as dr)
|
||||
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__)
|
||||
|
||||
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
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_GAS, default=DEFAULT_GAS): cv.boolean,
|
||||
vol.Optional(CONF_SOLAR, default=DEFAULT_SOLAR): cv.boolean,
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Toon component."""
|
||||
from toonlib import InvalidCredentials
|
||||
gas = config[DOMAIN][CONF_GAS]
|
||||
solar = config[DOMAIN][CONF_SOLAR]
|
||||
username = config[DOMAIN][CONF_USERNAME]
|
||||
password = config[DOMAIN][CONF_PASSWORD]
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Set up the Toon components."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
try:
|
||||
hass.data[TOON_HANDLE] = ToonDataStore(username, password, gas, solar)
|
||||
except InvalidCredentials:
|
||||
return False
|
||||
conf = config[DOMAIN]
|
||||
|
||||
for platform in ('climate', 'sensor', 'switch'):
|
||||
load_platform(hass, platform, DOMAIN, {}, config)
|
||||
# Store config to be used during entry setup
|
||||
hass.data[DATA_TOON_CONFIG] = conf
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ToonDataStore:
|
||||
"""An object to store the Toon data."""
|
||||
async def async_setup_entry(hass: HomeAssistantType,
|
||||
entry: ConfigType) -> bool:
|
||||
"""Set up Toon from a config entry."""
|
||||
from toonapilib import Toon
|
||||
|
||||
def __init__(
|
||||
self, username, password, gas=DEFAULT_GAS, solar=DEFAULT_SOLAR):
|
||||
"""Initialize Toon."""
|
||||
from toonlib import Toon
|
||||
conf = hass.data.get(DATA_TOON_CONFIG)
|
||||
|
||||
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.gas = gas
|
||||
self.solar = solar
|
||||
self.data = {}
|
||||
|
||||
self.last_update = datetime.min
|
||||
self.update()
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update Toon data."""
|
||||
self.last_update = datetime.now()
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the mdi icon of the entity."""
|
||||
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:
|
||||
self.data['state'] = self.toon.thermostat_state.name
|
||||
else:
|
||||
self.data['state'] = 'Manual'
|
||||
class ToonDisplayDeviceEntity(ToonEntity):
|
||||
"""Defines a Toon display device entity."""
|
||||
|
||||
self.data['setpoint'] = float(
|
||||
self.toon.thermostat_info.current_set_point) / 100
|
||||
self.data['gas_current'] = self.toon.gas.value
|
||||
self.data['gas_today'] = round(float(self.toon.gas.daily_usage) /
|
||||
1000, 2)
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this thermostat."""
|
||||
agreement = self.toon.agreement
|
||||
model = agreement.display_hardware_version.rpartition('/')[0]
|
||||
sw_version = agreement.display_software_version.rpartition('/')[-1]
|
||||
return {
|
||||
'identifiers': {
|
||||
(DOMAIN, agreement.id),
|
||||
},
|
||||
'name': 'Toon Display',
|
||||
'manufacturer': 'Eneco',
|
||||
'model': model,
|
||||
'sw_version': sw_version,
|
||||
}
|
||||
|
||||
for plug in self.toon.smartplugs:
|
||||
self.data[plug.name] = {
|
||||
'current_power': plug.current_usage,
|
||||
'today_energy': round(float(plug.daily_usage) / 1000, 2),
|
||||
'current_state': plug.current_state,
|
||||
'is_connected': plug.is_connected,
|
||||
}
|
||||
|
||||
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
|
||||
class ToonElectricityMeterDeviceEntity(ToonEntity):
|
||||
"""Defines a Electricity Meter device entity."""
|
||||
|
||||
for detector in self.toon.smokedetectors:
|
||||
value = '{}_smoke_detector'.format(detector.name)
|
||||
self.data[value] = {
|
||||
'smoke_detector': detector.battery_level,
|
||||
'device_type': detector.device_type,
|
||||
'is_connected': detector.is_connected,
|
||||
'last_connected_change': detector.last_connected_change,
|
||||
}
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this entity."""
|
||||
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):
|
||||
"""Push a new temperature to the Toon unit."""
|
||||
self.toon.thermostat = temp
|
||||
class ToonGasMeterDeviceEntity(ToonEntity):
|
||||
"""Defines a Gas Meter device entity."""
|
||||
|
||||
def get_data(self, data_id, plug_name=None):
|
||||
"""Get the cached data."""
|
||||
data = {'error': 'no data'}
|
||||
if plug_name:
|
||||
if data_id in self.data[plug_name]:
|
||||
data = self.data[plug_name][data_id]
|
||||
else:
|
||||
if data_id in self.data:
|
||||
data = self.data[data_id]
|
||||
return data
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this entity."""
|
||||
via_hub = 'meter_adapter'
|
||||
if self.toon.gas.is_smart:
|
||||
via_hub = 'electricity'
|
||||
|
||||
return {
|
||||
'name': 'Gas Meter',
|
||||
'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'),
|
||||
}
|
||||
|
127
homeassistant/components/toon/binary_sensor.py
Normal file
127
homeassistant/components/toon/binary_sensor.py
Normal 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
|
@ -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.const import (
|
||||
STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
import homeassistant.components.toon as toon_main
|
||||
STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
HA_TOON = {
|
||||
STATE_AUTO: 'Comfort',
|
||||
STATE_HEAT: 'Home',
|
||||
STATE_ECO: 'Away',
|
||||
STATE_COOL: 'Sleep',
|
||||
}
|
||||
|
||||
TOON_HA = {value: key for key, value in HA_TOON.items()}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Toon climate device."""
|
||||
add_entities([ThermostatDevice(hass)], True)
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry,
|
||||
async_add_entities) -> None:
|
||||
"""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."""
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, toon) -> None:
|
||||
"""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._temperature = None
|
||||
self._setpoint = None
|
||||
self._operation_list = [
|
||||
STATE_AUTO,
|
||||
STATE_HEAT,
|
||||
STATE_ECO,
|
||||
STATE_COOL,
|
||||
]
|
||||
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
self._next_target_temperature = None
|
||||
|
||||
self._heating_type = None
|
||||
|
||||
super().__init__(toon, "Toon Thermostat", 'mdi:thermostat')
|
||||
|
||||
@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 SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this thermostat."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def current_operation(self) -> str:
|
||||
"""Return current operation i.e. comfort, home, away."""
|
||||
return TOON_HA.get(self.thermos.get_data('state'))
|
||||
return TOON_HA.get(self._state)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
def operation_list(self) -> List[str]:
|
||||
"""Return a list of available operation modes."""
|
||||
return self._operation_list
|
||||
return list(HA_TOON.keys())
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
def current_temperature(self) -> float:
|
||||
"""Return the current temperature."""
|
||||
return self.thermos.get_data('temp')
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
def target_temperature(self) -> float:
|
||||
"""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."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.thermos.set_temp(temp)
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.toon.thermostat = temperature
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
def set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""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."""
|
||||
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
|
||||
|
158
homeassistant/components/toon/config_flow.py
Normal file
158
homeassistant/components/toon/config_flow.py
Normal 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]
|
||||
}
|
||||
)
|
21
homeassistant/components/toon/const.py
Normal file
21
homeassistant/components/toon/const.py
Normal 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'
|
@ -1,217 +1,189 @@
|
||||
"""Support for rebranded Quby thermostat as provided by Eneco."""
|
||||
"""Support for Toon sensors."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.components.toon as toon_main
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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__)
|
||||
|
||||
STATE_ATTR_DEVICE_TYPE = 'device_type'
|
||||
STATE_ATTR_LAST_CONNECTED_CHANGE = 'last_connected_change'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Toon sensors."""
|
||||
_toon_main = hass.data[toon_main.TOON_HANDLE]
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry,
|
||||
async_add_entities) -> None:
|
||||
"""Set up Toon sensors based on a config entry."""
|
||||
toon = hass.data[DATA_TOON_CLIENT][entry.entry_id]
|
||||
|
||||
sensor_items = []
|
||||
sensor_items.extend([
|
||||
ToonSensor(hass, 'Power_current', 'power-plug', 'Watt'),
|
||||
ToonSensor(hass, 'Power_today', 'power-plug', 'kWh'),
|
||||
])
|
||||
sensors = [
|
||||
ToonElectricityMeterDeviceSensor(toon, 'power', 'value',
|
||||
"Current Power Usage",
|
||||
'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_main.gas:
|
||||
sensor_items.extend([
|
||||
ToonSensor(hass, 'Gas_current', 'gas-cylinder', 'CM3'),
|
||||
ToonSensor(hass, 'Gas_today', 'gas-cylinder', 'M3'),
|
||||
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),
|
||||
])
|
||||
|
||||
for plug in _toon_main.toon.smartplugs:
|
||||
sensor_items.extend([
|
||||
FibaroSensor(hass, '{}_current_power'.format(plug.name),
|
||||
plug.name, 'power-socket-eu', 'Watt'),
|
||||
FibaroSensor(hass, '{}_today_energy'.format(plug.name),
|
||||
plug.name, 'power-socket-eu', 'kWh'),
|
||||
if toon.solar:
|
||||
sensors.extend([
|
||||
ToonSolarDeviceSensor(toon, 'solar', 'value',
|
||||
"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),
|
||||
])
|
||||
|
||||
if _toon_main.toon.solar.produced or _toon_main.solar:
|
||||
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'),
|
||||
if toon.thermostat_info.have_ot_boiler:
|
||||
sensors.extend([
|
||||
ToonBoilerDeviceSensor(toon, 'thermostat_info',
|
||||
'current_modulation_level',
|
||||
"Boiler Modulation Level",
|
||||
'mdi:percent',
|
||||
RATIO_PERCENT),
|
||||
])
|
||||
|
||||
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)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class ToonSensor(Entity):
|
||||
"""Representation of a Toon sensor."""
|
||||
class ToonSensor(ToonEntity):
|
||||
"""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."""
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._icon = 'mdi:{}'.format(icon)
|
||||
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
|
||||
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
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return '_'.join([DOMAIN, self.toon.agreement.id, 'sensor',
|
||||
self.section, self.measurement])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.thermos.get_data(self.name.lower())
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
async def async_update(self) -> None:
|
||||
"""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):
|
||||
"""Representation of a Fibaro sensor."""
|
||||
class ToonElectricityMeterDeviceSensor(ToonSensor,
|
||||
ToonElectricityMeterDeviceEntity):
|
||||
"""Defines a Eletricity Meter sensor."""
|
||||
|
||||
def __init__(self, hass, name, plug_name, icon, unit_of_measurement):
|
||||
"""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()
|
||||
pass
|
||||
|
||||
|
||||
class SolarSensor(Entity):
|
||||
"""Representation of a Solar sensor."""
|
||||
class ToonGasMeterDeviceSensor(ToonSensor, ToonGasMeterDeviceEntity):
|
||||
"""Defines a Gas Meter sensor."""
|
||||
|
||||
def __init__(self, hass, name, unit_of_measurement):
|
||||
"""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()
|
||||
pass
|
||||
|
||||
|
||||
class FibaroSmokeDetector(Entity):
|
||||
"""Representation of a Fibaro smoke detector."""
|
||||
class ToonSolarDeviceSensor(ToonSensor, ToonSolarDeviceEntity):
|
||||
"""Defines a Solar sensor."""
|
||||
|
||||
def __init__(self, hass, name, uid, icon, unit_of_measurement):
|
||||
"""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]
|
||||
pass
|
||||
|
||||
@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
|
||||
class ToonBoilerDeviceSensor(ToonSensor, ToonBoilerDeviceEntity):
|
||||
"""Defines a Boiler sensor."""
|
||||
|
||||
@property
|
||||
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()
|
||||
pass
|
||||
|
34
homeassistant/components/toon/strings.json
Normal file
34
homeassistant/components/toon/strings.json
Normal 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/)."
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
@ -172,6 +172,7 @@ FLOWS = [
|
||||
'smhi',
|
||||
'sonos',
|
||||
'tellduslive',
|
||||
'toon',
|
||||
'tplink',
|
||||
'tradfri',
|
||||
'twilio',
|
||||
|
@ -1685,7 +1685,7 @@ tikteck==0.4
|
||||
todoist-python==7.0.17
|
||||
|
||||
# homeassistant.components.toon
|
||||
toonlib==1.1.3
|
||||
toonapilib==3.0.9
|
||||
|
||||
# homeassistant.components.alarm_control_panel.totalconnect
|
||||
total_connect_client==0.22
|
||||
|
@ -290,6 +290,9 @@ srpenergy==1.0.5
|
||||
# homeassistant.components.statsd
|
||||
statsd==3.2.1
|
||||
|
||||
# homeassistant.components.toon
|
||||
toonapilib==3.0.9
|
||||
|
||||
# homeassistant.components.camera.uvc
|
||||
uvcclient==0.11.0
|
||||
|
||||
|
@ -118,6 +118,7 @@ TEST_REQUIREMENTS = (
|
||||
'sqlalchemy',
|
||||
'srpenergy',
|
||||
'statsd',
|
||||
'toonapilib',
|
||||
'uvcclient',
|
||||
'vsure',
|
||||
'warrant',
|
||||
|
1
tests/components/toon/__init__.py
Normal file
1
tests/components/toon/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Toon component."""
|
177
tests/components/toon/test_config_flow.py
Normal file
177
tests/components/toon/test_config_flow.py
Normal 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'
|
Loading…
x
Reference in New Issue
Block a user