Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions homeassistant/components/zha/core/channels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def decorate_command(channel, command):
"""Wrap a cluster command to make it safe."""
@wraps(command)
async def wrapper(*args, **kwds):
from zigpy.zcl.foundation import Status
from zigpy.exceptions import DeliveryError
try:
result = await command(*args, **kwds)
Expand All @@ -54,17 +53,16 @@ async def wrapper(*args, **kwds):
"{}: {}".format("with args", args),
"{}: {}".format("with kwargs", kwds),
"{}: {}".format("and result", result))
if isinstance(result, bool):
return result
return result[1] is Status.SUCCESS
return result

except (DeliveryError, Timeout) as ex:
_LOGGER.debug(
"%s: command failed: %s exception: %s",
channel.unique_id,
command.__name__,
str(ex)
)
return False
return ex
return wrapper


Expand Down
39 changes: 39 additions & 0 deletions homeassistant/components/zha/core/channels/closures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,44 @@
https://home-assistant.io/components/zha/
"""
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import ZigbeeChannel
from ..const import SIGNAL_ATTR_UPDATED

_LOGGER = logging.getLogger(__name__)


class DoorLockChannel(ZigbeeChannel):
"""Door lock channel."""

_value_attribute = 0

async def async_update(self):
"""Retrieve latest state."""
result = await self.get_attribute_value('lock_state', from_cache=True)

async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
result
)

@callback
def attribute_updated(self, attrid, value):
"""Handle attribute update from lock cluster."""
attr_name = self.cluster.attributes.get(attrid, [attrid])[0]
_LOGGER.debug("%s: Attribute report '%s'[%s] = %s",
self.unique_id, self.cluster.name, attr_name, value)
if attrid == self._value_attribute:
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
value
)

async def async_initialize(self, from_cache):
"""Initialize channel."""
await self.get_attribute_value(
self._value_attribute, from_cache=from_cache)
await super().async_initialize(from_cache)
4 changes: 3 additions & 1 deletion homeassistant/components/zha/core/channels/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
https://home-assistant.io/components/zha/
"""
from . import ZigbeeChannel

from .closures import DoorLockChannel
from .general import (
OnOffChannel, LevelControlChannel, PowerConfigurationChannel, BasicChannel
)
Expand All @@ -13,7 +15,6 @@
from .lighting import ColorChannel
from .security import IASZoneChannel


ZIGBEE_CHANNEL_REGISTRY = {}


Expand Down Expand Up @@ -44,4 +45,5 @@ def populate_channel_registry():
zcl.clusters.security.IasZone.cluster_id: IASZoneChannel,
zcl.clusters.hvac.Fan.cluster_id: FanChannel,
zcl.clusters.lightlink.LightLink.cluster_id: ZigbeeChannel,
zcl.clusters.closures.DoorLock.cluster_id: DoorLockChannel,
})
3 changes: 3 additions & 0 deletions homeassistant/components/zha/core/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT
from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH

Expand All @@ -27,6 +28,7 @@
BINARY_SENSOR,
FAN,
LIGHT,
LOCK,
SENSOR,
SWITCH,
)
Expand Down Expand Up @@ -92,6 +94,7 @@
ELECTRICAL_MEASUREMENT_CHANNEL = 'electrical_measurement'
POWER_CONFIGURATION_CHANNEL = 'power'
EVENT_RELAY_CHANNEL = 'event_relay'
DOORLOCK_CHANNEL = 'door_lock'

SIGNAL_ATTR_UPDATED = 'attribute_updated'
SIGNAL_MOVE_LEVEL = "move_level"
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/zha/core/registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT
from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH

Expand Down Expand Up @@ -154,7 +155,8 @@ def get_deconz_radio():
zcl.clusters.hvac.Fan: FAN,
SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
zcl.clusters.general.AnalogInput.cluster_id: SENSOR
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
zcl.clusters.closures.DoorLock: LOCK
})

SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
Expand Down Expand Up @@ -282,6 +284,10 @@ def get_deconz_radio():
'attr': 'fan_mode',
'config': REPORT_CONFIG_OP
}],
zcl.clusters.closures.DoorLock.cluster_id: [{
'attr': 'lock_state',
'config': REPORT_CONFIG_IMMEDIATE
}],
})

BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id)
Expand Down
34 changes: 17 additions & 17 deletions homeassistant/components/zha/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
import logging

from zigpy.zcl.foundation import Status
from homeassistant.components import light
from homeassistant.const import STATE_ON
from homeassistant.core import callback
Expand All @@ -14,7 +15,6 @@
)
from .entity import ZhaEntity


_LOGGER = logging.getLogger(__name__)

DEFAULT_DURATION = 5
Expand Down Expand Up @@ -173,33 +173,33 @@ async def async_turn_on(self, **kwargs):
level = min(254, brightness)
else:
level = self._brightness or 254
success = await self._level_channel.move_to_level_with_on_off(
result = await self._level_channel.move_to_level_with_on_off(
level,
duration
)
t_log['move_to_level_with_on_off'] = success
if not success:
t_log['move_to_level_with_on_off'] = result
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._state = bool(level)
if level:
self._brightness = level

if brightness is None or brightness:
success = await self._on_off_channel.on()
t_log['on_off'] = success
if not success:
result = await self._on_off_channel.on()
t_log['on_off'] = result
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._state = True

if light.ATTR_COLOR_TEMP in kwargs and \
self.supported_features & light.SUPPORT_COLOR_TEMP:
temperature = kwargs[light.ATTR_COLOR_TEMP]
success = await self._color_channel.move_to_color_temp(
result = await self._color_channel.move_to_color_temp(
temperature, duration)
t_log['move_to_color_temp'] = success
if not success:
t_log['move_to_color_temp'] = result
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._color_temp = temperature
Expand All @@ -208,13 +208,13 @@ async def async_turn_on(self, **kwargs):
self.supported_features & light.SUPPORT_COLOR:
hs_color = kwargs[light.ATTR_HS_COLOR]
xy_color = color_util.color_hs_to_xy(*hs_color)
success = await self._color_channel.move_to_color(
result = await self._color_channel.move_to_color(
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
duration,
)
t_log['move_to_color'] = success
if not success:
t_log['move_to_color'] = result
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._hs_color = hs_color
Expand All @@ -227,14 +227,14 @@ async def async_turn_off(self, **kwargs):
duration = kwargs.get(light.ATTR_TRANSITION)
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
if duration and supports_level:
success = await self._level_channel.move_to_level_with_on_off(
result = await self._level_channel.move_to_level_with_on_off(
0,
duration*10
)
else:
success = await self._on_off_channel.off()
self.debug("turned off: %s", success)
if not success:
result = await self._on_off_channel.off()
self.debug("turned off: %s", result)
if not isinstance(result, list) or result[1] is not Status.SUCCESS:
return
self._state = False
self.async_schedule_update_ha_state()
Expand Down
134 changes: 134 additions & 0 deletions homeassistant/components/zha/lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Locks on Zigbee Home Automation networks."""
import logging

from zigpy.zcl.foundation import Status
from homeassistant.core import callback
from homeassistant.components.lock import (
DOMAIN, STATE_UNLOCKED, STATE_LOCKED, LockDevice)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, DOORLOCK_CHANNEL,
SIGNAL_ATTR_UPDATED
)
from .entity import ZhaEntity

_LOGGER = logging.getLogger(__name__)

""" The first state is Zigbee 'Not fully locked' """

STATE_LIST = [
STATE_UNLOCKED,
STATE_LOCKED,
STATE_UNLOCKED
Comment thread
Adminiuga marked this conversation as resolved.
]

VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)}


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old way of setting up Zigbee Home Automation locks."""
pass


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation Door Lock from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])

unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)

locks = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if locks is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
locks.values())
del hass.data[DATA_ZHA][DOMAIN]


async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA locks."""
entities = []
for discovery_info in discovery_infos:
entities.append(ZhaDoorLock(**discovery_info))

async_add_entities(entities, update_before_add=True)


class ZhaDoorLock(ZhaEntity, LockDevice):
"""Representation of a ZHA lock."""

_domain = DOMAIN

def __init__(self, unique_id, zha_device, channels, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs)
self._doorlock_channel = self.cluster_channels.get(DOORLOCK_CHANNEL)

async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
await self.async_accept_signal(
self._doorlock_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)

@callback
def async_restore_last_state(self, last_state):
"""Restore previous state."""
self._state = VALUE_TO_STATE.get(last_state.state, last_state.state)

@property
def is_locked(self) -> bool:
"""Return true if entity is locked."""
if self._state is None:
return False
return self._state == STATE_LOCKED

@property
def device_state_attributes(self):
"""Return state attributes."""
return self.state_attributes

async def async_lock(self, **kwargs):
"""Lock the lock."""
result = await self._doorlock_channel.lock_door()
if not isinstance(result, list) or result[0] is not Status.SUCCESS:
_LOGGER.error("Error with lock_door: %s", result)
return
self.async_schedule_update_ha_state()

async def async_unlock(self, **kwargs):
"""Unlock the lock."""
result = await self._doorlock_channel.unlock_door()
if not isinstance(result, list) or result[0] is not Status.SUCCESS:
_LOGGER.error("Error with unlock_door: %s", result)
return
self.async_schedule_update_ha_state()

async def async_update(self):
"""Attempt to retrieve state from the lock."""
await super().async_update()
await self.async_get_state()

def async_set_state(self, state):
"""Handle state update from channel."""
self._state = VALUE_TO_STATE.get(state, self._state)
self.async_schedule_update_ha_state()

async def async_get_state(self, from_cache=True):
"""Attempt to retrieve state from the lock."""
if self._doorlock_channel:
state = await self._doorlock_channel.get_attribute_value(
'lock_state', from_cache=from_cache)
if state is not None:
self._state = VALUE_TO_STATE.get(state, self._state)

async def refresh(self, time):
"""Call async_get_state at an interval."""
await self.async_get_state(from_cache=False)

def debug(self, msg, *args):
"""Log debug message."""
_LOGGER.debug('%s: ' + msg, self.entity_id, *args)
Loading