diff --git a/.coveragerc b/.coveragerc index 0876aa0d7b7..a7c961d5a09 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/CODEOWNERS b/CODEOWNERS index 37a2494c182..99c103b1298 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py new file mode 100644 index 00000000000..97f78ff21d0 --- /dev/null +++ b/homeassistant/components/binary_sensor/deconz.py @@ -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 diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py new file mode 100644 index 00000000000..4b89594c62e --- /dev/null +++ b/homeassistant/components/deconz/__init__.py @@ -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", + ) diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml new file mode 100644 index 00000000000..2e6593c6ea0 --- /dev/null +++ b/homeassistant/components/deconz/services.yaml @@ -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}' diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index dde33aa10a2..b6578dd70fe 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -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'), diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py new file mode 100644 index 00000000000..a1c43ad4cbc --- /dev/null +++ b/homeassistant/components/light/deconz.py @@ -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) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index f5c910ea116..64e5dff0d26 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -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) diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py new file mode 100644 index 00000000000..f035ae3128e --- /dev/null +++ b/homeassistant/components/scene/deconz.py @@ -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 diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py new file mode 100644 index 00000000000..c01483169cb --- /dev/null +++ b/homeassistant/components/sensor/deconz.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index 0b7b804d277..9a8741aa4cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 7955cecba04..b612fa15931 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -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)