2018-01-01 16:08:13 +00:00
|
|
|
"""
|
|
|
|
Support for deCONZ devices.
|
|
|
|
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/deconz/
|
|
|
|
"""
|
|
|
|
import logging
|
2018-01-19 06:36:29 +00:00
|
|
|
|
2018-01-01 16:08:13 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2018-04-13 14:14:53 +00:00
|
|
|
from homeassistant import config_entries, data_entry_flow
|
2018-01-19 06:36:29 +00:00
|
|
|
from homeassistant.components.discovery import SERVICE_DECONZ
|
2018-01-01 16:08:13 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
2018-02-27 06:31:47 +00:00
|
|
|
from homeassistant.core import callback
|
2018-01-01 16:08:13 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv
|
2018-03-30 07:34:26 +00:00
|
|
|
from homeassistant.helpers import discovery, aiohttp_client
|
2018-01-01 16:08:13 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
|
from homeassistant.util.json import load_json, save_json
|
|
|
|
|
2018-04-17 18:00:53 +00:00
|
|
|
REQUIREMENTS = ['pydeconz==36']
|
2018-01-01 16:08:13 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DOMAIN = 'deconz'
|
2018-02-14 00:23:04 +00:00
|
|
|
DATA_DECONZ_ID = 'deconz_entities'
|
2018-01-01 16:08:13 +00:00
|
|
|
|
|
|
|
CONFIG_FILE = 'deconz.conf'
|
|
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: vol.Schema({
|
|
|
|
vol.Optional(CONF_API_KEY): cv.string,
|
2018-01-19 06:36:29 +00:00
|
|
|
vol.Optional(CONF_HOST): cv.string,
|
2018-01-01 16:08:13 +00:00
|
|
|
vol.Optional(CONF_PORT, default=80): cv.port,
|
|
|
|
})
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
SERVICE_FIELD = 'field'
|
2018-02-14 00:23:04 +00:00
|
|
|
SERVICE_ENTITY = 'entity'
|
2018-01-01 16:08:13 +00:00
|
|
|
SERVICE_DATA = 'data'
|
|
|
|
|
|
|
|
SERVICE_SCHEMA = vol.Schema({
|
2018-02-14 00:23:04 +00:00
|
|
|
vol.Exclusive(SERVICE_FIELD, 'deconz_id'): cv.string,
|
|
|
|
vol.Exclusive(SERVICE_ENTITY, 'deconz_id'): cv.entity_id,
|
2018-01-30 22:42:24 +00:00
|
|
|
vol.Required(SERVICE_DATA): dict,
|
2018-01-01 16:08:13 +00:00
|
|
|
})
|
|
|
|
|
2018-02-14 00:23:04 +00:00
|
|
|
|
2018-01-01 16:08:13 +00:00
|
|
|
CONFIG_INSTRUCTIONS = """
|
|
|
|
Unlock your deCONZ gateway to register with Home Assistant.
|
|
|
|
|
|
|
|
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
|
|
|
|
2. Press "Unlock Gateway" button
|
|
|
|
|
|
|
|
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2018-03-13 07:47:45 +00:00
|
|
|
async def async_setup(hass, config):
|
2018-01-19 06:36:29 +00:00
|
|
|
"""Set up services and configuration for deCONZ component."""
|
2018-01-01 16:08:13 +00:00
|
|
|
result = False
|
2018-03-13 07:47:45 +00:00
|
|
|
config_file = await hass.async_add_job(
|
2018-01-01 16:08:13 +00:00
|
|
|
load_json, hass.config.path(CONFIG_FILE))
|
|
|
|
|
2018-03-13 07:47:45 +00:00
|
|
|
async def async_deconz_discovered(service, discovery_info):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Call when deCONZ gateway has been found."""
|
2018-01-01 16:08:13 +00:00
|
|
|
deconz_config = {}
|
|
|
|
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
|
|
|
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
2018-03-13 07:47:45 +00:00
|
|
|
await async_request_configuration(hass, config, deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
|
|
|
|
if config_file:
|
2018-03-13 07:47:45 +00:00
|
|
|
result = await async_setup_deconz(hass, config, config_file)
|
2018-01-01 16:08:13 +00:00
|
|
|
|
|
|
|
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
|
|
|
|
deconz_config = config[DOMAIN]
|
|
|
|
if CONF_API_KEY in deconz_config:
|
2018-03-13 07:47:45 +00:00
|
|
|
result = await async_setup_deconz(hass, config, deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
else:
|
2018-03-13 07:47:45 +00:00
|
|
|
await async_request_configuration(hass, config, deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-03-13 07:47:45 +00:00
|
|
|
async def async_setup_deconz(hass, config, deconz_config):
|
2018-01-19 06:36:29 +00:00
|
|
|
"""Set up a deCONZ session.
|
2018-01-01 16:08:13 +00:00
|
|
|
|
|
|
|
Load config, group, light and sensor data for server information.
|
|
|
|
Start websocket for push notification of state changes from deCONZ.
|
|
|
|
"""
|
2018-01-21 06:35:38 +00:00
|
|
|
_LOGGER.debug("deCONZ config %s", deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
from pydeconz import DeconzSession
|
|
|
|
websession = async_get_clientsession(hass)
|
|
|
|
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
2018-03-13 07:47:45 +00:00
|
|
|
result = await deconz.async_load_parameters()
|
2018-01-01 16:08:13 +00:00
|
|
|
if result is False:
|
2018-01-21 06:35:38 +00:00
|
|
|
_LOGGER.error("Failed to communicate with deCONZ")
|
2018-01-01 16:08:13 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
hass.data[DOMAIN] = deconz
|
2018-02-14 00:23:04 +00:00
|
|
|
hass.data[DATA_DECONZ_ID] = {}
|
2018-01-01 16:08:13 +00:00
|
|
|
|
|
|
|
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
|
|
|
|
hass.async_add_job(discovery.async_load_platform(
|
|
|
|
hass, component, DOMAIN, {}, config))
|
|
|
|
deconz.start()
|
|
|
|
|
2018-03-13 07:47:45 +00:00
|
|
|
async def async_configure(call):
|
2018-01-01 16:08:13 +00:00
|
|
|
"""Set attribute of device in deCONZ.
|
|
|
|
|
|
|
|
Field is a string representing a specific device in deCONZ
|
|
|
|
e.g. field='/lights/1/state'.
|
2018-02-14 00:23:04 +00:00
|
|
|
Entity_id can be used to retrieve the proper field.
|
2018-01-01 16:08:13 +00:00
|
|
|
Data is a json object with what data you want to alter
|
|
|
|
e.g. data={'on': true}.
|
|
|
|
{
|
|
|
|
"field": "/lights/1/state",
|
|
|
|
"data": {"on": true}
|
|
|
|
}
|
|
|
|
See Dresden Elektroniks REST API documentation for details:
|
|
|
|
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
|
|
|
|
"""
|
|
|
|
field = call.data.get(SERVICE_FIELD)
|
2018-02-14 00:23:04 +00:00
|
|
|
entity_id = call.data.get(SERVICE_ENTITY)
|
2018-01-01 16:08:13 +00:00
|
|
|
data = call.data.get(SERVICE_DATA)
|
2018-02-14 00:23:04 +00:00
|
|
|
deconz = hass.data[DOMAIN]
|
|
|
|
if entity_id:
|
|
|
|
entities = hass.data.get(DATA_DECONZ_ID)
|
|
|
|
if entities:
|
|
|
|
field = entities.get(entity_id)
|
|
|
|
if field is None:
|
|
|
|
_LOGGER.error('Could not find the entity %s', entity_id)
|
|
|
|
return
|
2018-03-13 07:47:45 +00:00
|
|
|
await deconz.async_put_state(field, data)
|
2018-01-01 16:08:13 +00:00
|
|
|
hass.services.async_register(
|
2018-01-21 06:35:38 +00:00
|
|
|
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
|
2018-01-01 16:08:13 +00:00
|
|
|
|
2018-02-27 06:31:47 +00:00
|
|
|
@callback
|
|
|
|
def deconz_shutdown(event):
|
|
|
|
"""
|
|
|
|
Wrap the call to deconz.close.
|
|
|
|
|
|
|
|
Used as an argument to EventBus.async_listen_once - EventBus calls
|
|
|
|
this method with the event as the first argument, which should not
|
|
|
|
be passed on to deconz.close.
|
|
|
|
"""
|
|
|
|
deconz.close()
|
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
|
2018-01-01 16:08:13 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-03-13 07:47:45 +00:00
|
|
|
async def async_request_configuration(hass, config, deconz_config):
|
2018-01-01 16:08:13 +00:00
|
|
|
"""Request configuration steps from the user."""
|
|
|
|
configurator = hass.components.configurator
|
|
|
|
|
2018-03-13 07:47:45 +00:00
|
|
|
async def async_configuration_callback(data):
|
2018-01-01 16:08:13 +00:00
|
|
|
"""Set up actions to do when our configuration callback is called."""
|
|
|
|
from pydeconz.utils import async_get_api_key
|
2018-03-30 07:34:26 +00:00
|
|
|
websession = async_get_clientsession(hass)
|
|
|
|
api_key = await async_get_api_key(websession, **deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
if api_key:
|
|
|
|
deconz_config[CONF_API_KEY] = api_key
|
2018-03-13 07:47:45 +00:00
|
|
|
result = await async_setup_deconz(hass, config, deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
if result:
|
2018-03-13 07:47:45 +00:00
|
|
|
await hass.async_add_job(
|
2018-01-19 06:36:29 +00:00
|
|
|
save_json, hass.config.path(CONFIG_FILE), deconz_config)
|
2018-01-01 16:08:13 +00:00
|
|
|
configurator.async_request_done(request_id)
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
configurator.async_notify_errors(
|
|
|
|
request_id, "Couldn't load configuration.")
|
|
|
|
else:
|
|
|
|
configurator.async_notify_errors(
|
|
|
|
request_id, "Couldn't get an API key.")
|
|
|
|
return
|
|
|
|
|
|
|
|
instructions = CONFIG_INSTRUCTIONS.format(
|
|
|
|
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
|
|
|
|
|
|
|
|
request_id = configurator.async_request_config(
|
|
|
|
"deCONZ", async_configuration_callback,
|
|
|
|
description=instructions,
|
|
|
|
entity_picture="/static/images/logo_deconz.jpeg",
|
|
|
|
submit_caption="I have unlocked the gateway",
|
|
|
|
)
|
2018-03-30 07:34:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
@config_entries.HANDLERS.register(DOMAIN)
|
2018-04-13 14:14:53 +00:00
|
|
|
class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
2018-03-30 07:34:26 +00:00
|
|
|
"""Handle a deCONZ config flow."""
|
|
|
|
|
|
|
|
VERSION = 1
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
"""Initialize the deCONZ flow."""
|
|
|
|
self.bridges = []
|
|
|
|
self.deconz_config = {}
|
|
|
|
|
|
|
|
async def async_step_init(self, user_input=None):
|
|
|
|
"""Handle a flow start."""
|
|
|
|
from pydeconz.utils import async_discovery
|
|
|
|
|
|
|
|
if DOMAIN in self.hass.data:
|
|
|
|
return self.async_abort(
|
|
|
|
reason='one_instance_only'
|
|
|
|
)
|
|
|
|
|
|
|
|
if user_input is not None:
|
|
|
|
for bridge in self.bridges:
|
|
|
|
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
|
|
|
self.deconz_config = bridge
|
|
|
|
return await self.async_step_link()
|
|
|
|
|
|
|
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
|
|
|
self.bridges = await async_discovery(session)
|
|
|
|
|
|
|
|
if len(self.bridges) == 1:
|
|
|
|
self.deconz_config = self.bridges[0]
|
|
|
|
return await self.async_step_link()
|
|
|
|
elif len(self.bridges) > 1:
|
|
|
|
hosts = []
|
|
|
|
for bridge in self.bridges:
|
|
|
|
hosts.append(bridge[CONF_HOST])
|
|
|
|
return self.async_show_form(
|
|
|
|
step_id='init',
|
|
|
|
data_schema=vol.Schema({
|
|
|
|
vol.Required(CONF_HOST): vol.In(hosts)
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
return self.async_abort(
|
|
|
|
reason='no_bridges'
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_step_link(self, user_input=None):
|
|
|
|
"""Attempt to link with the deCONZ bridge."""
|
|
|
|
from pydeconz.utils import async_get_api_key
|
|
|
|
errors = {}
|
|
|
|
|
|
|
|
if user_input is not None:
|
|
|
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
|
|
|
api_key = await async_get_api_key(session, **self.deconz_config)
|
|
|
|
if api_key:
|
|
|
|
self.deconz_config[CONF_API_KEY] = api_key
|
|
|
|
return self.async_create_entry(
|
|
|
|
title='deCONZ',
|
|
|
|
data=self.deconz_config
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
errors['base'] = 'no_key'
|
|
|
|
|
|
|
|
return self.async_show_form(
|
|
|
|
step_id='link',
|
|
|
|
errors=errors,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass, entry):
|
|
|
|
"""Set up a bridge for a config entry."""
|
|
|
|
if DOMAIN in hass.data:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Config entry failed since one deCONZ instance already exists")
|
|
|
|
return False
|
|
|
|
result = await async_setup_deconz(hass, None, entry.data)
|
|
|
|
if result:
|
|
|
|
return True
|
|
|
|
return False
|