Skip to content

Commit

Permalink
Merge 5019b4d into 8b42d0c
Browse files Browse the repository at this point in the history
  • Loading branch information
Kane610 committed Sep 21, 2018
2 parents 8b42d0c + 5019b4d commit d6f28e7
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 12 deletions.
15 changes: 13 additions & 2 deletions homeassistant/components/cover/__init__.py
Expand Up @@ -35,8 +35,9 @@
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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""

Expand Down
146 changes: 146 additions & 0 deletions homeassistant/components/cover/deconz.py
@@ -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),
}
7 changes: 5 additions & 2 deletions homeassistant/components/deconz/__init__.py
Expand Up @@ -26,6 +26,9 @@

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,
Expand Down Expand Up @@ -104,7 +107,7 @@ def async_add_device_callback(device_type, device):
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))

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/deconz/const.py
Expand Up @@ -16,6 +16,8 @@
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
5 changes: 3 additions & 2 deletions homeassistant/components/light/deconz.py
Expand Up @@ -6,7 +6,8 @@
"""
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,
Expand All @@ -33,7 +34,7 @@ def async_add_light(lights):
"""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)

Expand Down
84 changes: 84 additions & 0 deletions tests/components/cover/test_deconz.py
@@ -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
14 changes: 9 additions & 5 deletions tests/components/deconz/test_init.py
Expand Up @@ -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')


Expand Down
2 changes: 1 addition & 1 deletion tests/components/switch/test_deconz.py
Expand Up @@ -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]
Expand Down

0 comments on commit d6f28e7

Please sign in to comment.