Add config flow for Daikin (#19182)

* config flow for daikin

* tox test

* return fixes

* tox test fixes

* tox formatting
This commit is contained in:
Fredrik Erlandsson 2018-12-16 16:19:18 +01:00 committed by Martin Hjelmare
parent 266477a4f5
commit 5a295ad42b
12 changed files with 331 additions and 124 deletions

View file

@ -73,7 +73,8 @@ omit =
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
homeassistant/components/daikin.py
homeassistant/components/daikin/__init__.py
homeassistant/components/daikin/const.py
homeassistant/components/*/daikin.py
homeassistant/components/digital_ocean.py

View file

@ -15,9 +15,9 @@ from homeassistant.components.climate import (
STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE,
ClimateDevice)
from homeassistant.components.daikin import (
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE,
daikin_api_setup)
from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN
from homeassistant.components.daikin.const import (
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv
@ -60,18 +60,18 @@ HA_ATTR_TO_DAIKIN = {
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Daikin HVAC platform."""
if discovery_info is not None:
host = discovery_info.get('ip')
name = None
_LOGGER.debug("Discovered a Daikin AC on %s", host)
else:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
_LOGGER.debug("Added Daikin AC on %s", host)
"""Old way of setting up the Daikin HVAC platform.
api = daikin_api_setup(hass, host, name)
add_entities([DaikinClimate(api)], True)
Can only be called when a user accidentally mentions the platform in their
config. But even in that case it would have been ignored.
"""
pass
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
async_add_entities([DaikinClimate(daikin_api)])
class DaikinClimate(ClimateDevice):

View file

@ -0,0 +1,19 @@
{
"config": {
"title": "Daikin AC",
"step": {
"user": {
"title": "Configure Daikin AC",
"description": "Enter IP address of your Daikin AC.",
"data": {
"host": "Host"
}
}
},
"abort": {
"device_timeout": "Timeout connecting to the device.",
"device_fail": "Unexpected error creating device.",
"already_configured": "Device is already configured"
}
}
}

View file

@ -4,112 +4,105 @@ Platform for the Daikin AC.
For more details about this component, please refer to the documentation
https://home-assistant.io/components/daikin/
"""
import logging
import asyncio
from datetime import timedelta
import logging
from socket import timeout
import async_timeout
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOSTS
import homeassistant.helpers.config_validation as cv
from homeassistant.components.discovery import SERVICE_DAIKIN
from homeassistant.const import (
CONF_HOSTS, CONF_ICON, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TYPE
)
from homeassistant.helpers import discovery
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
from . import config_flow # noqa pylint_disable=unused-import
from .const import KEY_HOST
REQUIREMENTS = ['pydaikin==0.8']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'daikin'
ATTR_TARGET_TEMPERATURE = 'target_temperature'
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
COMPONENT_TYPES = ['climate', 'sensor']
SENSOR_TYPE_TEMPERATURE = 'temperature'
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
CONF_NAME: 'Inside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
},
ATTR_OUTSIDE_TEMPERATURE: {
CONF_NAME: 'Outside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
}
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(
CONF_HOSTS, default=[]
): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(
CONF_MONITORED_CONDITIONS,
default=list(SENSOR_TYPES.keys())
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)])
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
async def async_setup(hass, config):
"""Establish connection with Daikin."""
def discovery_dispatch(service, discovery_info):
"""Dispatcher for Daikin discovery events."""
host = discovery_info.get('ip')
if daikin_api_setup(hass, host) is None:
return
for component in COMPONENT_TYPES:
load_platform(hass, component, DOMAIN, discovery_info,
config)
discovery.listen(hass, SERVICE_DAIKIN, discovery_dispatch)
for host in config.get(DOMAIN, {}).get(CONF_HOSTS, []):
if daikin_api_setup(hass, host) is None:
continue
discovery_info = {
'ip': host,
CONF_MONITORED_CONDITIONS:
config[DOMAIN][CONF_MONITORED_CONDITIONS]
}
load_platform(hass, 'sensor', DOMAIN, discovery_info, config)
if DOMAIN not in config:
return True
hosts = config[DOMAIN].get(CONF_HOSTS)
if not hosts:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={'source': config.SOURCE_IMPORT}))
for host in hosts:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': config.SOURCE_IMPORT},
data={
KEY_HOST: host,
}))
return True
def daikin_api_setup(hass, host, name=None):
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Establish connection with Daikin."""
conf = entry.data
daikin_api = await daikin_api_setup(hass, conf[KEY_HOST])
if not daikin_api:
return False
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api})
await asyncio.wait([
hass.config_entries.async_forward_entry_setup(entry, component)
for component in COMPONENT_TYPES
])
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
await asyncio.wait([
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in COMPONENT_TYPES
])
hass.data[DOMAIN].pop(config_entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return True
async def daikin_api_setup(hass, host):
"""Create a Daikin instance only once."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
from pydaikin.appliance import Appliance
try:
with async_timeout.timeout(10):
device = await hass.async_add_executor_job(Appliance, host)
except asyncio.TimeoutError:
_LOGGER.error("Connection to Daikin could not be established")
return None
except Exception: # pylint: disable=broad-except
_LOGGER.error("Unexpected error creating device")
return None
api = hass.data[DOMAIN].get(host)
if api is None:
from pydaikin import appliance
try:
device = appliance.Appliance(host)
except timeout:
_LOGGER.error("Connection to Daikin could not be established")
return False
if name is None:
name = device.values['name']
api = DaikinApi(device, name)
name = device.values['name']
api = DaikinApi(device, name)
return api

View file

@ -0,0 +1,74 @@
"""Config flow for the Daikin platform."""
import asyncio
import logging
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from .const import KEY_HOST, KEY_IP, KEY_MAC
_LOGGER = logging.getLogger(__name__)
@config_entries.HANDLERS.register('daikin')
class FlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def _create_entry(self, host, mac):
"""Register new entry."""
# Check if mac already is registered
for entry in self._async_current_entries():
if entry.data[KEY_MAC] == mac:
return self.async_abort(reason='already_configured')
return self.async_create_entry(
title=host,
data={
KEY_HOST: host,
KEY_MAC: mac
})
async def _create_device(self, host):
"""Create device."""
from pydaikin.appliance import Appliance
try:
with async_timeout.timeout(10):
device = await self.hass.async_add_executor_job(
Appliance, host)
except asyncio.TimeoutError:
return self.async_abort(reason='device_timeout')
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error creating device")
return self.async_abort(reason='device_fail')
mac = device.values.get('mac')
return await self._create_entry(host, mac)
async def async_step_user(self, user_input=None):
"""User initiated config flow."""
if user_input is None:
return self.async_show_form(
step_id='user',
data_schema=vol.Schema({
vol.Required(KEY_HOST): str
})
)
return await self._create_device(user_input[KEY_HOST])
async def async_step_import(self, user_input):
"""Import a config entry."""
host = user_input.get(KEY_HOST)
if not host:
return await self.async_step_user()
return await self._create_device(host)
async def async_step_discovery(self, user_input):
"""Initialize step from discovery."""
_LOGGER.info("Discovered device: %s", user_input)
return await self._create_entry(user_input[KEY_IP],
user_input[KEY_MAC])

View file

@ -0,0 +1,25 @@
"""Constants for Daikin."""
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE
ATTR_TARGET_TEMPERATURE = 'target_temperature'
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
SENSOR_TYPE_TEMPERATURE = 'temperature'
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
CONF_NAME: 'Inside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
},
ATTR_OUTSIDE_TEMPERATURE: {
CONF_NAME: 'Outside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
}
}
KEY_HOST = 'host'
KEY_MAC = 'mac'
KEY_IP = 'ip'

View file

@ -0,0 +1,19 @@
{
"config": {
"title": "Daikin AC",
"step": {
"user": {
"title": "Configure Daikin AC",
"description": "Enter IP address of your Daikin AC.",
"data": {
"host": "Host"
}
}
},
"abort": {
"device_timeout": "Timeout connecting to the device.",
"device_fail": "Unexpected error creating device.",
"already_configured": "Device is already configured"
}
}
}

View file

@ -46,6 +46,7 @@ SERVICE_HOMEKIT = 'homekit'
SERVICE_OCTOPRINT = 'octoprint'
CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin',
SERVICE_DECONZ: 'deconz',
'google_cast': 'cast',
SERVICE_HUE: 'hue',
@ -63,7 +64,6 @@ SERVICE_HANDLERS = {
SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
SERVICE_KONNECTED: ('konnected', None),

View file

@ -6,52 +6,33 @@ https://home-assistant.io/components/sensor.daikin/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.daikin import (
SENSOR_TYPES, SENSOR_TYPE_TEMPERATURE,
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE,
daikin_api_setup
)
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_ICON, CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_TYPE
)
from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN
from homeassistant.components.daikin.const import (
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, SENSOR_TYPE_TEMPERATURE,
SENSOR_TYPES)
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE
from homeassistant.helpers.entity import Entity
from homeassistant.util.unit_system import UnitSystem
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Daikin sensors."""
if discovery_info is not None:
host = discovery_info.get('ip')
name = None
monitored_conditions = discovery_info.get(
CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES.keys())
)
else:
host = config[CONF_HOST]
name = config.get(CONF_NAME)
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
_LOGGER.info("Added Daikin AC sensor on %s", host)
"""Old way of setting up the Daikin sensors.
api = daikin_api_setup(hass, host, name)
units = hass.config.units
sensors = []
for monitored_state in monitored_conditions:
sensors.append(DaikinClimateSensor(api, monitored_state, units, name))
Can only be called when a user accidentally mentions the platform in their
config. But even in that case it would have been ignored.
"""
pass
add_entities(sensors, True)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
async_add_entities([
DaikinClimateSensor(daikin_api, sensor, hass.config.units)
for sensor in SENSOR_TYPES
])
class DaikinClimateSensor(Entity):

View file

@ -136,6 +136,7 @@ HANDLERS = Registry()
# Components that have config flows. In future we will auto-generate this list.
FLOWS = [
'cast',
'daikin',
'deconz',
'dialogflow',
'hangouts',

View file

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

View file

@ -0,0 +1,93 @@
# pylint: disable=W0621
"""Tests for the Daikin config flow."""
import asyncio
import pytest
from homeassistant import data_entry_flow
from homeassistant.components.daikin import config_flow
from homeassistant.components.daikin.const import KEY_HOST, KEY_IP, KEY_MAC
from tests.common import MockConfigEntry, MockDependency
MAC = 'AABBCCDDEEFF'
HOST = '127.0.0.1'
def init_config_flow(hass):
"""Init a configuration flow."""
flow = config_flow.FlowHandler()
flow.hass = hass
return flow
@pytest.fixture
def mock_daikin():
"""Mock tellduslive."""
with MockDependency('pydaikin.appliance') as mock_daikin_:
mock_daikin_.Appliance().values.get.return_value = 'AABBCCDDEEFF'
yield mock_daikin_
async def test_user(hass, mock_daikin):
"""Test user config."""
flow = init_config_flow(hass)
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'user'
result = await flow.async_step_user({KEY_HOST: HOST})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['title'] == HOST
assert result['data'][KEY_HOST] == HOST
assert result['data'][KEY_MAC] == MAC
async def test_abort_if_already_setup(hass, mock_daikin):
"""Test we abort if Daikin is already setup."""
flow = init_config_flow(hass)
MockConfigEntry(domain='daikin', data={KEY_MAC: MAC}).add_to_hass(hass)
result = await flow.async_step_user({KEY_HOST: HOST})
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'already_configured'
async def test_import(hass, mock_daikin):
"""Test import step."""
flow = init_config_flow(hass)
result = await flow.async_step_import({})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'user'
result = await flow.async_step_import({KEY_HOST: HOST})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['title'] == HOST
assert result['data'][KEY_HOST] == HOST
assert result['data'][KEY_MAC] == MAC
async def test_discovery(hass, mock_daikin):
"""Test discovery step."""
flow = init_config_flow(hass)
result = await flow.async_step_discovery({KEY_IP: HOST, KEY_MAC: MAC})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['title'] == HOST
assert result['data'][KEY_HOST] == HOST
assert result['data'][KEY_MAC] == MAC
@pytest.mark.parametrize('s_effect,reason',
[(asyncio.TimeoutError, 'device_timeout'),
(Exception, 'device_fail')])
async def test_device_abort(hass, mock_daikin, s_effect, reason):
"""Test device abort."""
flow = init_config_flow(hass)
mock_daikin.Appliance.side_effect = s_effect
result = await flow.async_step_user({KEY_HOST: HOST})
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == reason