Add deCONZ component (#10321)

* Base implementation of component, no sensors yet

* Added senor files

* First fully working chain of sensors and binary sensors going from hardware in to hass

* Clean up

* Clean up

* Added light platform

* Turning lights on and off and set brightness now works

* Pydeconz is now a proper pypi package
Stop sessions when Home Assistant is shutting down
Use a simpler websocket client

* Updated pydocstrings
Followed recommendations from pylint and flake8

* Clean up

* Updated requirements_all.txt

* Updated Codeowners to include deconz.py
Also re-added the Axis component since it had gotten removed

* Bump requirement

* Bumped to v2
Reran script/gen_requirements

* Removed global DECONZ since it wasn't relevant any more

* Username and password is only relevant in the context of getting a API key

* Add support for additional sensors

* Added support for groups

* Moved import of component library to inside of methods

* Moved the need for device id to library

* Bump pydeconz to v5

* Add support for colored lights

* Pylint and flake8 import improvements

* DATA_DECONZ TO DECONZ_DATA

* Add support for transition time

* Add support for flash

* Bump to v7

* ZHASwitch devices will now only generate events by default, instead of being a sensor entity

* Clean up

* Add battery sensor when device signals through an event

* Third-party library communicates with service

* Add support for effect colorloop

* Bump to pydeconz v8

* Same domain everywhere

* Clean up

* Updated requirements_all

* Generated API key will now be stored in a config file

* Change battery sensor to register to callback since library now supports multiple callbacks
Move DeconzEvent to hub
Bump to v9

* Improve entity attributes

* Change end of battery name to battery level
No need for static icon variable when using battery level helper

* Bump requirement to v10

* Improve pydocstring for DeconzEvent
Rename TYPE_AS_EVENT to CONF_TYPE_AS_EVENT

* Allow separate brightness to override RGB brightness

* Expose device.reachable in entity available property

* Bump requirement to 11 (it goes up to 11!)

* Pylint comment

* Binary sensors don't have unit of measurement

* Removed service to generate API key in favor of just generating it as a last resort of no API key is specified in configuration.yaml or deconz.conf

* Replace clear text to attribute definitions

* Use more constants

* Bump requirements to v12

* Color temp requires xy color support

* Only ZHASwitch should be an event

* Bump requirements to v13

* Added effect_list property

* Add attribute to battery sensor to easy find event id

* Bump requirements to v14

* Fix hound comment

* Bumped requirements_all information to v14

* Add service to configure devices on deCONZ

* Add initial support for scenes

* Bump requirements to v15

* Fix review comments

* Python doc string improvement

* Improve setup and error handling during setup

* Changed how to evaluate light features

* Remove 'ghost' events by not triggering updates if the signal originates from a config event
Bump requirement to v17

* Fix pylint issue by moving scene ownership in to groups in requirement pydeconz
Bump requirement to v18

* Added configurator option to register to deCONZ when unlocking gateway through settings
Bump requirement to v20

* Improve async configurator

* No user interaction for deconz.conf

* No file management in event loop

* Improve readability of load platform

* Fewer entity attributes

* Use values() instead of items() for dicts where applicable

* Do one add devices per platform

* Clean up of unused attributes

* Make sure that discovery info is not None

* Only register configure service and shutdown service when deconz has been setup properly

* Move description

* Fix lines longer than 80

* Moved deconz services to a separate file and moved hub to deconz/__init__.py

* Remove option to configure switch as entity

* Moved DeconzEvent to sensor since it is only Switch buttonpress that will be sent as event

* Added support for automatic discovery of deconz
Thanks to Kroimon for adding support to netdisco

* Use markup for configuration description

* Fix coveragerc

* Remove deCONZ support from Hue component

* Improved docstrings and readability

* Remove unnecessary extra name for storing in hass.data, using domain instead

* Improve readability by renaming all async methods
Bump to v21 - improved async naming on methods

* Fix first line not being in imperative mood

* Added logo to configurator
Let deconz.conf be visible since it will be the main config for the component after initial setup

* Removed bridge_type from new unit tests as part of removing deconz support from hue component

* Capitalize first letters of Battery Level

* Properly update state of sensor as well as reachable and battery
Bump dependency to v22

* Fix flake8 Multi-line docstring closing quotes should be on a separate line

* Fix martinhjelmares comments

Bump dependency to v23
Use only HASS aiohttp session
Change when to use 'deconz' or domain or deconz data
Clean up unused logger defines
Remove unnecessary return values
Fix faulty references to component documentation
Move callback registration to after entity has been initialized by HASS
Less inception style on pydocs ;)
Simplify loading platforms by using a for loop
Added voluptous schema for service
Yaml file is for deconz only, no need to have the domain present
Remove domain constraint when creating event title
This commit is contained in:
Kane610 2018-01-01 17:08:13 +01:00 committed by Martin Hjelmare
parent 976a0fe38c
commit b9c852392c
12 changed files with 789 additions and 131 deletions

View File

@ -53,6 +53,9 @@ omit =
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
homeassistant/components/deconz/*
homeassistant/components/*/deconz.py
homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py

View File

@ -65,9 +65,11 @@ homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342

View File

@ -0,0 +1,97 @@
"""
Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
DEPENDENCIES = ['deconz']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup binary sensor for deCONZ component."""
if discovery_info is None:
return
from pydeconz.sensor import DECONZ_BINARY_SENSOR
sensors = hass.data[DECONZ_DATA].sensors
entities = []
for sensor in sensors.values():
if sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
class DeconzBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor."""
def __init__(self, sensor):
"""Setup sensor and add update callback to get data from websocket."""
self._sensor = sensor
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return true if sensor is on."""
return self._sensor.is_tripped
@property
def name(self):
"""Return the name of the sensor."""
return self._sensor.name
@property
def device_class(self):
"""Class of the sensor."""
return self._sensor.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._sensor.sensor_icon
@property
def available(self):
"""Return True if sensor is available."""
return self._sensor.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {
ATTR_BATTERY_LEVEL: self._sensor.battery,
}
if self._sensor.type == PRESENCE:
attr['dark'] = self._sensor.dark
return attr

View File

@ -0,0 +1,176 @@
"""
Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
import asyncio
import logging
import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pydeconz==23']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port,
})
}, extra=vol.ALLOW_EXTRA)
SERVICE_FIELD = 'field'
SERVICE_DATA = 'data'
SERVICE_SCHEMA = vol.Schema({
vol.Required(SERVICE_FIELD): cv.string,
vol.Required(SERVICE_DATA): cv.string,
})
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/)
"""
@asyncio.coroutine
def async_setup(hass, config):
"""Setup services and configuration for deCONZ component."""
result = False
config_file = yield from hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
@asyncio.coroutine
def async_deconz_discovered(service, discovery_info):
"""Called when deCONZ gateway has been found."""
deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
yield from async_request_configuration(hass, config, deconz_config)
if config_file:
result = yield from async_setup_deconz(hass, config, config_file)
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config:
result = yield from async_setup_deconz(hass, config, deconz_config)
else:
yield from async_request_configuration(hass, config, deconz_config)
return True
if not result:
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
return True
@asyncio.coroutine
def async_setup_deconz(hass, config, deconz_config):
"""Setup deCONZ session.
Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ.
"""
from pydeconz import DeconzSession
websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config)
result = yield from deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ.")
return False
hass.data[DOMAIN] = deconz
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(discovery.async_load_platform(
hass, component, DOMAIN, {}, config))
deconz.start()
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def async_configure(call):
"""Set attribute of device in deCONZ.
Field is a string representing a specific device in deCONZ
e.g. field='/lights/1/state'.
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/
"""
deconz = hass.data[DOMAIN]
field = call.data.get(SERVICE_FIELD)
data = call.data.get(SERVICE_DATA)
yield from deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, 'configure', async_configure,
descriptions['configure'], schema=SERVICE_SCHEMA)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz.close)
return True
@asyncio.coroutine
def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
@asyncio.coroutine
def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = yield from async_setup_deconz(hass, config, deconz_config)
if result:
yield from hass.async_add_job(save_json,
hass.config.path(CONFIG_FILE),
deconz_config)
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",
)

View File

@ -0,0 +1,10 @@
configure:
description: Set attribute of device in Deconz. See Dresden Elektroniks REST API documentation for details http://dresden-elektronik.github.io/deconz-rest-doc/rest/
fields:
field:
description: Field is a string representing a specific device in Deconz.
example: '/lights/1/state'
data:
description: Data is a json object with what data you want to alter.
example: '{"on": true}'

View File

@ -37,6 +37,7 @@ SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@ -50,6 +51,7 @@ SERVICE_HANDLERS = {
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),

View File

@ -0,0 +1,172 @@
"""
Support for deCONZ light.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.deconz/
"""
import asyncio
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_FLASH, ATTR_COLOR_TEMP, ATTR_EFFECT,
ATTR_RGB_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR)
from homeassistant.core import callback
from homeassistant.util.color import color_RGB_to_xy
DEPENDENCIES = ['deconz']
ATTR_LIGHT_GROUP = 'LightGroup'
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup light for deCONZ component."""
if discovery_info is None:
return
lights = hass.data[DECONZ_DATA].lights
groups = hass.data[DECONZ_DATA].groups
entities = []
for light in lights.values():
entities.append(DeconzLight(light))
for group in groups.values():
if group.lights: # Don't create entity for group not containing light
entities.append(DeconzLight(group))
async_add_devices(entities, True)
class DeconzLight(Light):
"""Representation of a deCONZ light."""
def __init__(self, light):
"""Setup light and add update callback to get data from websocket."""
self._light = light
self._features = SUPPORT_BRIGHTNESS
self._features |= SUPPORT_FLASH
self._features |= SUPPORT_TRANSITION
if self._light.ct is not None:
self._features |= SUPPORT_COLOR_TEMP
if self._light.xy is not None:
self._features |= SUPPORT_RGB_COLOR
self._features |= SUPPORT_XY_COLOR
if self._light.effect is not None:
self._features |= SUPPORT_EFFECT
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe to lights events."""
self._light.register_async_callback(self.async_update_callback)
@callback
def async_update_callback(self, reason):
"""Update the light's state."""
self.async_schedule_update_ha_state()
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._light.brightness
@property
def effect_list(self):
"""Return the list of supported effects."""
return [EFFECT_COLORLOOP]
@property
def color_temp(self):
"""Return the CT color value."""
return self._light.ct
@property
def xy_color(self):
"""Return the XY color value."""
return self._light.xy
@property
def is_on(self):
"""Return true if light is on."""
return self._light.state
@property
def name(self):
"""Return the name of the light."""
return self._light.name
@property
def supported_features(self):
"""Flag supported features."""
return self._features
@property
def available(self):
"""Return True if light is available."""
return self._light.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn on light."""
data = {'on': True}
if ATTR_COLOR_TEMP in kwargs:
data['ct'] = kwargs[ATTR_COLOR_TEMP]
if ATTR_RGB_COLOR in kwargs:
xyb = color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
data['xy'] = xyb[0], xyb[1]
data['bri'] = xyb[2]
if ATTR_BRIGHTNESS in kwargs:
data['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_TRANSITION in kwargs:
data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10
if ATTR_FLASH in kwargs:
if kwargs[ATTR_FLASH] == FLASH_SHORT:
data['alert'] = 'select'
del data['on']
elif kwargs[ATTR_FLASH] == FLASH_LONG:
data['alert'] = 'lselect'
del data['on']
if ATTR_EFFECT in kwargs:
if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
data['effect'] = 'colorloop'
else:
data['effect'] = 'none'
yield from self._light.async_set_state(data)
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn off light."""
data = {'on': False}
if ATTR_TRANSITION in kwargs:
data = {'bri': 0}
data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10
if ATTR_FLASH in kwargs:
if kwargs[ATTR_FLASH] == FLASH_SHORT:
data['alert'] = 'select'
del data['on']
elif kwargs[ATTR_FLASH] == FLASH_LONG:
data['alert'] = 'lselect'
del data['on']
yield from self._light.async_set_state(data)

View File

@ -130,14 +130,12 @@ def unthrottled_update_lights(hass, bridge, add_devices):
_LOGGER.exception('Cannot reach the bridge')
return
bridge_type = get_bridge_type(api)
new_lights = process_lights(
hass, api, bridge, bridge_type,
hass, api, bridge,
lambda **kw: update_lights(hass, bridge, add_devices, **kw))
if bridge.allow_hue_groups:
new_lightgroups = process_groups(
hass, api, bridge, bridge_type,
hass, api, bridge,
lambda **kw: update_lights(hass, bridge, add_devices, **kw))
new_lights.extend(new_lightgroups)
@ -145,16 +143,7 @@ def unthrottled_update_lights(hass, bridge, add_devices):
add_devices(new_lights)
def get_bridge_type(api):
"""Return the bridge type."""
api_name = api.get('config').get('name')
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
return 'deconz'
else:
return 'hue'
def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
def process_lights(hass, api, bridge, update_lights_cb):
"""Set up HueLight objects for all lights."""
api_lights = api.get('lights')
@ -169,7 +158,7 @@ def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
bridge.lights[light_id] = HueLight(
int(light_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_unreachable,
bridge.allow_in_emulated_hue)
new_lights.append(bridge.lights[light_id])
else:
@ -179,7 +168,7 @@ def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
return new_lights
def process_groups(hass, api, bridge, bridge_type, update_lights_cb):
def process_groups(hass, api, bridge, update_lights_cb):
"""Set up HueLight objects for all groups."""
api_groups = api.get('groups')
@ -199,7 +188,7 @@ def process_groups(hass, api, bridge, bridge_type, update_lights_cb):
bridge.lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_unreachable,
bridge.allow_in_emulated_hue, True)
new_lights.append(bridge.lightgroups[lightgroup_id])
else:
@ -213,14 +202,12 @@ class HueLight(Light):
"""Representation of a Hue light."""
def __init__(self, light_id, info, bridge, update_lights_cb,
bridge_type, allow_unreachable, allow_in_emulated_hue,
is_group=False):
allow_unreachable, allow_in_emulated_hue, is_group=False):
"""Initialize the light."""
self.light_id = light_id
self.info = info
self.bridge = bridge
self.update_lights = update_lights_cb
self.bridge_type = bridge_type
self.allow_unreachable = allow_unreachable
self.is_group = is_group
self.allow_in_emulated_hue = allow_in_emulated_hue
@ -330,7 +317,7 @@ class HueLight(Light):
elif flash == FLASH_SHORT:
command['alert'] = 'select'
del command['on']
elif self.bridge_type == 'hue':
else:
command['alert'] = 'none'
effect = kwargs.get(ATTR_EFFECT)
@ -340,8 +327,7 @@ class HueLight(Light):
elif effect == EFFECT_RANDOM:
command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254)
elif (self.bridge_type == 'hue' and
self.info.get('manufacturername') == 'Philips'):
elif self.info.get('manufacturername') == 'Philips':
command['effect'] = 'none'
self._command_func(self.light_id, command)
@ -361,7 +347,7 @@ class HueLight(Light):
elif flash == FLASH_SHORT:
command['alert'] = 'select'
del command['on']
elif self.bridge_type == 'hue':
else:
command['alert'] = 'none'
self._command_func(self.light_id, command)

View File

@ -0,0 +1,45 @@
"""
Support for deCONZ scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.deconz/
"""
import asyncio
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.components.scene import Scene
DEPENDENCIES = ['deconz']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up scenes for deCONZ component."""
if discovery_info is None:
return
scenes = hass.data[DECONZ_DATA].scenes
entities = []
for scene in scenes.values():
entities.append(DeconzScene(scene))
async_add_devices(entities)
class DeconzScene(Scene):
"""Representation of a deCONZ scene."""
def __init__(self, scene):
"""Setup scene."""
self._scene = scene
@asyncio.coroutine
def async_activate(self, **kwargs):
"""Activate the scene."""
yield from self._scene.async_set_state({})
@property
def name(self):
"""Return the name of the scene."""
return self._scene.full_name

View File

@ -0,0 +1,192 @@
"""
Support for deCONZ sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.deconz/
"""
import asyncio
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID
from homeassistant.core import callback, EventOrigin
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util import slugify
DEPENDENCIES = ['deconz']
ATTR_EVENT_ID = 'event_id'
ATTR_ZHASWITCH = 'ZHASwitch'
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup sensor for deCONZ component."""
if discovery_info is None:
return
from pydeconz.sensor import DECONZ_SENSOR
sensors = hass.data[DECONZ_DATA].sensors
entities = []
for sensor in sensors.values():
if sensor.type in DECONZ_SENSOR:
if sensor.type == ATTR_ZHASWITCH:
DeconzEvent(hass, sensor)
if sensor.battery:
entities.append(DeconzBattery(sensor))
else:
entities.append(DeconzSensor(sensor))
async_add_devices(entities, True)
class DeconzSensor(Entity):
"""Representation of a sensor."""
def __init__(self, sensor):
"""Setup sensor and add update callback to get data from websocket."""
self._sensor = sensor
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe to sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def state(self):
"""Return the state of the sensor."""
return self._sensor.state
@property
def name(self):
"""Return the name of the sensor."""
return self._sensor.name
@property
def device_class(self):
"""Class of the sensor."""
return self._sensor.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._sensor.sensor_icon
@property
def unit_of_measurement(self):
"""Unit of measurement of this sensor."""
return self._sensor.sensor_unit
@property
def available(self):
"""Return True if sensor is available."""
return self._sensor.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
attr = {
ATTR_BATTERY_LEVEL: self._sensor.battery,
}
return attr
class DeconzBattery(Entity):
"""Battery class for when a device is only represented as an event."""
def __init__(self, device):
"""Register dispatcher callback for update of battery state."""
self._device = device
self._name = self._device.name + ' Battery Level'
self._device_class = 'battery'
self._unit_of_measurement = "%"
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe to sensors events."""
self._device.register_async_callback(self.async_update_callback)
@callback
def async_update_callback(self, reason):
"""Update the battery's state, if needed."""
if 'battery' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def state(self):
"""Return the state of the battery."""
return self._device.battery
@property
def name(self):
"""Return the name of the battery."""
return self._name
@property
def device_class(self):
"""Class of the sensor."""
return self._device_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return icon_for_battery_level(int(self.state))
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self._unit_of_measurement
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the battery."""
attr = {
ATTR_EVENT_ID: slugify(self._device.name),
}
return attr
class DeconzEvent(object):
"""When you want signals instead of entities.
Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""
def __init__(self, hass, device):
"""Register callback that will be used for signals."""
self._hass = hass
self._device = device
self._device.register_async_callback(self.async_update_callback)
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""
if reason['state']:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View File

@ -664,6 +664,9 @@ pycsspeechtts==1.0.2
# homeassistant.components.sensor.cups
# pycups==1.9.73
# homeassistant.components.deconz
pydeconz==23
# homeassistant.components.zwave
pydispatcher==2.0.5

View File

@ -32,7 +32,6 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.allow_hue_groups = False
self.mock_api = MagicMock()
self.mock_bridge.get_api.return_value = self.mock_api
self.mock_bridge_type = MagicMock()
self.mock_lights = []
self.mock_groups = []
self.mock_add_devices = MagicMock()
@ -43,7 +42,6 @@ class TestSetup(unittest.TestCase):
self.mock_api = MagicMock()
self.mock_api.get.return_value = {}
self.mock_bridge.get_api.return_value = self.mock_api
self.mock_bridge_type = MagicMock()
def setup_mocks_for_process_groups(self):
"""Set up all mocks for process_groups tests."""
@ -55,8 +53,6 @@ class TestSetup(unittest.TestCase):
self.mock_api.get.return_value = {}
self.mock_bridge.get_api.return_value = self.mock_api
self.mock_bridge_type = MagicMock()
def create_mock_bridge(self, host, allow_hue_groups=True):
"""Return a mock HueBridge with reasonable defaults."""
mock_bridge = MagicMock()
@ -137,21 +133,18 @@ class TestSetup(unittest.TestCase):
"""Test the update_lights function when no lights are found."""
self.setup_mocks_for_update_lights()
with patch('homeassistant.components.light.hue.get_bridge_type',
return_value=self.mock_bridge_type):
with patch('homeassistant.components.light.hue.process_lights',
return_value=[]) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch('homeassistant.components.light.hue.process_lights',
return_value=[]) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge,
self.mock_bridge_type, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_not_called()
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_not_called()
@MockDependency('phue')
def test_update_lights_with_some_lights(self, mock_phue):
@ -159,22 +152,19 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_update_lights()
self.mock_lights = ['some', 'light']
with patch('homeassistant.components.light.hue.get_bridge_type',
return_value=self.mock_bridge_type):
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge,
self.mock_bridge_type, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
@MockDependency('phue')
def test_update_lights_no_groups(self, mock_phue):
@ -183,24 +173,20 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.allow_hue_groups = True
self.mock_lights = ['some', 'light']
with patch('homeassistant.components.light.hue.get_bridge_type',
return_value=self.mock_bridge_type):
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge,
self.mock_bridge_type, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge,
self.mock_bridge_type, mock.ANY)
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
@MockDependency('phue')
def test_update_lights_with_lights_and_groups(self, mock_phue):
@ -210,24 +196,20 @@ class TestSetup(unittest.TestCase):
self.mock_lights = ['some', 'light']
self.mock_groups = ['and', 'groups']
with patch('homeassistant.components.light.hue.get_bridge_type',
return_value=self.mock_bridge_type):
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge,
self.mock_bridge_type, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge,
self.mock_bridge_type, mock.ANY)
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
@MockDependency('phue')
def test_update_lights_with_two_bridges(self, mock_phue):
@ -242,23 +224,21 @@ class TestSetup(unittest.TestCase):
mock_bridge_two_lights = self.create_mock_lights(
{1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}})
with patch('homeassistant.components.light.hue.get_bridge_type',
return_value=self.mock_bridge_type):
with patch('homeassistant.components.light.hue.HueLight.'
'schedule_update_ha_state'):
mock_api = MagicMock()
mock_api.get.return_value = mock_bridge_one_lights
with patch.object(mock_bridge_one, 'get_api',
return_value=mock_api):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_one, self.mock_add_devices)
with patch('homeassistant.components.light.hue.HueLight.'
'schedule_update_ha_state'):
mock_api = MagicMock()
mock_api.get.return_value = mock_bridge_one_lights
with patch.object(mock_bridge_one, 'get_api',
return_value=mock_api):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_one, self.mock_add_devices)
mock_api = MagicMock()
mock_api.get.return_value = mock_bridge_two_lights
with patch.object(mock_bridge_two, 'get_api',
return_value=mock_api):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_two, self.mock_add_devices)
mock_api = MagicMock()
mock_api.get.return_value = mock_bridge_two_lights
with patch.object(mock_bridge_two, 'get_api',
return_value=mock_api):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_two, self.mock_add_devices)
self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2])
self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3])
@ -299,8 +279,7 @@ class TestSetup(unittest.TestCase):
self.mock_api.get.return_value = None
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals([], ret)
self.assertEquals(self.mock_bridge.lights, {})
@ -310,8 +289,7 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_process_lights()
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals([], ret)
self.assertEquals(self.mock_bridge.lights, {})
@ -324,18 +302,17 @@ class TestSetup(unittest.TestCase):
1: {'state': 'on'}, 2: {'state': 'off'}}
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
self.assertEquals(len(self.mock_bridge.lights), 2)
@ -353,14 +330,13 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.lights = {1: MagicMock()}
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
self.assertEquals(len(self.mock_bridge.lights), 2)
@ -373,8 +349,7 @@ class TestSetup(unittest.TestCase):
self.mock_api.get.return_value = None
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals([], ret)
self.assertEquals(self.mock_bridge.lightgroups, {})
@ -385,8 +360,7 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.get_group.return_value = {'name': 'Group 0'}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals([], ret)
self.assertEquals(self.mock_bridge.lightgroups, {})
@ -399,18 +373,17 @@ class TestSetup(unittest.TestCase):
1: {'state': 'on'}, 2: {'state': 'off'}}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
@ -428,14 +401,13 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.lightgroups = {1: MagicMock()}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
None)
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
@ -455,7 +427,6 @@ class TestHueLight(unittest.TestCase):
self.mock_info = MagicMock()
self.mock_bridge = MagicMock()
self.mock_update_lights = MagicMock()
self.mock_bridge_type = MagicMock()
self.mock_allow_unreachable = MagicMock()
self.mock_is_group = MagicMock()
self.mock_allow_in_emulated_hue = MagicMock()
@ -476,7 +447,6 @@ class TestHueLight(unittest.TestCase):
(update_lights
if update_lights is not None
else self.mock_update_lights),
self.mock_bridge_type,
self.mock_allow_unreachable, self.mock_allow_in_emulated_hue,
is_group if is_group is not None else self.mock_is_group)