Add Home Connect integration (#29214)

pull/35253/head
David Straub 2020-05-05 11:26:14 +02:00 committed by GitHub
parent 6b9eed5a70
commit 86d410d863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 987 additions and 0 deletions

View File

@ -299,6 +299,7 @@ omit =
homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/*
homeassistant/components/hlk_sw16/*
homeassistant/components/home_connect/*
homeassistant/components/homematic/*
homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py

View File

@ -163,6 +163,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl
homeassistant/components/hisense_aehw4a1/* @bannhead
homeassistant/components/history/* @home-assistant/core
homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/home_connect/* @DavidMStraub
homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k

View File

@ -0,0 +1,106 @@
"""Support for BSH Home Connect appliances."""
import asyncio
from datetime import timedelta
import logging
from requests import HTTPError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.util import Throttle
from . import api, config_flow
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=1)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
PLATFORMS = ["binary_sensor", "sensor", "switch"]
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up Home Connect component."""
hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True
config_flow.OAuth2FlowHandler.async_register_implementation(
hass,
config_entry_oauth2_flow.LocalOAuth2Implementation(
hass,
DOMAIN,
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Home Connect from a config entry."""
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
hc_api = api.ConfigEntryAuth(hass, entry, implementation)
hass.data[DOMAIN][entry.entry_id] = hc_api
await update_all_devices(hass, entry)
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
@Throttle(SCAN_INTERVAL)
async def update_all_devices(hass, entry):
"""Update all the devices."""
data = hass.data[DOMAIN]
hc_api = data[entry.entry_id]
try:
await hass.async_add_executor_job(hc_api.get_devices)
for device_dict in hc_api.devices:
await hass.async_add_executor_job(device_dict["device"].initialize)
except HTTPError as err:
_LOGGER.warning("Cannot update devices: %s", err.response.status_code)

View File

@ -0,0 +1,372 @@
"""API for Home Connect bound to HASS OAuth."""
from asyncio import run_coroutine_threadsafe
import logging
import homeconnect
from homeconnect.api import HomeConnectError
from homeassistant import config_entries, core
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, TIME_SECONDS, UNIT_PERCENTAGE
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
BSH_ACTIVE_PROGRAM,
BSH_POWER_OFF,
BSH_POWER_STANDBY,
SIGNAL_UPDATE_ENTITIES,
)
_LOGGER = logging.getLogger(__name__)
class ConfigEntryAuth(homeconnect.HomeConnectAPI):
"""Provide Home Connect authentication tied to an OAuth2 based config entry."""
def __init__(
self,
hass: core.HomeAssistant,
config_entry: config_entries.ConfigEntry,
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
):
"""Initialize Home Connect Auth."""
self.hass = hass
self.config_entry = config_entry
self.session = config_entry_oauth2_flow.OAuth2Session(
hass, config_entry, implementation
)
super().__init__(self.session.token)
self.devices = []
def refresh_tokens(self) -> dict:
"""Refresh and return new Home Connect tokens using Home Assistant OAuth2 session."""
run_coroutine_threadsafe(
self.session.async_ensure_token_valid(), self.hass.loop
).result()
return self.session.token
def get_devices(self):
"""Get a dictionary of devices."""
appl = self.get_appliances()
devices = []
for app in appl:
if app.type == "Dryer":
device = Dryer(self.hass, app)
elif app.type == "Washer":
device = Washer(self.hass, app)
elif app.type == "Dishwasher":
device = Dishwasher(self.hass, app)
elif app.type == "FridgeFreezer":
device = FridgeFreezer(self.hass, app)
elif app.type == "Oven":
device = Oven(self.hass, app)
elif app.type == "CoffeeMaker":
device = CoffeeMaker(self.hass, app)
elif app.type == "Hood":
device = Hood(self.hass, app)
elif app.type == "Hob":
device = Hob(self.hass, app)
else:
_LOGGER.warning("Appliance type %s not implemented.", app.type)
continue
devices.append({"device": device, "entities": device.get_entity_info()})
self.devices = devices
return devices
class HomeConnectDevice:
"""Generic Home Connect device."""
# for some devices, this is instead BSH_POWER_STANDBY
# see https://developer.home-connect.com/docs/settings/power_state
power_off_state = BSH_POWER_OFF
def __init__(self, hass, appliance):
"""Initialize the device class."""
self.hass = hass
self.appliance = appliance
def initialize(self):
"""Fetch the info needed to initialize the device."""
try:
self.appliance.get_status()
except (HomeConnectError, ValueError):
_LOGGER.debug("Unable to fetch appliance status. Probably offline.")
try:
self.appliance.get_settings()
except (HomeConnectError, ValueError):
_LOGGER.debug("Unable to fetch settings. Probably offline.")
try:
program_active = self.appliance.get_programs_active()
except (HomeConnectError, ValueError):
_LOGGER.debug("Unable to fetch active programs. Probably offline.")
program_active = None
if program_active and "key" in program_active:
self.appliance.status[BSH_ACTIVE_PROGRAM] = {"value": program_active["key"]}
self.appliance.listen_events(callback=self.event_callback)
def event_callback(self, appliance):
"""Handle event."""
_LOGGER.debug("Update triggered on %s", appliance.name)
_LOGGER.debug(self.appliance.status)
dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId)
class DeviceWithPrograms(HomeConnectDevice):
"""Device with programs."""
PROGRAMS = []
def get_programs_available(self):
"""Get the available programs."""
return self.PROGRAMS
def get_program_switches(self):
"""Get a dictionary with info about program switches.
There will be one switch for each program.
"""
programs = self.get_programs_available()
return [{"device": self, "program_name": p["name"]} for p in programs]
def get_program_sensors(self):
"""Get a dictionary with info about program sensors.
There will be one of the four types of sensors for each
device.
"""
sensors = {
"Remaining Program Time": (None, None, DEVICE_CLASS_TIMESTAMP, 1),
"Duration": (TIME_SECONDS, "mdi:update", None, 1),
"Program Progress": (UNIT_PERCENTAGE, "mdi:progress-clock", None, 1),
}
return [
{
"device": self,
"desc": k,
"unit": unit,
"key": "BSH.Common.Option.{}".format(k.replace(" ", "")),
"icon": icon,
"device_class": device_class,
"sign": sign,
}
for k, (unit, icon, device_class, sign) in sensors.items()
]
class DeviceWithDoor(HomeConnectDevice):
"""Device that has a door sensor."""
def get_door_entity(self):
"""Get a dictionary with info about the door binary sensor."""
return {
"device": self,
"desc": "Door",
"device_class": "door",
}
class Dryer(DeviceWithDoor, DeviceWithPrograms):
"""Dryer class."""
PROGRAMS = [
{"name": "LaundryCare.Dryer.Program.Cotton"},
{"name": "LaundryCare.Dryer.Program.Synthetic"},
{"name": "LaundryCare.Dryer.Program.Mix"},
{"name": "LaundryCare.Dryer.Program.Blankets"},
{"name": "LaundryCare.Dryer.Program.BusinessShirts"},
{"name": "LaundryCare.Dryer.Program.DownFeathers"},
{"name": "LaundryCare.Dryer.Program.Hygiene"},
{"name": "LaundryCare.Dryer.Program.Jeans"},
{"name": "LaundryCare.Dryer.Program.Outdoor"},
{"name": "LaundryCare.Dryer.Program.SyntheticRefresh"},
{"name": "LaundryCare.Dryer.Program.Towels"},
{"name": "LaundryCare.Dryer.Program.Delicates"},
{"name": "LaundryCare.Dryer.Program.Super40"},
{"name": "LaundryCare.Dryer.Program.Shirts15"},
{"name": "LaundryCare.Dryer.Program.Pillow"},
{"name": "LaundryCare.Dryer.Program.AntiShrink"},
]
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
"binary_sensor": [door_entity],
"switch": program_switches,
"sensor": program_sensors,
}
class Dishwasher(DeviceWithDoor, DeviceWithPrograms):
"""Dishwasher class."""
PROGRAMS = [
{"name": "Dishcare.Dishwasher.Program.Auto1"},
{"name": "Dishcare.Dishwasher.Program.Auto2"},
{"name": "Dishcare.Dishwasher.Program.Auto3"},
{"name": "Dishcare.Dishwasher.Program.Eco50"},
{"name": "Dishcare.Dishwasher.Program.Quick45"},
{"name": "Dishcare.Dishwasher.Program.Intensiv70"},
{"name": "Dishcare.Dishwasher.Program.Normal65"},
{"name": "Dishcare.Dishwasher.Program.Glas40"},
{"name": "Dishcare.Dishwasher.Program.GlassCare"},
{"name": "Dishcare.Dishwasher.Program.NightWash"},
{"name": "Dishcare.Dishwasher.Program.Quick65"},
{"name": "Dishcare.Dishwasher.Program.Normal45"},
{"name": "Dishcare.Dishwasher.Program.Intensiv45"},
{"name": "Dishcare.Dishwasher.Program.AutoHalfLoad"},
{"name": "Dishcare.Dishwasher.Program.IntensivPower"},
{"name": "Dishcare.Dishwasher.Program.MagicDaily"},
{"name": "Dishcare.Dishwasher.Program.Super60"},
{"name": "Dishcare.Dishwasher.Program.Kurz60"},
{"name": "Dishcare.Dishwasher.Program.ExpressSparkle65"},
{"name": "Dishcare.Dishwasher.Program.MachineCare"},
{"name": "Dishcare.Dishwasher.Program.SteamFresh"},
{"name": "Dishcare.Dishwasher.Program.MaximumCleaning"},
]
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
"binary_sensor": [door_entity],
"switch": program_switches,
"sensor": program_sensors,
}
class Oven(DeviceWithDoor, DeviceWithPrograms):
"""Oven class."""
PROGRAMS = [
{"name": "Cooking.Oven.Program.HeatingMode.PreHeating"},
{"name": "Cooking.Oven.Program.HeatingMode.HotAir"},
{"name": "Cooking.Oven.Program.HeatingMode.TopBottomHeating"},
{"name": "Cooking.Oven.Program.HeatingMode.PizzaSetting"},
{"name": "Cooking.Oven.Program.Microwave.600Watt"},
]
power_off_state = BSH_POWER_STANDBY
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
"binary_sensor": [door_entity],
"switch": program_switches,
"sensor": program_sensors,
}
class Washer(DeviceWithDoor, DeviceWithPrograms):
"""Washer class."""
PROGRAMS = [
{"name": "LaundryCare.Washer.Program.Cotton"},
{"name": "LaundryCare.Washer.Program.Cotton.CottonEco"},
{"name": "LaundryCare.Washer.Program.EasyCare"},
{"name": "LaundryCare.Washer.Program.Mix"},
{"name": "LaundryCare.Washer.Program.DelicatesSilk"},
{"name": "LaundryCare.Washer.Program.Wool"},
{"name": "LaundryCare.Washer.Program.Sensitive"},
{"name": "LaundryCare.Washer.Program.Auto30"},
{"name": "LaundryCare.Washer.Program.Auto40"},
{"name": "LaundryCare.Washer.Program.Auto60"},
{"name": "LaundryCare.Washer.Program.Chiffon"},
{"name": "LaundryCare.Washer.Program.Curtains"},
{"name": "LaundryCare.Washer.Program.DarkWash"},
{"name": "LaundryCare.Washer.Program.Dessous"},
{"name": "LaundryCare.Washer.Program.Monsoon"},
{"name": "LaundryCare.Washer.Program.Outdoor"},
{"name": "LaundryCare.Washer.Program.PlushToy"},
{"name": "LaundryCare.Washer.Program.ShirtsBlouses"},
{"name": "LaundryCare.Washer.Program.SportFitness"},
{"name": "LaundryCare.Washer.Program.Towels"},
{"name": "LaundryCare.Washer.Program.WaterProof"},
]
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {
"binary_sensor": [door_entity],
"switch": program_switches,
"sensor": program_sensors,
}
class CoffeeMaker(DeviceWithPrograms):
"""Coffee maker class."""
PROGRAMS = [
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte"},
{"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Americano"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoDoppio"},
{"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.FlatWhite"},
{"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Galao"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.MilkFroth"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.WarmMilk"},
{"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Ristretto"},
{"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Cortado"},
]
power_off_state = BSH_POWER_STANDBY
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {"switch": program_switches, "sensor": program_sensors}
class Hood(DeviceWithPrograms):
"""Hood class."""
PROGRAMS = [
{"name": "Cooking.Common.Program.Hood.Automatic"},
{"name": "Cooking.Common.Program.Hood.Venting"},
{"name": "Cooking.Common.Program.Hood.DelayedShutOff"},
]
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {"switch": program_switches, "sensor": program_sensors}
class FridgeFreezer(DeviceWithDoor):
"""Fridge/Freezer class."""
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
door_entity = self.get_door_entity()
return {"binary_sensor": [door_entity]}
class Hob(DeviceWithPrograms):
"""Hob class."""
PROGRAMS = [{"name": "Cooking.Hob.Program.PowerLevelMode"}]
def get_entity_info(self):
"""Get a dictionary with infos about the associated entities."""
program_sensors = self.get_program_sensors()
program_switches = self.get_program_switches()
return {"switch": program_switches, "sensor": program_sensors}

View File

@ -0,0 +1,65 @@
"""Provides a binary sensor for Home Connect."""
import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
from .const import BSH_DOOR_STATE, DOMAIN
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Home Connect binary sensor."""
def get_entities():
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
entity_dicts = device_dict.get("entities", {}).get("binary_sensor", [])
entities += [HomeConnectBinarySensor(**d) for d in entity_dicts]
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
"""Binary sensor for Home Connect."""
def __init__(self, device, desc, device_class):
"""Initialize the entity."""
super().__init__(device, desc)
self._device_class = device_class
self._state = None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return bool(self._state)
@property
def available(self):
"""Return true if the binary sensor is available."""
return self._state is not None
async def async_update(self):
"""Update the binary sensor's status."""
state = self.device.appliance.status.get(BSH_DOOR_STATE, {})
if not state:
self._state = None
elif state.get("value") in [
"BSH.Common.EnumType.DoorState.Closed",
"BSH.Common.EnumType.DoorState.Locked",
]:
self._state = False
elif state.get("value") == "BSH.Common.EnumType.DoorState.Open":
self._state = True
else:
_LOGGER.warning("Unexpected value for HomeConnect door state: %s", state)
self._state = None
_LOGGER.debug("Updated, new state: %s", self._state)
@property
def device_class(self):
"""Return the device class."""
return self._device_class

View File

@ -0,0 +1,23 @@
"""Config flow for Home Connect."""
import logging
from homeassistant import config_entries
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Config flow to handle Home Connect OAuth2 authentication."""
DOMAIN = DOMAIN
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

View File

@ -0,0 +1,16 @@
"""Constants for the Home Connect integration."""
DOMAIN = "home_connect"
OAUTH2_AUTHORIZE = "https://api.home-connect.com/security/oauth/authorize"
OAUTH2_TOKEN = "https://api.home-connect.com/security/oauth/token"
BSH_POWER_STATE = "BSH.Common.Setting.PowerState"
BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On"
BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off"
BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby"
BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram"
BSH_OPERATION_STATE = "BSH.Common.Status.OperationState"
BSH_DOOR_STATE = "BSH.Common.Status.DoorState"
SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities"

View File

@ -0,0 +1,67 @@
"""Home Connect entity base class."""
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .api import HomeConnectDevice
from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES
_LOGGER = logging.getLogger(__name__)
class HomeConnectEntity(Entity):
"""Generic Home Connect entity (base class)."""
def __init__(self, device: HomeConnectDevice, desc: str) -> None:
"""Initialize the entity."""
self.device = device
self.desc = desc
self._name = f"{self.device.appliance.name} {desc}"
async def async_added_to_hass(self):
"""Register callbacks."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITIES, self._update_callback
)
)
@callback
def _update_callback(self, ha_id):
"""Update data."""
if ha_id == self.device.appliance.haId:
self.async_entity_update()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the node (used for Entity_ID)."""
return self._name
@property
def unique_id(self):
"""Return the unique id base on the id returned by Home Connect and the entity name."""
return f"{self.device.appliance.haId}-{self.desc}"
@property
def device_info(self):
"""Return info about the device."""
return {
"identifiers": {(DOMAIN, self.device.appliance.haId)},
"name": self.device.appliance.name,
"manufacturer": self.device.appliance.brand,
"model": self.device.appliance.vib,
}
@callback
def async_entity_update(self):
"""Update the entity."""
_LOGGER.debug("Entity update triggered on %s", self)
self.async_schedule_update_ha_state(True)

View File

@ -0,0 +1,9 @@
{
"domain": "home_connect",
"name": "Home Connect",
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"dependencies": ["http"],
"codeowners": ["@DavidMStraub"],
"requirements": ["homeconnect==0.5"],
"config_flow": true
}

View File

@ -0,0 +1,92 @@
"""Provides a sensor for Home Connect."""
from datetime import timedelta
import logging
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
import homeassistant.util.dt as dt_util
from .const import DOMAIN
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Home Connect sensor."""
def get_entities():
"""Get a list of entities."""
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
entity_dicts = device_dict.get("entities", {}).get("sensor", [])
entities += [HomeConnectSensor(**d) for d in entity_dicts]
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
class HomeConnectSensor(HomeConnectEntity):
"""Sensor class for Home Connect."""
def __init__(self, device, desc, key, unit, icon, device_class, sign=1):
"""Initialize the entity."""
super().__init__(device, desc)
self._state = None
self._key = key
self._unit = unit
self._icon = icon
self._device_class = device_class
self._sign = sign
@property
def state(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def available(self):
"""Return true if the sensor is available."""
return self._state is not None
async def async_update(self):
"""Update the sensos status."""
status = self.device.appliance.status
if self._key not in status:
self._state = None
else:
if self.device_class == DEVICE_CLASS_TIMESTAMP:
if "value" not in status[self._key]:
self._state = None
elif (
self._state is not None
and self._sign == 1
and self._state < dt_util.utcnow()
):
# if the date is supposed to be in the future but we're
# already past it, set state to None.
self._state = None
else:
seconds = self._sign * float(status[self._key]["value"])
self._state = (
dt_util.utcnow() + timedelta(seconds=seconds)
).isoformat()
else:
self._state = status[self._key].get("value")
_LOGGER.debug("Updated, new state: %s", self._state)
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def device_class(self):
"""Return the device class."""
return self._device_class

View File

@ -0,0 +1,15 @@
{
"config": {
"step": {
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation."
},
"create_entry": {
"default": "Successfully authenticated with Home Connect."
}
}
}

View File

@ -0,0 +1,158 @@
"""Provides a switch for Home Connect."""
import logging
from homeconnect.api import HomeConnectError
from homeassistant.components.switch import SwitchEntity
from .const import (
BSH_ACTIVE_PROGRAM,
BSH_OPERATION_STATE,
BSH_POWER_ON,
BSH_POWER_STATE,
DOMAIN,
)
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Home Connect switch."""
def get_entities():
"""Get a list of entities."""
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
entity_dicts = device_dict.get("entities", {}).get("switch", [])
entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts]
entity_list += [HomeConnectPowerSwitch(device_dict["device"])]
entities += entity_list
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
"""Switch class for Home Connect."""
def __init__(self, device, program_name):
"""Initialize the entity."""
desc = " ".join(["Program", program_name.split(".")[-1]])
super().__init__(device, desc)
self.program_name = program_name
self._state = None
self._remote_allowed = None
@property
def is_on(self):
"""Return true if the switch is on."""
return bool(self._state)
@property
def available(self):
"""Return true if the entity is available."""
return True
async def async_turn_on(self, **kwargs):
"""Start the program."""
_LOGGER.debug("Tried to turn on program %s", self.program_name)
try:
await self.hass.async_add_executor_job(
self.device.appliance.start_program, self.program_name
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to start program: %s", err)
self.async_entity_update()
async def async_turn_off(self, **kwargs):
"""Stop the program."""
_LOGGER.debug("Tried to stop program %s", self.program_name)
try:
await self.hass.async_add_executor_job(self.device.appliance.stop_program)
except HomeConnectError as err:
_LOGGER.error("Error while trying to stop program: %s", err)
self.async_entity_update()
async def async_update(self):
"""Update the switch's status."""
state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {})
if state.get("value") == self.program_name:
self._state = True
else:
self._state = False
_LOGGER.debug("Updated, new state: %s", self._state)
class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
"""Power switch class for Home Connect."""
def __init__(self, device):
"""Inititialize the entity."""
super().__init__(device, "Power")
self._state = None
@property
def is_on(self):
"""Return true if the switch is on."""
return bool(self._state)
async def async_turn_on(self, **kwargs):
"""Switch the device on."""
_LOGGER.debug("Tried to switch on %s", self.name)
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON,
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on device: %s", err)
self._state = False
self.async_entity_update()
async def async_turn_off(self, **kwargs):
"""Switch the device off."""
_LOGGER.debug("tried to switch off %s", self.name)
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting,
BSH_POWER_STATE,
self.device.power_off_state,
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off device: %s", err)
self._state = True
self.async_entity_update()
async def async_update(self):
"""Update the switch's status."""
if (
self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value")
== BSH_POWER_ON
):
self._state = True
elif (
self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value")
== self.device.power_off_state
):
self._state = False
elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get(
"value", None
) in [
"BSH.Common.EnumType.OperationState.Ready",
"BSH.Common.EnumType.OperationState.DelayedStart",
"BSH.Common.EnumType.OperationState.Run",
"BSH.Common.EnumType.OperationState.Pause",
"BSH.Common.EnumType.OperationState.ActionRequired",
"BSH.Common.EnumType.OperationState.Aborting",
"BSH.Common.EnumType.OperationState.Finished",
]:
self._state = True
elif (
self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get("value")
== "BSH.Common.EnumType.OperationState.Inactive"
):
self._state = False
else:
self._state = None
_LOGGER.debug("Updated, new state: %s", self._state)

View File

@ -50,6 +50,7 @@ FLOWS = [
"harmony",
"heos",
"hisense_aehw4a1",
"home_connect",
"homekit",
"homekit_controller",
"homematicip_cloud",

View File

@ -721,6 +721,9 @@ home-assistant-frontend==20200427.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
# homeassistant.components.home_connect
homeconnect==0.5
# homeassistant.components.homematicip_cloud
homematicip==0.10.17

View File

@ -302,6 +302,9 @@ home-assistant-frontend==20200427.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
# homeassistant.components.home_connect
homeconnect==0.5
# homeassistant.components.homematicip_cloud
homematicip==0.10.17

View File

@ -0,0 +1 @@
"""Tests for the Home Connect integration."""

View File

@ -0,0 +1,54 @@
"""Test the Home Connect config flow."""
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.home_connect.const import (
DOMAIN,
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
)
from homeassistant.helpers import config_entry_oauth2_flow
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
async def test_full_flow(hass, aiohttp_client, aioclient_mock):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
"home_connect",
{
"home_connect": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
"http": {"base_url": "https://example.com"},
},
)
result = await hass.config_entries.flow.async_init(
"home_connect", context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}"
)
client = await aiohttp_client(hass.http.app)
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
aioclient_mock.post(
OAUTH2_TOKEN,
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert len(hass.config_entries.async_entries(DOMAIN)) == 1