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 + '.{}'
|
||||
|
||||
DEVICE_CLASSES = [
|
||||
'window', # Window control
|
||||
'damper',
|
||||
'garage', # Garage door control
|
||||
'window', # Window control
|
||||
]
|
||||
|
||||
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):
|
||||
"""Track states and offer events for covers."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
|
||||
|
||||
await component.async_setup(config)
|
||||
|
@ -195,6 +196,16 @@ async def async_setup(hass, config):
|
|||
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):
|
||||
"""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']
|
||||
|
||||
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
|
||||
'light', 'scene', 'sensor', 'switch']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
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_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(
|
||||
config_entry, component))
|
||||
|
||||
|
@ -228,7 +231,7 @@ async def async_unload_entry(hass, config_entry):
|
|||
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
|
||||
deconz.close()
|
||||
|
||||
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
|
||||
for component in SUPPORTED_PLATFORMS:
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry, component)
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
|||
ATTR_DARK = 'dark'
|
||||
ATTR_ON = 'on'
|
||||
|
||||
COVER_TYPES = ["Level controllable output"]
|
||||
|
||||
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
|
||||
SIRENS = ["Warning device"]
|
||||
SWITCH_TYPES = POWER_PLUGS + SIRENS
|
||||
|
|
|
@ -6,7 +6,8 @@ https://home-assistant.io/components/light.deconz/
|
|||
"""
|
||||
from homeassistant.components.deconz.const import (
|
||||
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 (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
|
||||
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."""
|
||||
entities = []
|
||||
for light in lights:
|
||||
if light.type not in SWITCH_TYPES:
|
||||
if light.type not in COVER_TYPES + SWITCH_TYPES:
|
||||
entities.append(DeconzLight(light))
|
||||
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.DATA_DECONZ_ID] == {}
|
||||
assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1
|
||||
assert len(mock_add_job.mock_calls) == 5
|
||||
assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 5
|
||||
assert len(mock_add_job.mock_calls) == \
|
||||
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] == \
|
||||
(entry, 'binary_sensor')
|
||||
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] == \
|
||||
(entry, 'scene')
|
||||
(entry, 'light')
|
||||
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] == \
|
||||
(entry, 'sensor')
|
||||
assert mock_config_entries.async_forward_entry_setup.mock_calls[5][1] == \
|
||||
(entry, 'switch')
|
||||
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ async def test_no_switches(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})
|
||||
assert "switch.switch_1_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