parent
8b42d0c471
commit
7fe0d8b2f4
8 changed files with 263 additions and 12 deletions
|
@ -35,8 +35,9 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
DEVICE_CLASSES = [
|
DEVICE_CLASSES = [
|
||||||
'window', # Window control
|
'damper',
|
||||||
'garage', # Garage door control
|
'garage', # Garage door control
|
||||||
|
'window', # Window control
|
||||||
]
|
]
|
||||||
|
|
||||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||||
|
@ -140,7 +141,7 @@ def stop_cover_tilt(hass, entity_id=None):
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Track states and offer events for covers."""
|
"""Track states and offer events for covers."""
|
||||||
component = EntityComponent(
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
|
||||||
|
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
|
@ -195,6 +196,16 @@ async def async_setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up a config entry."""
|
||||||
|
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class CoverDevice(Entity):
|
class CoverDevice(Entity):
|
||||||
"""Representation a cover."""
|
"""Representation a cover."""
|
||||||
|
|
||||||
|
|
146
homeassistant/components/cover/deconz.py
Normal file
146
homeassistant/components/cover/deconz.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
"""
|
||||||
|
Support for deCONZ covers.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/cover.deconz/
|
||||||
|
"""
|
||||||
|
from homeassistant.components.deconz.const import (
|
||||||
|
COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
|
||||||
|
DECONZ_DOMAIN)
|
||||||
|
from homeassistant.components.cover import (
|
||||||
|
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN,
|
||||||
|
SUPPORT_SET_POSITION)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
DEPENDENCIES = ['deconz']
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
|
discovery_info=None):
|
||||||
|
"""Unsupported way of setting up deCONZ covers."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up covers for deCONZ component.
|
||||||
|
|
||||||
|
Covers are based on same device class as lights in deCONZ.
|
||||||
|
"""
|
||||||
|
@callback
|
||||||
|
def async_add_cover(lights):
|
||||||
|
"""Add cover from deCONZ."""
|
||||||
|
entities = []
|
||||||
|
for light in lights:
|
||||||
|
if light.type in COVER_TYPES:
|
||||||
|
entities.append(DeconzCover(light))
|
||||||
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
hass.data[DATA_DECONZ_UNSUB].append(
|
||||||
|
async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover))
|
||||||
|
|
||||||
|
async_add_cover(hass.data[DATA_DECONZ].lights.values())
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzCover(CoverDevice):
|
||||||
|
"""Representation of a deCONZ cover."""
|
||||||
|
|
||||||
|
def __init__(self, cover):
|
||||||
|
"""Set up cover and add update callback to get data from websocket."""
|
||||||
|
self._cover = cover
|
||||||
|
self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Subscribe to covers events."""
|
||||||
|
self._cover.register_async_callback(self.async_update_callback)
|
||||||
|
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._cover.deconz_id
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Disconnect cover object when removed."""
|
||||||
|
self._cover.remove_callback(self.async_update_callback)
|
||||||
|
self._cover = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_callback(self, reason):
|
||||||
|
"""Update the cover's state."""
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_cover_position(self):
|
||||||
|
"""Return the current position of the cover."""
|
||||||
|
if self.is_closed:
|
||||||
|
return 0
|
||||||
|
return int(self._cover.brightness / 255 * 100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
"""Return if the cover is closed."""
|
||||||
|
return not self._cover.state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the cover."""
|
||||||
|
return self._cover.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique identifier for this cover."""
|
||||||
|
return self._cover.uniqueid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the class of the cover."""
|
||||||
|
return 'damper'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return self._features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if light is available."""
|
||||||
|
return self._cover.reachable
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_set_cover_position(self, **kwargs):
|
||||||
|
"""Move the cover to a specific position."""
|
||||||
|
position = kwargs[ATTR_POSITION]
|
||||||
|
data = {'on': False}
|
||||||
|
if position > 0:
|
||||||
|
data['on'] = True
|
||||||
|
data['bri'] = int(position / 100 * 255)
|
||||||
|
await self._cover.async_set_state(data)
|
||||||
|
|
||||||
|
async def async_open_cover(self, **kwargs):
|
||||||
|
"""Open cover."""
|
||||||
|
data = {ATTR_POSITION: 100}
|
||||||
|
await self.async_set_cover_position(**data)
|
||||||
|
|
||||||
|
async def async_close_cover(self, **kwargs):
|
||||||
|
"""Close cover."""
|
||||||
|
data = {ATTR_POSITION: 0}
|
||||||
|
await self.async_set_cover_position(**data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return a device description for device registry."""
|
||||||
|
if (self._cover.uniqueid is None or
|
||||||
|
self._cover.uniqueid.count(':') != 7):
|
||||||
|
return None
|
||||||
|
serial = self._cover.uniqueid.split('-', 1)[0]
|
||||||
|
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
|
||||||
|
return {
|
||||||
|
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||||
|
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||||
|
'manufacturer': self._cover.manufacturer,
|
||||||
|
'model': self._cover.modelid,
|
||||||
|
'name': self._cover.name,
|
||||||
|
'sw_version': self._cover.swversion,
|
||||||
|
'via_hub': (DECONZ_DOMAIN, bridgeid),
|
||||||
|
}
|
|
@ -26,6 +26,9 @@ from .const import (
|
||||||
|
|
||||||
REQUIREMENTS = ['pydeconz==47']
|
REQUIREMENTS = ['pydeconz==47']
|
||||||
|
|
||||||
|
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
|
||||||
|
'light', 'scene', 'sensor', 'switch']
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Optional(CONF_API_KEY): cv.string,
|
vol.Optional(CONF_API_KEY): cv.string,
|
||||||
|
@ -104,7 +107,7 @@ async def async_setup_entry(hass, config_entry):
|
||||||
hass.data[DATA_DECONZ_EVENT] = []
|
hass.data[DATA_DECONZ_EVENT] = []
|
||||||
hass.data[DATA_DECONZ_UNSUB] = []
|
hass.data[DATA_DECONZ_UNSUB] = []
|
||||||
|
|
||||||
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
|
for component in SUPPORTED_PLATFORMS:
|
||||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, component))
|
config_entry, component))
|
||||||
|
|
||||||
|
@ -228,7 +231,7 @@ async def async_unload_entry(hass, config_entry):
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
|
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
|
||||||
deconz.close()
|
deconz.close()
|
||||||
|
|
||||||
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
|
for component in SUPPORTED_PLATFORMS:
|
||||||
await hass.config_entries.async_forward_entry_unload(
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
config_entry, component)
|
config_entry, component)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
||||||
ATTR_DARK = 'dark'
|
ATTR_DARK = 'dark'
|
||||||
ATTR_ON = 'on'
|
ATTR_ON = 'on'
|
||||||
|
|
||||||
|
COVER_TYPES = ["Level controllable output"]
|
||||||
|
|
||||||
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
|
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
|
||||||
SIRENS = ["Warning device"]
|
SIRENS = ["Warning device"]
|
||||||
SWITCH_TYPES = POWER_PLUGS + SIRENS
|
SWITCH_TYPES = POWER_PLUGS + SIRENS
|
||||||
|
|
|
@ -6,7 +6,8 @@ https://home-assistant.io/components/light.deconz/
|
||||||
"""
|
"""
|
||||||
from homeassistant.components.deconz.const import (
|
from homeassistant.components.deconz.const import (
|
||||||
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ,
|
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ,
|
||||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, SWITCH_TYPES)
|
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN,
|
||||||
|
COVER_TYPES, SWITCH_TYPES)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
|
||||||
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
|
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
|
||||||
|
@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Add light from deCONZ."""
|
"""Add light from deCONZ."""
|
||||||
entities = []
|
entities = []
|
||||||
for light in lights:
|
for light in lights:
|
||||||
if light.type not in SWITCH_TYPES:
|
if light.type not in COVER_TYPES + SWITCH_TYPES:
|
||||||
entities.append(DeconzLight(light))
|
entities.append(DeconzLight(light))
|
||||||
async_add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
84
tests/components/cover/test_deconz.py
Normal file
84
tests/components/cover/test_deconz.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
"""deCONZ cover platform tests."""
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import deconz
|
||||||
|
from homeassistant.components.deconz.const import COVER_TYPES
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
SUPPORTED_COVERS = {
|
||||||
|
"1": {
|
||||||
|
"id": "Cover 1 id",
|
||||||
|
"name": "Cover 1 name",
|
||||||
|
"type": "Level controllable output",
|
||||||
|
"state": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSUPPORTED_COVER = {
|
||||||
|
"1": {
|
||||||
|
"id": "Cover id",
|
||||||
|
"name": "Unsupported switch",
|
||||||
|
"type": "Not a cover",
|
||||||
|
"state": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_bridge(hass, data):
|
||||||
|
"""Load the deCONZ cover platform."""
|
||||||
|
from pydeconz import DeconzSession
|
||||||
|
loop = Mock()
|
||||||
|
session = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
|
||||||
|
bridge = DeconzSession(loop, session, **entry.data)
|
||||||
|
with patch('pydeconz.DeconzSession.async_get_state',
|
||||||
|
return_value=mock_coro(data)):
|
||||||
|
await bridge.async_load_parameters()
|
||||||
|
hass.data[deconz.DOMAIN] = bridge
|
||||||
|
hass.data[deconz.DATA_DECONZ_UNSUB] = []
|
||||||
|
hass.data[deconz.DATA_DECONZ_ID] = {}
|
||||||
|
config_entry = config_entries.ConfigEntry(
|
||||||
|
1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test',
|
||||||
|
config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||||
|
await hass.config_entries.async_forward_entry_setup(config_entry, 'cover')
|
||||||
|
# To flush out the service call to update the group
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_switches(hass):
|
||||||
|
"""Test that no cover entities are created."""
|
||||||
|
data = {}
|
||||||
|
await setup_bridge(hass, data)
|
||||||
|
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cover(hass):
|
||||||
|
"""Test that all supported cover entities are created."""
|
||||||
|
await setup_bridge(hass, {"lights": SUPPORTED_COVERS})
|
||||||
|
assert "cover.cover_1_name" in hass.data[deconz.DATA_DECONZ_ID]
|
||||||
|
assert len(SUPPORTED_COVERS) == len(COVER_TYPES)
|
||||||
|
assert len(hass.states.async_all()) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_new_cover(hass):
|
||||||
|
"""Test successful creation of cover entity."""
|
||||||
|
data = {}
|
||||||
|
await setup_bridge(hass, data)
|
||||||
|
cover = Mock()
|
||||||
|
cover.name = 'name'
|
||||||
|
cover.type = "Level controllable output"
|
||||||
|
cover.register_async_callback = Mock()
|
||||||
|
async_dispatcher_send(hass, 'deconz_new_light', [cover])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert "cover.name" in hass.data[deconz.DATA_DECONZ_ID]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unsupported_cover(hass):
|
||||||
|
"""Test that unsupported covers are not created."""
|
||||||
|
await setup_bridge(hass, {"lights": UNSUPPORTED_COVER})
|
||||||
|
assert len(hass.states.async_all()) == 0
|
|
@ -112,17 +112,21 @@ async def test_setup_entry_successful(hass):
|
||||||
assert hass.data[deconz.DOMAIN]
|
assert hass.data[deconz.DOMAIN]
|
||||||
assert hass.data[deconz.DATA_DECONZ_ID] == {}
|
assert hass.data[deconz.DATA_DECONZ_ID] == {}
|
||||||
assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1
|
assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1
|
||||||
assert len(mock_add_job.mock_calls) == 5
|
assert len(mock_add_job.mock_calls) == \
|
||||||
assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 5
|
len(deconz.SUPPORTED_PLATFORMS)
|
||||||
|
assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == \
|
||||||
|
len(deconz.SUPPORTED_PLATFORMS)
|
||||||
assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \
|
assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \
|
||||||
(entry, 'binary_sensor')
|
(entry, 'binary_sensor')
|
||||||
assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \
|
assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \
|
||||||
(entry, 'light')
|
(entry, 'cover')
|
||||||
assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \
|
assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \
|
||||||
(entry, 'scene')
|
(entry, 'light')
|
||||||
assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \
|
assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \
|
||||||
(entry, 'sensor')
|
(entry, 'scene')
|
||||||
assert mock_config_entries.async_forward_entry_setup.mock_calls[4][1] == \
|
assert mock_config_entries.async_forward_entry_setup.mock_calls[4][1] == \
|
||||||
|
(entry, 'sensor')
|
||||||
|
assert mock_config_entries.async_forward_entry_setup.mock_calls[5][1] == \
|
||||||
(entry, 'switch')
|
(entry, 'switch')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ async def test_no_switches(hass):
|
||||||
|
|
||||||
|
|
||||||
async def test_switch(hass):
|
async def test_switch(hass):
|
||||||
"""Test that all supported switch entities and switch group are created."""
|
"""Test that all supported switch entities are created."""
|
||||||
await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES})
|
await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES})
|
||||||
assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID]
|
assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID]
|
||||||
assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID]
|
assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID]
|
||||||
|
|
Loading…
Add table
Reference in a new issue