mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
ZHA component rewrite (#20434)
* rebase reorg * update coveragerc for now * sensor cleanup * remove availability tracking for entities * finish removing changes from tests * review comments pass 1 * use asyncio.gather - review comments * review comments * cleanup - review comments * review comments * review comments * cleanup * cleanup - review comments * review comments * review comments * use signal for removal * correct comment * remove entities from gateway * remove dead module * remove accidently committed file * use named tuple - review comments * squash bugs * squash bugs * add light and sensor back to coveragerc until % is higher
This commit is contained in:
parent
65a225da75
commit
e6cd04d711
@ -658,7 +658,6 @@ omit =
|
||||
homeassistant/components/zeroconf/*
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/api.py
|
||||
homeassistant/components/zha/binary_sensor.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/zha/core/const.py
|
||||
homeassistant/components/zha/core/device.py
|
||||
@ -667,11 +666,8 @@ omit =
|
||||
homeassistant/components/zha/core/listeners.py
|
||||
homeassistant/components/zha/device_entity.py
|
||||
homeassistant/components/zha/entity.py
|
||||
homeassistant/components/zha/event.py
|
||||
homeassistant/components/zha/fan.py
|
||||
homeassistant/components/zha/light.py
|
||||
homeassistant/components/zha/sensor.py
|
||||
homeassistant/components/zha/switch.py
|
||||
homeassistant/components/zigbee/*
|
||||
homeassistant/components/zoneminder/*
|
||||
homeassistant/components/zwave/util.py
|
||||
|
@ -4,6 +4,7 @@ Support for Zigbee Home Automation devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import types
|
||||
@ -17,14 +18,15 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
# Loading the config flow file will register the flow
|
||||
from . import config_flow # noqa # pylint: disable=unused-import
|
||||
from . import api
|
||||
from .core.gateway import ZHAGateway
|
||||
from .const import (
|
||||
from .core import ZHAGateway
|
||||
from .core.const import (
|
||||
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
|
||||
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
|
||||
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
|
||||
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
|
||||
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS,
|
||||
ENABLE_QUIRKS)
|
||||
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS)
|
||||
from .core.gateway import establish_device_mappings
|
||||
from .core.listeners import populate_listener_registry
|
||||
|
||||
REQUIREMENTS = [
|
||||
'bellows==0.7.0',
|
||||
@ -87,9 +89,16 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
Will automatically load components to support devices found on the network.
|
||||
"""
|
||||
establish_device_mappings()
|
||||
populate_listener_registry()
|
||||
|
||||
for component in COMPONENTS:
|
||||
hass.data[DATA_ZHA][component] = (
|
||||
hass.data[DATA_ZHA].get(component, {})
|
||||
)
|
||||
|
||||
hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {})
|
||||
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
|
||||
|
||||
config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
|
||||
|
||||
if config.get(ENABLE_QUIRKS, True):
|
||||
@ -137,14 +146,32 @@ async def async_setup_entry(hass, config_entry):
|
||||
ClusterPersistingListener
|
||||
)
|
||||
|
||||
application_controller = ControllerApplication(radio, database)
|
||||
zha_gateway = ZHAGateway(hass, config)
|
||||
hass.bus.async_listen_once(
|
||||
ha_const.EVENT_HOMEASSISTANT_START, zha_gateway.accept_zigbee_messages)
|
||||
|
||||
# Patch handle_message until zigpy can provide an event here
|
||||
def handle_message(sender, is_reply, profile, cluster,
|
||||
src_ep, dst_ep, tsn, command_id, args):
|
||||
"""Handle message from a device."""
|
||||
if sender.last_seen is None and not sender.initializing:
|
||||
if sender.ieee in zha_gateway.devices:
|
||||
device = zha_gateway.devices[sender.ieee]
|
||||
device.update_available(True)
|
||||
return sender.handle_message(
|
||||
is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args)
|
||||
|
||||
application_controller = ControllerApplication(radio, database)
|
||||
application_controller.handle_message = handle_message
|
||||
application_controller.add_listener(zha_gateway)
|
||||
await application_controller.startup(auto_form=True)
|
||||
|
||||
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee)
|
||||
|
||||
init_tasks = []
|
||||
for device in application_controller.devices.values():
|
||||
hass.async_create_task(
|
||||
zha_gateway.async_device_initialized(device, False))
|
||||
init_tasks.append(zha_gateway.async_device_initialized(device, False))
|
||||
await asyncio.gather(*init_tasks)
|
||||
|
||||
device_registry = await \
|
||||
hass.helpers.device_registry.async_get_registry()
|
||||
@ -157,8 +184,6 @@ async def async_setup_entry(hass, config_entry):
|
||||
model=radio_description,
|
||||
)
|
||||
|
||||
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee)
|
||||
|
||||
for component in COMPONENTS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(
|
||||
|
@ -11,8 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .device_entity import ZhaDeviceEntity
|
||||
from .const import (
|
||||
from .core.const import (
|
||||
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
|
||||
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
|
||||
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER)
|
||||
@ -118,115 +117,7 @@ SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_cluster_attributes(hass, connection, msg):
|
||||
"""Return a list of cluster attributes."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
cluster_attributes = []
|
||||
if entity is not None:
|
||||
res = await entity.get_cluster_attributes(cluster_id, cluster_type)
|
||||
if res is not None:
|
||||
for attr_id in res:
|
||||
cluster_attributes.append(
|
||||
{
|
||||
ID: attr_id,
|
||||
NAME: res[attr_id][0]
|
||||
}
|
||||
)
|
||||
_LOGGER.debug("Requested attributes for: %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(RESPONSE, cluster_attributes)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
cluster_attributes
|
||||
))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_cluster_commands(hass, connection, msg):
|
||||
"""Return a list of cluster commands."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
cluster_commands = []
|
||||
if entity is not None:
|
||||
res = await entity.get_cluster_commands(cluster_id, cluster_type)
|
||||
if res is not None:
|
||||
for cmd_id in res[CLIENT_COMMANDS]:
|
||||
cluster_commands.append(
|
||||
{
|
||||
TYPE: CLIENT,
|
||||
ID: cmd_id,
|
||||
NAME: res[CLIENT_COMMANDS][cmd_id][0]
|
||||
}
|
||||
)
|
||||
for cmd_id in res[SERVER_COMMANDS]:
|
||||
cluster_commands.append(
|
||||
{
|
||||
TYPE: SERVER,
|
||||
ID: cmd_id,
|
||||
NAME: res[SERVER_COMMANDS][cmd_id][0]
|
||||
}
|
||||
)
|
||||
_LOGGER.debug("Requested commands for: %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(RESPONSE, cluster_commands)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
cluster_commands
|
||||
))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
|
||||
"""Read zigbee attribute for cluster on zha entity."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
attribute = msg[ATTR_ATTRIBUTE]
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
clusters = await entity.get_clusters()
|
||||
cluster = clusters[cluster_type][cluster_id]
|
||||
manufacturer = msg.get(ATTR_MANUFACTURER) or None
|
||||
success = failure = None
|
||||
if entity is not None:
|
||||
success, failure = await cluster.read_attributes(
|
||||
[attribute],
|
||||
allow_cache=False,
|
||||
only_cache=False,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
_LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(ATTR_ATTRIBUTE, attribute),
|
||||
"{}: [{}]".format(ATTR_MANUFACTURER, manufacturer),
|
||||
"{}: [{}]".format(RESPONSE, str(success.get(attribute))),
|
||||
"{}: [{}]".format('failure', failure)
|
||||
)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
str(success.get(attribute))
|
||||
))
|
||||
|
||||
|
||||
def async_load_api(hass, application_controller, listener):
|
||||
def async_load_api(hass, application_controller, zha_gateway):
|
||||
"""Set up the web socket API."""
|
||||
async def permit(service):
|
||||
"""Allow devices to join this network."""
|
||||
@ -256,11 +147,12 @@ def async_load_api(hass, application_controller, listener):
|
||||
attribute = service.data.get(ATTR_ATTRIBUTE)
|
||||
value = service.data.get(ATTR_VALUE)
|
||||
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||
response = None
|
||||
if entity is not None:
|
||||
response = await entity.write_zigbe_attribute(
|
||||
if entity_ref is not None:
|
||||
response = await entity_ref.zha_device.write_zigbee_attribute(
|
||||
list(entity_ref.cluster_listeners.values())[
|
||||
0].cluster.endpoint.endpoint_id,
|
||||
cluster_id,
|
||||
attribute,
|
||||
value,
|
||||
@ -292,11 +184,13 @@ def async_load_api(hass, application_controller, listener):
|
||||
command_type = service.data.get(ATTR_COMMAND_TYPE)
|
||||
args = service.data.get(ATTR_ARGS)
|
||||
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||
zha_device = entity_ref.zha_device
|
||||
response = None
|
||||
if entity is not None:
|
||||
response = await entity.issue_cluster_command(
|
||||
if entity_ref is not None:
|
||||
response = await zha_device.issue_cluster_command(
|
||||
list(entity_ref.cluster_listeners.values())[
|
||||
0].cluster.endpoint.endpoint_id,
|
||||
cluster_id,
|
||||
command,
|
||||
command_type,
|
||||
@ -325,11 +219,9 @@ def async_load_api(hass, application_controller, listener):
|
||||
async def websocket_reconfigure_node(hass, connection, msg):
|
||||
"""Reconfigure a ZHA nodes entities by its ieee address."""
|
||||
ieee = msg[ATTR_IEEE]
|
||||
entities = listener.get_entities_for_ieee(ieee)
|
||||
device = zha_gateway.get_device(ieee)
|
||||
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
|
||||
for entity in entities:
|
||||
if hasattr(entity, 'async_configure'):
|
||||
hass.async_create_task(entity.async_configure())
|
||||
hass.async_create_task(device.async_configure())
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_RECONFIGURE_NODE, websocket_reconfigure_node,
|
||||
@ -340,15 +232,15 @@ def async_load_api(hass, application_controller, listener):
|
||||
async def websocket_entities_by_ieee(hass, connection, msg):
|
||||
"""Return a dict of all zha entities grouped by ieee."""
|
||||
entities_by_ieee = {}
|
||||
for ieee, entities in listener.device_registry.items():
|
||||
for ieee, entities in zha_gateway.device_registry.items():
|
||||
ieee_string = str(ieee)
|
||||
entities_by_ieee[ieee_string] = []
|
||||
for entity in entities:
|
||||
if not isinstance(entity, ZhaDeviceEntity):
|
||||
entities_by_ieee[ieee_string].append({
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
DEVICE_INFO: entity.device_info
|
||||
})
|
||||
entities_by_ieee[ieee_string].append({
|
||||
ATTR_ENTITY_ID: entity.reference_id,
|
||||
DEVICE_INFO: entity.device_info
|
||||
})
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
entities_by_ieee
|
||||
@ -363,24 +255,25 @@ def async_load_api(hass, application_controller, listener):
|
||||
async def websocket_entity_clusters(hass, connection, msg):
|
||||
"""Return a list of entity clusters."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
entities = listener.get_entities_for_ieee(msg[ATTR_IEEE])
|
||||
entity = next(
|
||||
ent for ent in entities if ent.entity_id == entity_id)
|
||||
entity_clusters = await entity.get_clusters()
|
||||
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||
clusters = []
|
||||
|
||||
for cluster_id, cluster in entity_clusters[IN].items():
|
||||
clusters.append({
|
||||
TYPE: IN,
|
||||
ID: cluster_id,
|
||||
NAME: cluster.__class__.__name__
|
||||
})
|
||||
for cluster_id, cluster in entity_clusters[OUT].items():
|
||||
clusters.append({
|
||||
TYPE: OUT,
|
||||
ID: cluster_id,
|
||||
NAME: cluster.__class__.__name__
|
||||
})
|
||||
if entity_ref is not None:
|
||||
for listener in entity_ref.cluster_listeners.values():
|
||||
cluster = listener.cluster
|
||||
in_clusters = cluster.endpoint.in_clusters.values()
|
||||
out_clusters = cluster.endpoint.out_clusters.values()
|
||||
if cluster in in_clusters:
|
||||
clusters.append({
|
||||
TYPE: IN,
|
||||
ID: cluster.cluster_id,
|
||||
NAME: cluster.__class__.__name__
|
||||
})
|
||||
elif cluster in out_clusters:
|
||||
clusters.append({
|
||||
TYPE: OUT,
|
||||
ID: cluster.cluster_id,
|
||||
NAME: cluster.__class__.__name__
|
||||
})
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
@ -392,16 +285,141 @@ def async_load_api(hass, application_controller, listener):
|
||||
SCHEMA_WS_CLUSTERS
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_cluster_attributes(hass, connection, msg):
|
||||
"""Return a list of cluster attributes."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
ieee = msg[ATTR_IEEE]
|
||||
cluster_attributes = []
|
||||
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||
device = zha_gateway.get_device(ieee)
|
||||
attributes = None
|
||||
if entity_ref is not None:
|
||||
attributes = await device.get_cluster_attributes(
|
||||
list(entity_ref.cluster_listeners.values())[
|
||||
0].cluster.endpoint.endpoint_id,
|
||||
cluster_id,
|
||||
cluster_type)
|
||||
if attributes is not None:
|
||||
for attr_id in attributes:
|
||||
cluster_attributes.append(
|
||||
{
|
||||
ID: attr_id,
|
||||
NAME: attributes[attr_id][0]
|
||||
}
|
||||
)
|
||||
_LOGGER.debug("Requested attributes for: %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(RESPONSE, cluster_attributes)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
cluster_attributes
|
||||
))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes,
|
||||
SCHEMA_WS_CLUSTER_ATTRIBUTES
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_cluster_commands(hass, connection, msg):
|
||||
"""Return a list of cluster commands."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
ieee = msg[ATTR_IEEE]
|
||||
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||
device = zha_gateway.get_device(ieee)
|
||||
cluster_commands = []
|
||||
commands = None
|
||||
if entity_ref is not None:
|
||||
commands = await device.get_cluster_commands(
|
||||
list(entity_ref.cluster_listeners.values())[
|
||||
0].cluster.endpoint.endpoint_id,
|
||||
cluster_id,
|
||||
cluster_type)
|
||||
|
||||
if commands is not None:
|
||||
for cmd_id in commands[CLIENT_COMMANDS]:
|
||||
cluster_commands.append(
|
||||
{
|
||||
TYPE: CLIENT,
|
||||
ID: cmd_id,
|
||||
NAME: commands[CLIENT_COMMANDS][cmd_id][0]
|
||||
}
|
||||
)
|
||||
for cmd_id in commands[SERVER_COMMANDS]:
|
||||
cluster_commands.append(
|
||||
{
|
||||
TYPE: SERVER,
|
||||
ID: cmd_id,
|
||||
NAME: commands[SERVER_COMMANDS][cmd_id][0]
|
||||
}
|
||||
)
|
||||
_LOGGER.debug("Requested commands for: %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(RESPONSE, cluster_commands)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
cluster_commands
|
||||
))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands,
|
||||
SCHEMA_WS_CLUSTER_COMMANDS
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
|
||||
"""Read zigbee attribute for cluster on zha entity."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
attribute = msg[ATTR_ATTRIBUTE]
|
||||
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||
manufacturer = msg.get(ATTR_MANUFACTURER) or None
|
||||
success = failure = None
|
||||
clusters = []
|
||||
if cluster_type == IN:
|
||||
clusters = \
|
||||
list(entity_ref.cluster_listeners.values())[
|
||||
0].cluster.endpoint.in_clusters
|
||||
else:
|
||||
clusters = \
|
||||
list(entity_ref.cluster_listeners.values())[
|
||||
0].cluster.endpoint.out_clusters
|
||||
cluster = clusters[cluster_id]
|
||||
if entity_ref is not None:
|
||||
success, failure = await cluster.read_attributes(
|
||||
[attribute],
|
||||
allow_cache=False,
|
||||
only_cache=False,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
_LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(ATTR_ATTRIBUTE, attribute),
|
||||
"{}: [{}]".format(ATTR_MANUFACTURER, manufacturer),
|
||||
"{}: [{}]".format(RESPONSE, str(success.get(attribute))),
|
||||
"{}: [{}]".format('failure', failure)
|
||||
)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
str(success.get(attribute))
|
||||
))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes,
|
||||
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE
|
||||
|
@ -7,16 +7,13 @@ at https://home-assistant.io/components/binary_sensor.zha/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .core import helpers
|
||||
from .core.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF,
|
||||
LISTENER_LEVEL, LISTENER_ZONE, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL,
|
||||
SIGNAL_SET_LEVEL, LISTENER_ATTRIBUTE, UNKNOWN, OPENING, ZONE, OCCUPANCY,
|
||||
ATTR_LEVEL, SENSOR_TYPE)
|
||||
from .entity import ZhaEntity
|
||||
from .core.listeners import (
|
||||
OnOffListener, LevelListener
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -31,7 +28,20 @@ CLASS_MAPPING = {
|
||||
0x002b: 'gas',
|
||||
0x002d: 'vibration',
|
||||
}
|
||||
DEVICE_CLASS_OCCUPANCY = 'occupancy'
|
||||
|
||||
|
||||
async def get_ias_device_class(listener):
|
||||
"""Get the HA device class from the listener."""
|
||||
zone_type = await listener.get_attribute_value('zone_type')
|
||||
return CLASS_MAPPING.get(zone_type)
|
||||
|
||||
|
||||
DEVICE_CLASS_REGISTRY = {
|
||||
UNKNOWN: None,
|
||||
OPENING: OPENING,
|
||||
ZONE: get_ias_device_class,
|
||||
OCCUPANCY: OCCUPANCY,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
@ -60,249 +70,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
discovery_infos):
|
||||
"""Set up the ZHA binary sensors."""
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
from zigpy.zcl.clusters.measurement import OccupancySensing
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
|
||||
entities = []
|
||||
for discovery_info in discovery_infos:
|
||||
if IasZone.cluster_id in discovery_info['in_clusters']:
|
||||
entities.append(await _async_setup_iaszone(discovery_info))
|
||||
elif OccupancySensing.cluster_id in discovery_info['in_clusters']:
|
||||
entities.append(
|
||||
BinarySensor(DEVICE_CLASS_OCCUPANCY, **discovery_info))
|
||||
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
||||
entities.append(Remote(**discovery_info))
|
||||
entities.append(BinarySensor(**discovery_info))
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
async def _async_setup_iaszone(discovery_info):
|
||||
device_class = None
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
|
||||
|
||||
try:
|
||||
zone_type = await cluster['zone_type']
|
||||
device_class = CLASS_MAPPING.get(zone_type, None)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# If we fail to read from the device, use a non-specific class
|
||||
pass
|
||||
|
||||
return IasZoneSensor(device_class, **discovery_info)
|
||||
|
||||
|
||||
class IasZoneSensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
|
||||
"""The IasZoneSensor Binary Sensor."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
def __init__(self, device_class, **kwargs):
|
||||
"""Initialize the ZHA binary sensor."""
|
||||
super().__init__(**kwargs)
|
||||
self._device_class = device_class
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
if command_id == 0:
|
||||
self._state = args[0] & 3
|
||||
_LOGGER.debug("Updated alarm state: %s", self._state)
|
||||
self.async_schedule_update_ha_state()
|
||||
elif command_id == 1:
|
||||
_LOGGER.debug("Enroll requested")
|
||||
res = self._ias_zone_cluster.enroll_response(0, 0)
|
||||
self.hass.async_add_job(res)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
old_state = await self.async_get_last_state()
|
||||
if self._state is not None or old_state is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
|
||||
if old_state.state == STATE_ON:
|
||||
self._state = 3
|
||||
else:
|
||||
self._state = 0
|
||||
|
||||
async def async_configure(self):
|
||||
"""Configure IAS device."""
|
||||
await self._ias_zone_cluster.bind()
|
||||
ieee = self._ias_zone_cluster.endpoint.device.application.ieee
|
||||
await self._ias_zone_cluster.write_attributes({'cie_addr': ieee})
|
||||
_LOGGER.debug("%s: finished configuration", self.entity_id)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
from zigpy.types.basic import uint16_t
|
||||
|
||||
result = await helpers.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
|
||||
|
||||
class Remote(RestoreEntity, ZhaEntity, BinarySensorDevice):
|
||||
"""ZHA switch/remote controller/button."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize Switch."""
|
||||
super().__init__(**kwargs)
|
||||
self._level = 0
|
||||
from zigpy.zcl.clusters import general
|
||||
self._out_listeners = {
|
||||
general.OnOff.cluster_id: OnOffListener(
|
||||
self,
|
||||
self._out_clusters[general.OnOff.cluster_id]
|
||||
)
|
||||
}
|
||||
|
||||
out_clusters = kwargs.get('out_clusters')
|
||||
self._zcl_reporting = {}
|
||||
|
||||
if general.LevelControl.cluster_id in out_clusters:
|
||||
self._out_listeners.update({
|
||||
general.LevelControl.cluster_id: LevelListener(
|
||||
self,
|
||||
out_clusters[general.LevelControl.cluster_id]
|
||||
)
|
||||
})
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
self._device_state_attributes.update({
|
||||
'level': self._state and self._level or 0
|
||||
})
|
||||
return self._device_state_attributes
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self):
|
||||
"""Return ZCL attribute reporting configuration."""
|
||||
return self._zcl_reporting
|
||||
|
||||
def move_level(self, change):
|
||||
"""Increment the level, setting state if appropriate."""
|
||||
if not self._state and change > 0:
|
||||
self._level = 0
|
||||
self._level = min(255, max(0, self._level + change))
|
||||
self._state = bool(self._level)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_level(self, level):
|
||||
"""Set the level, setting state if appropriate."""
|
||||
self._level = level
|
||||
self._state = bool(self._level)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set the state."""
|
||||
self._state = state
|
||||
if self._level == 0:
|
||||
self._level = 255
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_configure(self):
|
||||
"""Bind clusters."""
|
||||
from zigpy.zcl.clusters import general
|
||||
await helpers.bind_cluster(
|
||||
self.entity_id,
|
||||
self._out_clusters[general.OnOff.cluster_id]
|
||||
)
|
||||
if general.LevelControl.cluster_id in self._out_clusters:
|
||||
await helpers.bind_cluster(
|
||||
self.entity_id,
|
||||
self._out_clusters[general.LevelControl.cluster_id]
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
old_state = await self.async_get_last_state()
|
||||
if self._state is not None or old_state is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
|
||||
if 'level' in old_state.attributes:
|
||||
self._level = old_state.attributes['level']
|
||||
self._state = old_state.state == STATE_ON
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
result = await helpers.safe_read(
|
||||
self._endpoint.out_clusters[OnOff.cluster_id],
|
||||
['on_off'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized)
|
||||
)
|
||||
self._state = result.get('on_off', self._state)
|
||||
|
||||
|
||||
class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
|
||||
"""ZHA switch."""
|
||||
class BinarySensor(ZhaEntity, BinarySensorDevice):
|
||||
"""ZHA BinarySensor."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_device_class = None
|
||||
value_attribute = 0
|
||||
|
||||
def __init__(self, device_class, **kwargs):
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the ZHA binary sensor."""
|
||||
super().__init__(**kwargs)
|
||||
self._device_class = device_class
|
||||
self._cluster = list(kwargs['in_clusters'].values())[0]
|
||||
self._device_state_attributes = {}
|
||||
self._zone_listener = self.cluster_listeners.get(LISTENER_ZONE)
|
||||
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
|
||||
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL)
|
||||
self._attr_listener = self.cluster_listeners.get(LISTENER_ATTRIBUTE)
|
||||
self._zha_sensor_type = kwargs[SENSOR_TYPE]
|
||||
self._level = None
|
||||
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle attribute update from device."""
|
||||
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
|
||||
if attribute == self.value_attribute:
|
||||
self._state = bool(value)
|
||||
self.async_schedule_update_ha_state()
|
||||
async def _determine_device_class(self):
|
||||
"""Determine the device class for this binary sensor."""
|
||||
device_class_supplier = DEVICE_CLASS_REGISTRY.get(
|
||||
self._zha_sensor_type)
|
||||
if callable(device_class_supplier):
|
||||
listener = self.cluster_listeners.get(self._zha_sensor_type)
|
||||
if listener is None:
|
||||
return None
|
||||
return await device_class_supplier(listener)
|
||||
return device_class_supplier
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
self._device_class = await self._determine_device_class()
|
||||
await super().async_added_to_hass()
|
||||
old_state = await self.async_get_last_state()
|
||||
if self._state is not None or old_state is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
|
||||
self._state = old_state.state == STATE_ON
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Zigbee cluster for this entity."""
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self):
|
||||
"""ZHA reporting configuration."""
|
||||
return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}}
|
||||
if self._level_listener:
|
||||
await self.async_accept_signal(
|
||||
self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
|
||||
await self.async_accept_signal(
|
||||
self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level)
|
||||
if self._on_off_listener:
|
||||
await self.async_accept_signal(
|
||||
self._on_off_listener, SIGNAL_ATTR_UPDATED,
|
||||
self.async_set_state)
|
||||
if self._zone_listener:
|
||||
await self.async_accept_signal(
|
||||
self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||
if self._attr_listener:
|
||||
await self.async_accept_signal(
|
||||
self._attr_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@ -315,3 +136,32 @@ class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
|
||||
def device_class(self) -> str:
|
||||
"""Return device class from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
|
||||
def async_set_state(self, state):
|
||||
"""Set the state."""
|
||||
self._state = bool(state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def move_level(self, change):
|
||||
"""Increment the level, setting state if appropriate."""
|
||||
level = self._level or 0
|
||||
if not self._state and change > 0:
|
||||
level = 0
|
||||
self._level = min(254, max(0, level + change))
|
||||
self._state = bool(self._level)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_level(self, level):
|
||||
"""Set the level, setting state if appropriate."""
|
||||
self._level = level
|
||||
self._state = bool(level)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
if self._level_listener is not None:
|
||||
self._device_state_attributes.update({
|
||||
ATTR_LEVEL: self._state and self._level or 0
|
||||
})
|
||||
return self._device_state_attributes
|
||||
|
@ -4,3 +4,10 @@ Core module for Zigbee Home Automation.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
from .device import ZHADevice
|
||||
from .gateway import ZHAGateway
|
||||
from .listeners import (
|
||||
ClusterListener, AttributeListener, OnOffListener, LevelListener,
|
||||
IASZoneListener, ActivePowerListener, BatteryListener, EventRelayListener)
|
||||
|
@ -55,10 +55,38 @@ IEEE = 'ieee'
|
||||
MODEL = 'model'
|
||||
NAME = 'name'
|
||||
|
||||
SENSOR_TYPE = 'sensor_type'
|
||||
HUMIDITY = 'humidity'
|
||||
TEMPERATURE = 'temperature'
|
||||
ILLUMINANCE = 'illuminance'
|
||||
PRESSURE = 'pressure'
|
||||
METERING = 'metering'
|
||||
ELECTRICAL_MEASUREMENT = 'electrical_measurement'
|
||||
POWER_CONFIGURATION = 'power_configuration'
|
||||
GENERIC = 'generic'
|
||||
UNKNOWN = 'unknown'
|
||||
OPENING = 'opening'
|
||||
ZONE = 'zone'
|
||||
OCCUPANCY = 'occupancy'
|
||||
|
||||
ATTR_LEVEL = 'level'
|
||||
|
||||
LISTENER_ON_OFF = 'on_off'
|
||||
LISTENER_ATTRIBUTE = 'attribute'
|
||||
LISTENER_COLOR = 'color'
|
||||
LISTENER_FAN = 'fan'
|
||||
LISTENER_LEVEL = ATTR_LEVEL
|
||||
LISTENER_ZONE = 'zone'
|
||||
LISTENER_ACTIVE_POWER = 'active_power'
|
||||
LISTENER_BATTERY = 'battery'
|
||||
LISTENER_EVENT_RELAY = 'event_relay'
|
||||
|
||||
SIGNAL_ATTR_UPDATED = 'attribute_updated'
|
||||
SIGNAL_MOVE_LEVEL = "move_level"
|
||||
SIGNAL_SET_LEVEL = "set_level"
|
||||
SIGNAL_STATE_ATTR = "update_state_attribute"
|
||||
SIGNAL_AVAILABLE = 'available'
|
||||
SIGNAL_REMOVE = 'remove'
|
||||
|
||||
|
||||
class RadioType(enum.Enum):
|
||||
@ -78,9 +106,10 @@ DISCOVERY_KEY = 'zha_discovery_info'
|
||||
DEVICE_CLASS = {}
|
||||
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
||||
CLUSTER_REPORT_CONFIGS = {}
|
||||
CUSTOM_CLUSTER_MAPPINGS = {}
|
||||
COMPONENT_CLUSTERS = {}
|
||||
EVENTABLE_CLUSTERS = []
|
||||
EVENT_RELAY_CLUSTERS = []
|
||||
|
||||
REPORT_CONFIG_MAX_INT = 900
|
||||
REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800
|
||||
|
@ -14,7 +14,7 @@ from .const import (
|
||||
ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT,
|
||||
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
|
||||
ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS,
|
||||
ATTR_ENDPOINT_ID, IEEE, MODEL, NAME
|
||||
ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN
|
||||
)
|
||||
from .listeners import EventRelayListener
|
||||
|
||||
@ -30,11 +30,14 @@ class ZHADevice:
|
||||
self._zigpy_device = zigpy_device
|
||||
# Get first non ZDO endpoint id to use to get manufacturer and model
|
||||
endpoint_ids = zigpy_device.endpoints.keys()
|
||||
ept_id = next(ept_id for ept_id in endpoint_ids if ept_id != 0)
|
||||
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer
|
||||
self._model = zigpy_device.endpoints[ept_id].model
|
||||
self._manufacturer = UNKNOWN
|
||||
self._model = UNKNOWN
|
||||
ept_id = next((ept_id for ept_id in endpoint_ids if ept_id != 0), None)
|
||||
if ept_id is not None:
|
||||
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer
|
||||
self._model = zigpy_device.endpoints[ept_id].model
|
||||
self._zha_gateway = zha_gateway
|
||||
self._cluster_listeners = {}
|
||||
self.cluster_listeners = {}
|
||||
self._relay_listeners = []
|
||||
self._all_listeners = []
|
||||
self._name = "{} {}".format(
|
||||
@ -101,21 +104,11 @@ class ZHADevice:
|
||||
"""Return the gateway for this device."""
|
||||
return self._zha_gateway
|
||||
|
||||
@property
|
||||
def cluster_listeners(self):
|
||||
"""Return cluster listeners for device."""
|
||||
return self._cluster_listeners.values()
|
||||
|
||||
@property
|
||||
def all_listeners(self):
|
||||
"""Return cluster listeners and relay listeners for device."""
|
||||
return self._all_listeners
|
||||
|
||||
@property
|
||||
def cluster_listener_keys(self):
|
||||
"""Return cluster listeners for device."""
|
||||
return self._cluster_listeners.keys()
|
||||
|
||||
@property
|
||||
def available_signal(self):
|
||||
"""Signal to use to subscribe to device availability changes."""
|
||||
@ -157,17 +150,13 @@ class ZHADevice:
|
||||
"""Add cluster listener to device."""
|
||||
# only keep 1 power listener
|
||||
if cluster_listener.name is LISTENER_BATTERY and \
|
||||
LISTENER_BATTERY in self._cluster_listeners:
|
||||
LISTENER_BATTERY in self.cluster_listeners:
|
||||
return
|
||||
self._all_listeners.append(cluster_listener)
|
||||
if isinstance(cluster_listener, EventRelayListener):
|
||||
self._relay_listeners.append(cluster_listener)
|
||||
else:
|
||||
self._cluster_listeners[cluster_listener.name] = cluster_listener
|
||||
|
||||
def get_cluster_listener(self, name):
|
||||
"""Get cluster listener by name."""
|
||||
return self._cluster_listeners.get(name, None)
|
||||
self.cluster_listeners[cluster_listener.name] = cluster_listener
|
||||
|
||||
async def async_configure(self):
|
||||
"""Configure the device."""
|
||||
|
@ -5,7 +5,9 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
from homeassistant import const as ha_const
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
@ -13,15 +15,27 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from . import const as zha_const
|
||||
from .const import (
|
||||
COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN,
|
||||
ZHA_DISCOVERY_NEW, EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, DEVICE_CLASS,
|
||||
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
||||
CUSTOM_CLUSTER_MAPPINGS, COMPONENT_CLUSTERS)
|
||||
ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
|
||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
|
||||
TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
|
||||
POWER_CONFIGURATION, GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS,
|
||||
LISTENER_BATTERY, UNKNOWN, OPENING, ZONE, OCCUPANCY,
|
||||
CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_ASAP,
|
||||
REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_OP, SIGNAL_REMOVE)
|
||||
from .device import ZHADevice
|
||||
from ..device_entity import ZhaDeviceEntity
|
||||
from ..event import ZhaEvent, ZhaRelayEvent
|
||||
from .listeners import (
|
||||
LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener)
|
||||
from .helpers import convert_ieee
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {}
|
||||
BINARY_SENSOR_TYPES = {}
|
||||
EntityReference = collections.namedtuple(
|
||||
'EntityReference', 'reference_id zha_device cluster_listeners device_info')
|
||||
|
||||
|
||||
class ZHAGateway:
|
||||
"""Gateway that handles events that happen on the ZHA Zigbee network."""
|
||||
@ -31,16 +45,9 @@ class ZHAGateway:
|
||||
self._hass = hass
|
||||
self._config = config
|
||||
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
self._devices = {}
|
||||
self._device_registry = collections.defaultdict(list)
|
||||
self._events = {}
|
||||
establish_device_mappings()
|
||||
|
||||
for component in COMPONENTS:
|
||||
hass.data[DATA_ZHA][component] = (
|
||||
hass.data[DATA_ZHA].get(component, {})
|
||||
)
|
||||
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
|
||||
hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events
|
||||
|
||||
def device_joined(self, device):
|
||||
"""Handle device joined.
|
||||
@ -67,197 +74,310 @@ class ZHAGateway:
|
||||
|
||||
def device_removed(self, device):
|
||||
"""Handle device being removed from the network."""
|
||||
for device_entity in self._device_registry[device.ieee]:
|
||||
self._hass.async_create_task(device_entity.async_remove())
|
||||
if device.ieee in self._events:
|
||||
self._events.pop(device.ieee)
|
||||
|
||||
def get_device_entity(self, ieee_str):
|
||||
"""Return ZHADeviceEntity for given ieee."""
|
||||
ieee = convert_ieee(ieee_str)
|
||||
if ieee in self._device_registry:
|
||||
entities = self._device_registry[ieee]
|
||||
entity = next(
|
||||
ent for ent in entities if isinstance(ent, ZhaDeviceEntity))
|
||||
return entity
|
||||
return None
|
||||
|
||||
def get_entities_for_ieee(self, ieee_str):
|
||||
"""Return list of entities for given ieee."""
|
||||
ieee = convert_ieee(ieee_str)
|
||||
if ieee in self._device_registry:
|
||||
return self._device_registry[ieee]
|
||||
return []
|
||||
|
||||
@property
|
||||
def device_registry(self) -> str:
|
||||
"""Return devices."""
|
||||
return self._device_registry
|
||||
|
||||
async def async_device_initialized(self, device, join):
|
||||
"""Handle device joined and basic information discovered (async)."""
|
||||
import zigpy.profiles
|
||||
|
||||
device_manufacturer = device_model = None
|
||||
|
||||
for endpoint_id, endpoint in device.endpoints.items():
|
||||
if endpoint_id == 0: # ZDO
|
||||
continue
|
||||
|
||||
if endpoint.manufacturer is not None:
|
||||
device_manufacturer = endpoint.manufacturer
|
||||
if endpoint.model is not None:
|
||||
device_model = endpoint.model
|
||||
|
||||
component = None
|
||||
profile_clusters = ([], [])
|
||||
device_key = "{}-{}".format(device.ieee, endpoint_id)
|
||||
node_config = {}
|
||||
if CONF_DEVICE_CONFIG in self._config:
|
||||
node_config = self._config[CONF_DEVICE_CONFIG].get(
|
||||
device_key, {}
|
||||
)
|
||||
|
||||
if endpoint.profile_id in zigpy.profiles.PROFILES:
|
||||
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
|
||||
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
|
||||
{}).get(endpoint.device_type,
|
||||
None):
|
||||
profile_clusters = profile.CLUSTERS[endpoint.device_type]
|
||||
profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id]
|
||||
component = profile_info[endpoint.device_type]
|
||||
|
||||
if ha_const.CONF_TYPE in node_config:
|
||||
component = node_config[ha_const.CONF_TYPE]
|
||||
profile_clusters = zha_const.COMPONENT_CLUSTERS[component]
|
||||
|
||||
if component:
|
||||
in_clusters = [endpoint.in_clusters[c]
|
||||
for c in profile_clusters[0]
|
||||
if c in endpoint.in_clusters]
|
||||
out_clusters = [endpoint.out_clusters[c]
|
||||
for c in profile_clusters[1]
|
||||
if c in endpoint.out_clusters]
|
||||
discovery_info = {
|
||||
'application_listener': self,
|
||||
'endpoint': endpoint,
|
||||
'in_clusters': {c.cluster_id: c for c in in_clusters},
|
||||
'out_clusters': {c.cluster_id: c for c in out_clusters},
|
||||
'manufacturer': endpoint.manufacturer,
|
||||
'model': endpoint.model,
|
||||
'new_join': join,
|
||||
'unique_id': device_key,
|
||||
}
|
||||
|
||||
if join:
|
||||
async_dispatcher_send(
|
||||
self._hass,
|
||||
ZHA_DISCOVERY_NEW.format(component),
|
||||
discovery_info
|
||||
)
|
||||
else:
|
||||
self._hass.data[DATA_ZHA][component][device_key] = (
|
||||
discovery_info
|
||||
)
|
||||
|
||||
for cluster in endpoint.in_clusters.values():
|
||||
await self._attempt_single_cluster_device(
|
||||
endpoint,
|
||||
cluster,
|
||||
profile_clusters[0],
|
||||
device_key,
|
||||
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
|
||||
'in_clusters',
|
||||
join,
|
||||
)
|
||||
|
||||
for cluster in endpoint.out_clusters.values():
|
||||
await self._attempt_single_cluster_device(
|
||||
endpoint,
|
||||
cluster,
|
||||
profile_clusters[1],
|
||||
device_key,
|
||||
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
||||
'out_clusters',
|
||||
join,
|
||||
)
|
||||
|
||||
endpoint_entity = ZhaDeviceEntity(
|
||||
device,
|
||||
device_manufacturer,
|
||||
device_model,
|
||||
self,
|
||||
)
|
||||
await self._component.async_add_entities([endpoint_entity])
|
||||
|
||||
def register_entity(self, ieee, entity_obj):
|
||||
"""Record the creation of a hass entity associated with ieee."""
|
||||
self._device_registry[ieee].append(entity_obj)
|
||||
|
||||
async def _attempt_single_cluster_device(self, endpoint, cluster,
|
||||
profile_clusters, device_key,
|
||||
device_classes, discovery_attr,
|
||||
is_new_join):
|
||||
"""Try to set up an entity from a "bare" cluster."""
|
||||
if cluster.cluster_id in EVENTABLE_CLUSTERS:
|
||||
if cluster.endpoint.device.ieee not in self._events:
|
||||
self._events.update({cluster.endpoint.device.ieee: []})
|
||||
from zigpy.zcl.clusters.general import OnOff, LevelControl
|
||||
if discovery_attr == 'out_clusters' and \
|
||||
(cluster.cluster_id == OnOff.cluster_id or
|
||||
cluster.cluster_id == LevelControl.cluster_id):
|
||||
self._events[cluster.endpoint.device.ieee].append(
|
||||
ZhaRelayEvent(self._hass, cluster)
|
||||
)
|
||||
else:
|
||||
self._events[cluster.endpoint.device.ieee].append(ZhaEvent(
|
||||
self._hass,
|
||||
cluster
|
||||
))
|
||||
|
||||
if cluster.cluster_id in profile_clusters:
|
||||
return
|
||||
|
||||
component = sub_component = None
|
||||
for cluster_type, candidate_component in device_classes.items():
|
||||
if isinstance(cluster, cluster_type):
|
||||
component = candidate_component
|
||||
break
|
||||
|
||||
for signature, comp in zha_const.CUSTOM_CLUSTER_MAPPINGS.items():
|
||||
if (isinstance(endpoint.device, signature[0]) and
|
||||
cluster.cluster_id == signature[1]):
|
||||
component = comp[0]
|
||||
sub_component = comp[1]
|
||||
break
|
||||
|
||||
if component is None:
|
||||
return
|
||||
|
||||
cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
|
||||
discovery_info = {
|
||||
'application_listener': self,
|
||||
'endpoint': endpoint,
|
||||
'in_clusters': {},
|
||||
'out_clusters': {},
|
||||
'manufacturer': endpoint.manufacturer,
|
||||
'model': endpoint.model,
|
||||
'new_join': is_new_join,
|
||||
'unique_id': cluster_key,
|
||||
'entity_suffix': '_{}'.format(cluster.cluster_id),
|
||||
}
|
||||
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
|
||||
if sub_component:
|
||||
discovery_info.update({'sub_component': sub_component})
|
||||
|
||||
if is_new_join:
|
||||
device = self._devices.pop(device.ieee, None)
|
||||
self._device_registry.pop(device.ieee, None)
|
||||
if device is not None:
|
||||
self._hass.async_create_task(device.async_unsub_dispatcher())
|
||||
async_dispatcher_send(
|
||||
self._hass,
|
||||
ZHA_DISCOVERY_NEW.format(component),
|
||||
discovery_info
|
||||
"{}_{}".format(SIGNAL_REMOVE, str(device.ieee))
|
||||
)
|
||||
else:
|
||||
self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info
|
||||
|
||||
def get_device(self, ieee_str):
|
||||
"""Return ZHADevice for given ieee."""
|
||||
ieee = convert_ieee(ieee_str)
|
||||
return self._devices.get(ieee)
|
||||
|
||||
def get_entity_reference(self, entity_id):
|
||||
"""Return entity reference for given entity_id if found."""
|
||||
for entity_reference in itertools.chain.from_iterable(
|
||||
self.device_registry.values()):
|
||||
if entity_id == entity_reference.reference_id:
|
||||
return entity_reference
|
||||
|
||||
@property
|
||||
def devices(self):
|
||||
"""Return devices."""
|
||||
return self._devices
|
||||
|
||||
@property
|
||||
def device_registry(self):
|
||||
"""Return entities by ieee."""
|
||||
return self._device_registry
|
||||
|
||||
def register_entity_reference(
|
||||
self, ieee, reference_id, zha_device, cluster_listeners,
|
||||
device_info):
|
||||
"""Record the creation of a hass entity associated with ieee."""
|
||||
self._device_registry[ieee].append(
|
||||
EntityReference(
|
||||
reference_id=reference_id,
|
||||
zha_device=zha_device,
|
||||
cluster_listeners=cluster_listeners,
|
||||
device_info=device_info
|
||||
)
|
||||
)
|
||||
|
||||
async def _get_or_create_device(self, zigpy_device):
|
||||
"""Get or create a ZHA device."""
|
||||
zha_device = self._devices.get(zigpy_device.ieee)
|
||||
if zha_device is None:
|
||||
zha_device = ZHADevice(self._hass, zigpy_device, self)
|
||||
self._devices[zigpy_device.ieee] = zha_device
|
||||
return zha_device
|
||||
|
||||
async def accept_zigbee_messages(self, _service_or_event):
|
||||
"""Allow devices to accept zigbee messages."""
|
||||
accept_messages_calls = []
|
||||
for device in self.devices.values():
|
||||
accept_messages_calls.append(device.async_accept_messages())
|
||||
await asyncio.gather(*accept_messages_calls)
|
||||
|
||||
async def async_device_initialized(self, device, is_new_join):
|
||||
"""Handle device joined and basic information discovered (async)."""
|
||||
zha_device = await self._get_or_create_device(device)
|
||||
discovery_infos = []
|
||||
endpoint_tasks = []
|
||||
for endpoint_id, endpoint in device.endpoints.items():
|
||||
endpoint_tasks.append(self._async_process_endpoint(
|
||||
endpoint_id, endpoint, discovery_infos, device, zha_device,
|
||||
is_new_join
|
||||
))
|
||||
await asyncio.gather(*endpoint_tasks)
|
||||
|
||||
await zha_device.async_initialize(not is_new_join)
|
||||
|
||||
discovery_tasks = []
|
||||
for discovery_info in discovery_infos:
|
||||
discovery_tasks.append(_dispatch_discovery_info(
|
||||
self._hass,
|
||||
is_new_join,
|
||||
discovery_info
|
||||
))
|
||||
await asyncio.gather(*discovery_tasks)
|
||||
|
||||
device_entity = _create_device_entity(zha_device)
|
||||
await self._component.async_add_entities([device_entity])
|
||||
|
||||
async def _async_process_endpoint(
|
||||
self, endpoint_id, endpoint, discovery_infos, device, zha_device,
|
||||
is_new_join):
|
||||
"""Process an endpoint on a zigpy device."""
|
||||
import zigpy.profiles
|
||||
|
||||
if endpoint_id == 0: # ZDO
|
||||
await _create_cluster_listener(
|
||||
endpoint,
|
||||
zha_device,
|
||||
is_new_join,
|
||||
listener_class=ZDOListener
|
||||
)
|
||||
return
|
||||
|
||||
component = None
|
||||
profile_clusters = ([], [])
|
||||
device_key = "{}-{}".format(device.ieee, endpoint_id)
|
||||
node_config = {}
|
||||
if CONF_DEVICE_CONFIG in self._config:
|
||||
node_config = self._config[CONF_DEVICE_CONFIG].get(
|
||||
device_key, {}
|
||||
)
|
||||
|
||||
if endpoint.profile_id in zigpy.profiles.PROFILES:
|
||||
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
|
||||
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
|
||||
{}).get(endpoint.device_type,
|
||||
None):
|
||||
profile_clusters = profile.CLUSTERS[endpoint.device_type]
|
||||
profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id]
|
||||
component = profile_info[endpoint.device_type]
|
||||
|
||||
if ha_const.CONF_TYPE in node_config:
|
||||
component = node_config[ha_const.CONF_TYPE]
|
||||
profile_clusters = zha_const.COMPONENT_CLUSTERS[component]
|
||||
|
||||
if component and component in COMPONENTS:
|
||||
profile_match = await _handle_profile_match(
|
||||
self._hass, endpoint, profile_clusters, zha_device,
|
||||
component, device_key, is_new_join)
|
||||
discovery_infos.append(profile_match)
|
||||
|
||||
discovery_infos.extend(await _handle_single_cluster_matches(
|
||||
self._hass,
|
||||
endpoint,
|
||||
zha_device,
|
||||
profile_clusters,
|
||||
device_key,
|
||||
is_new_join
|
||||
))
|
||||
|
||||
|
||||
async def _create_cluster_listener(cluster, zha_device, is_new_join,
|
||||
listeners=None, listener_class=None):
|
||||
"""Create a cluster listener and attach it to a device."""
|
||||
if listener_class is None:
|
||||
listener_class = LISTENER_REGISTRY.get(cluster.cluster_id,
|
||||
AttributeListener)
|
||||
listener = listener_class(cluster, zha_device)
|
||||
if is_new_join:
|
||||
await listener.async_configure()
|
||||
zha_device.add_cluster_listener(listener)
|
||||
if listeners is not None:
|
||||
listeners.append(listener)
|
||||
|
||||
|
||||
async def _dispatch_discovery_info(hass, is_new_join, discovery_info):
|
||||
"""Dispatch or store discovery information."""
|
||||
component = discovery_info['component']
|
||||
if is_new_join:
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
ZHA_DISCOVERY_NEW.format(component),
|
||||
discovery_info
|
||||
)
|
||||
else:
|
||||
hass.data[DATA_ZHA][component][discovery_info['unique_id']] = \
|
||||
discovery_info
|
||||
|
||||
|
||||
async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device,
|
||||
component, device_key, is_new_join):
|
||||
"""Dispatch a profile match to the appropriate HA component."""
|
||||
in_clusters = [endpoint.in_clusters[c]
|
||||
for c in profile_clusters[0]
|
||||
if c in endpoint.in_clusters]
|
||||
out_clusters = [endpoint.out_clusters[c]
|
||||
for c in profile_clusters[1]
|
||||
if c in endpoint.out_clusters]
|
||||
|
||||
listeners = []
|
||||
cluster_tasks = []
|
||||
|
||||
for cluster in in_clusters:
|
||||
cluster_tasks.append(_create_cluster_listener(
|
||||
cluster, zha_device, is_new_join, listeners=listeners))
|
||||
|
||||
for cluster in out_clusters:
|
||||
cluster_tasks.append(_create_cluster_listener(
|
||||
cluster, zha_device, is_new_join, listeners=listeners))
|
||||
|
||||
await asyncio.gather(*cluster_tasks)
|
||||
|
||||
discovery_info = {
|
||||
'unique_id': device_key,
|
||||
'zha_device': zha_device,
|
||||
'listeners': listeners,
|
||||
'component': component
|
||||
}
|
||||
|
||||
if component == 'binary_sensor':
|
||||
discovery_info.update({SENSOR_TYPE: UNKNOWN})
|
||||
cluster_ids = []
|
||||
cluster_ids.extend(profile_clusters[0])
|
||||
cluster_ids.extend(profile_clusters[1])
|
||||
for cluster_id in cluster_ids:
|
||||
if cluster_id in BINARY_SENSOR_TYPES:
|
||||
discovery_info.update({
|
||||
SENSOR_TYPE: BINARY_SENSOR_TYPES.get(
|
||||
cluster_id, UNKNOWN)
|
||||
})
|
||||
break
|
||||
|
||||
return discovery_info
|
||||
|
||||
|
||||
async def _handle_single_cluster_matches(hass, endpoint, zha_device,
|
||||
profile_clusters, device_key,
|
||||
is_new_join):
|
||||
"""Dispatch single cluster matches to HA components."""
|
||||
cluster_matches = []
|
||||
cluster_match_tasks = []
|
||||
event_listener_tasks = []
|
||||
for cluster in endpoint.in_clusters.values():
|
||||
if cluster.cluster_id not in profile_clusters[0]:
|
||||
cluster_match_tasks.append(_handle_single_cluster_match(
|
||||
hass,
|
||||
zha_device,
|
||||
cluster,
|
||||
device_key,
|
||||
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
|
||||
is_new_join,
|
||||
))
|
||||
|
||||
for cluster in endpoint.out_clusters.values():
|
||||
if cluster.cluster_id not in profile_clusters[1]:
|
||||
cluster_match_tasks.append(_handle_single_cluster_match(
|
||||
hass,
|
||||
zha_device,
|
||||
cluster,
|
||||
device_key,
|
||||
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
||||
is_new_join,
|
||||
))
|
||||
|
||||
if cluster.cluster_id in EVENT_RELAY_CLUSTERS:
|
||||
event_listener_tasks.append(_create_cluster_listener(
|
||||
cluster,
|
||||
zha_device,
|
||||
is_new_join,
|
||||
listener_class=EventRelayListener
|
||||
))
|
||||
await asyncio.gather(*event_listener_tasks)
|
||||
cluster_match_results = await asyncio.gather(*cluster_match_tasks)
|
||||
for cluster_match in cluster_match_results:
|
||||
if cluster_match is not None:
|
||||
cluster_matches.append(cluster_match)
|
||||
return cluster_matches
|
||||
|
||||
|
||||
async def _handle_single_cluster_match(hass, zha_device, cluster, device_key,
|
||||
device_classes, is_new_join):
|
||||
"""Dispatch a single cluster match to a HA component."""
|
||||
component = None # sub_component = None
|
||||
for cluster_type, candidate_component in device_classes.items():
|
||||
if isinstance(cluster, cluster_type):
|
||||
component = candidate_component
|
||||
break
|
||||
|
||||
if component is None or component not in COMPONENTS:
|
||||
return
|
||||
listeners = []
|
||||
await _create_cluster_listener(cluster, zha_device, is_new_join,
|
||||
listeners=listeners)
|
||||
# don't actually create entities for PowerConfiguration
|
||||
# find a better way to do this without abusing single cluster reg
|
||||
from zigpy.zcl.clusters.general import PowerConfiguration
|
||||
if cluster.cluster_id == PowerConfiguration.cluster_id:
|
||||
return
|
||||
|
||||
cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
|
||||
discovery_info = {
|
||||
'unique_id': cluster_key,
|
||||
'zha_device': zha_device,
|
||||
'listeners': listeners,
|
||||
'entity_suffix': '_{}'.format(cluster.cluster_id),
|
||||
'component': component
|
||||
}
|
||||
|
||||
if component == 'sensor':
|
||||
discovery_info.update({
|
||||
SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC)
|
||||
})
|
||||
if component == 'binary_sensor':
|
||||
discovery_info.update({
|
||||
SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)
|
||||
})
|
||||
|
||||
return discovery_info
|
||||
|
||||
|
||||
def _create_device_entity(zha_device):
|
||||
"""Create ZHADeviceEntity."""
|
||||
device_entity_listeners = []
|
||||
if LISTENER_BATTERY in zha_device.cluster_listeners:
|
||||
listener = zha_device.cluster_listeners.get(LISTENER_BATTERY)
|
||||
device_entity_listeners.append(listener)
|
||||
return ZhaDeviceEntity(zha_device, device_entity_listeners)
|
||||
|
||||
|
||||
def establish_device_mappings():
|
||||
@ -266,19 +386,16 @@ def establish_device_mappings():
|
||||
These cannot be module level, as importing bellows must be done in a
|
||||
in a function.
|
||||
"""
|
||||
from zigpy import zcl, quirks
|
||||
from zigpy import zcl
|
||||
from zigpy.profiles import PROFILES, zha, zll
|
||||
from ..sensor import RelativeHumiditySensor
|
||||
|
||||
if zha.PROFILE_ID not in DEVICE_CLASS:
|
||||
DEVICE_CLASS[zha.PROFILE_ID] = {}
|
||||
if zll.PROFILE_ID not in DEVICE_CLASS:
|
||||
DEVICE_CLASS[zll.PROFILE_ID] = {}
|
||||
|
||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id)
|
||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
|
||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id)
|
||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
||||
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
|
||||
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
||||
|
||||
DEVICE_CLASS[zha.PROFILE_ID].update({
|
||||
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
|
||||
@ -293,6 +410,7 @@ def establish_device_mappings():
|
||||
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
|
||||
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
|
||||
})
|
||||
|
||||
DEVICE_CLASS[zll.PROFILE_ID].update({
|
||||
zll.DeviceType.ON_OFF_LIGHT: 'light',
|
||||
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
|
||||
@ -321,14 +439,97 @@ def establish_device_mappings():
|
||||
zcl.clusters.measurement.OccupancySensing: 'binary_sensor',
|
||||
zcl.clusters.hvac.Fan: 'fan',
|
||||
})
|
||||
|
||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
|
||||
zcl.clusters.general.OnOff: 'binary_sensor',
|
||||
})
|
||||
|
||||
# A map of device/cluster to component/sub-component
|
||||
CUSTOM_CLUSTER_MAPPINGS.update({
|
||||
(quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581):
|
||||
('sensor', RelativeHumiditySensor)
|
||||
SENSOR_TYPES.update({
|
||||
zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY,
|
||||
zcl.clusters.measurement.TemperatureMeasurement.cluster_id:
|
||||
TEMPERATURE,
|
||||
zcl.clusters.measurement.PressureMeasurement.cluster_id: PRESSURE,
|
||||
zcl.clusters.measurement.IlluminanceMeasurement.cluster_id:
|
||||
ILLUMINANCE,
|
||||
zcl.clusters.smartenergy.Metering.cluster_id: METERING,
|
||||
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
|
||||
ELECTRICAL_MEASUREMENT,
|
||||
zcl.clusters.general.PowerConfiguration.cluster_id:
|
||||
POWER_CONFIGURATION,
|
||||
})
|
||||
|
||||
BINARY_SENSOR_TYPES.update({
|
||||
zcl.clusters.measurement.OccupancySensing.cluster_id: OCCUPANCY,
|
||||
zcl.clusters.security.IasZone.cluster_id: ZONE,
|
||||
zcl.clusters.general.OnOff.cluster_id: OPENING
|
||||
})
|
||||
|
||||
CLUSTER_REPORT_CONFIGS.update({
|
||||
zcl.clusters.general.OnOff.cluster_id: [{
|
||||
'attr': 'on_off',
|
||||
'config': REPORT_CONFIG_IMMEDIATE
|
||||
}],
|
||||
zcl.clusters.general.LevelControl.cluster_id: [{
|
||||
'attr': 'current_level',
|
||||
'config': REPORT_CONFIG_ASAP
|
||||
}],
|
||||
zcl.clusters.lighting.Color.cluster_id: [{
|
||||
'attr': 'current_x',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}, {
|
||||
'attr': 'current_y',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}, {
|
||||
'attr': 'color_temperature',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}],
|
||||
zcl.clusters.measurement.RelativeHumidity.cluster_id: [{
|
||||
'attr': 'measured_value',
|
||||
'config': (
|
||||
REPORT_CONFIG_MIN_INT,
|
||||
REPORT_CONFIG_MAX_INT,
|
||||
50
|
||||
)
|
||||
}],
|
||||
zcl.clusters.measurement.TemperatureMeasurement.cluster_id: [{
|
||||
'attr': 'measured_value',
|
||||
'config': (
|
||||
REPORT_CONFIG_MIN_INT,
|
||||
REPORT_CONFIG_MAX_INT,
|
||||
50
|
||||
)
|
||||
}],
|
||||
zcl.clusters.measurement.PressureMeasurement.cluster_id: [{
|
||||
'attr': 'measured_value',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}],
|
||||
zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: [{
|
||||
'attr': 'measured_value',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}],
|
||||
zcl.clusters.smartenergy.Metering.cluster_id: [{
|
||||
'attr': 'instantaneous_demand',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}],
|
||||
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: [{
|
||||
'attr': 'active_power',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}],
|
||||
zcl.clusters.general.PowerConfiguration.cluster_id: [{
|
||||
'attr': 'battery_voltage',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}, {
|
||||
'attr': 'battery_percentage_remaining',
|
||||
'config': REPORT_CONFIG_DEFAULT
|
||||
}],
|
||||
zcl.clusters.measurement.OccupancySensing.cluster_id: [{
|
||||
'attr': 'occupancy',
|
||||
'config': REPORT_CONFIG_IMMEDIATE
|
||||
}],
|
||||
zcl.clusters.hvac.Fan.cluster_id: [{
|
||||
'attr': 'fan_mode',
|
||||
'config': REPORT_CONFIG_OP
|
||||
}],
|
||||
})
|
||||
|
||||
# A map of hass components to all Zigbee clusters it could use
|
||||
|
@ -5,20 +5,48 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
import logging
|
||||
from random import uniform
|
||||
|
||||
from homeassistant.core import callback
|
||||
from .const import SIGNAL_ATTR_UPDATED
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from .helpers import (
|
||||
bind_configure_reporting, construct_unique_id,
|
||||
safe_read, get_attr_id_by_name)
|
||||
from .const import (
|
||||
CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED,
|
||||
SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, ATTR_LEVEL
|
||||
)
|
||||
|
||||
LISTENER_REGISTRY = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_and_log_command(entity_id, cluster, tsn, command_id, args):
|
||||
def populate_listener_registry():
|
||||
"""Populate the listener registry."""
|
||||
from zigpy import zcl
|
||||
LISTENER_REGISTRY.update({
|
||||
zcl.clusters.general.OnOff.cluster_id: OnOffListener,
|
||||
zcl.clusters.general.LevelControl.cluster_id: LevelListener,
|
||||
zcl.clusters.lighting.Color.cluster_id: ColorListener,
|
||||
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
|
||||
ActivePowerListener,
|
||||
zcl.clusters.general.PowerConfiguration.cluster_id: BatteryListener,
|
||||
zcl.clusters.security.IasZone.cluster_id: IASZoneListener,
|
||||
zcl.clusters.hvac.Fan.cluster_id: FanListener,
|
||||
})
|
||||
|
||||
|
||||
def parse_and_log_command(unique_id, cluster, tsn, command_id, args):
|
||||
"""Parse and log a zigbee cluster command."""
|
||||
cmd = cluster.server_commands.get(command_id, [command_id])[0]
|
||||
_LOGGER.debug(
|
||||
"%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'",
|
||||
entity_id,
|
||||
unique_id,
|
||||
cmd,
|
||||
args,
|
||||
cluster.cluster_id,
|
||||
@ -27,40 +55,214 @@ def parse_and_log_command(entity_id, cluster, tsn, command_id, args):
|
||||
return cmd
|
||||
|
||||
|
||||
def decorate_command(listener, 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)
|
||||
_LOGGER.debug("%s: executed command: %s %s %s %s",
|
||||
listener.unique_id,
|
||||
command.__name__,
|
||||
"{}: {}".format("with args", args),
|
||||
"{}: {}".format("with kwargs", kwds),
|
||||
"{}: {}".format("and result", result))
|
||||
return result[1] is Status.SUCCESS
|
||||
except DeliveryError:
|
||||
_LOGGER.debug("%s: command failed: %s", listener.unique_id,
|
||||
command.__name__)
|
||||
return False
|
||||
return wrapper
|
||||
|
||||
|
||||
class ListenerStatus(Enum):
|
||||
"""Status of a listener."""
|
||||
|
||||
CREATED = 1
|
||||
CONFIGURED = 2
|
||||
INITIALIZED = 3
|
||||
LISTENING = 4
|
||||
|
||||
|
||||
class ClusterListener:
|
||||
"""Listener for a Zigbee cluster."""
|
||||
|
||||
def __init__(self, entity, cluster):
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize ClusterListener."""
|
||||
self._entity = entity
|
||||
self._cluster = cluster
|
||||
self._zha_device = device
|
||||
self._unique_id = construct_unique_id(cluster)
|
||||
self._report_config = CLUSTER_REPORT_CONFIGS.get(
|
||||
self._cluster.cluster_id,
|
||||
[{'attr': 0, 'config': REPORT_CONFIG_DEFAULT}]
|
||||
)
|
||||
self._status = ListenerStatus.CREATED
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id for this listener."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Return the zigpy cluster for this listener."""
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the device this listener is linked to."""
|
||||
return self._zha_device
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the status of the listener."""
|
||||
return self._status
|
||||
|
||||
def set_report_config(self, report_config):
|
||||
"""Set the reporting configuration."""
|
||||
self._report_config = report_config
|
||||
|
||||
async def async_configure(self):
|
||||
"""Set cluster binding and attribute reporting."""
|
||||
manufacturer = None
|
||||
manufacturer_code = self._zha_device.manufacturer_code
|
||||
if self.cluster.cluster_id >= 0xfc00 and manufacturer_code:
|
||||
manufacturer = manufacturer_code
|
||||
|
||||
skip_bind = False # bind cluster only for the 1st configured attr
|
||||
for report_config in self._report_config:
|
||||
attr = report_config.get('attr')
|
||||
min_report_interval, max_report_interval, change = \
|
||||
report_config.get('config')
|
||||
await bind_configure_reporting(
|
||||
self._unique_id, self.cluster, attr,
|
||||
min_report=min_report_interval,
|
||||
max_report=max_report_interval,
|
||||
reportable_change=change,
|
||||
skip_bind=skip_bind,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
skip_bind = True
|
||||
await asyncio.sleep(uniform(0.1, 0.5))
|
||||
_LOGGER.debug(
|
||||
"%s: finished listener configuration",
|
||||
self._unique_id
|
||||
)
|
||||
self._status = ListenerStatus.CONFIGURED
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
self._status = ListenerStatus.INITIALIZED
|
||||
|
||||
async def accept_messages(self):
|
||||
"""Attach to the cluster so we can receive messages."""
|
||||
self._cluster.add_listener(self)
|
||||
self._status = ListenerStatus.LISTENING
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def zdo_command(self, *args, **kwargs):
|
||||
"""Handle ZDO commands on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def zha_send_event(self, cluster, command, args):
|
||||
"""Relay entity events to hass."""
|
||||
pass # don't let entities fire events
|
||||
"""Relay events to hass."""
|
||||
self._zha_device.hass.bus.async_fire(
|
||||
'zha_event',
|
||||
{
|
||||
'unique_id': self._unique_id,
|
||||
'command': command,
|
||||
'args': args
|
||||
}
|
||||
)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state from cluster."""
|
||||
pass
|
||||
|
||||
async def get_attribute_value(self, attribute, from_cache=True):
|
||||
"""Get the value for an attribute."""
|
||||
result = await safe_read(
|
||||
self._cluster,
|
||||
[attribute],
|
||||
allow_cache=from_cache,
|
||||
only_cache=from_cache
|
||||
)
|
||||
return result.get(attribute)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Get attribute or a decorated cluster command."""
|
||||
if hasattr(self._cluster, name) and callable(
|
||||
getattr(self._cluster, name)):
|
||||
command = getattr(self._cluster, name)
|
||||
command.__name__ = name
|
||||
return decorate_command(
|
||||
self,
|
||||
command
|
||||
)
|
||||
return self.__getattribute__(name)
|
||||
|
||||
|
||||
class AttributeListener(ClusterListener):
|
||||
"""Listener for the attribute reports cluster."""
|
||||
|
||||
name = 'attribute'
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize AttributeListener."""
|
||||
super().__init__(cluster, device)
|
||||
attr = self._report_config[0].get('attr')
|
||||
if isinstance(attr, str):
|
||||
self._value_attribute = get_attr_id_by_name(self.cluster, attr)
|
||||
else:
|
||||
self._value_attribute = attr
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
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 listener."""
|
||||
await self.get_attribute_value(
|
||||
self._report_config[0].get('attr'), from_cache=from_cache)
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
|
||||
class OnOffListener(ClusterListener):
|
||||
"""Listener for the OnOff Zigbee cluster."""
|
||||
|
||||
name = 'on_off'
|
||||
|
||||
ON_OFF = 0
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize ClusterListener."""
|
||||
super().__init__(cluster, device)
|
||||
self._state = None
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
cmd = parse_and_log_command(
|
||||
self._entity.entity_id,
|
||||
self.unique_id,
|
||||
self._cluster,
|
||||
tsn,
|
||||
command_id,
|
||||
@ -68,27 +270,42 @@ class OnOffListener(ClusterListener):
|
||||
)
|
||||
|
||||
if cmd in ('off', 'off_with_effect'):
|
||||
self._entity.set_state(False)
|
||||
self.attribute_updated(self.ON_OFF, False)
|
||||
elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'):
|
||||
self._entity.set_state(True)
|
||||
self.attribute_updated(self.ON_OFF, True)
|
||||
elif cmd == 'toggle':
|
||||
self._entity.set_state(not self._entity.is_on)
|
||||
self.attribute_updated(self.ON_OFF, not bool(self._state))
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
if attrid == self.ON_OFF:
|
||||
self._entity.set_state(bool(value))
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
|
||||
value
|
||||
)
|
||||
self._state = bool(value)
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
self._state = bool(
|
||||
await self.get_attribute_value(self.ON_OFF, from_cache=from_cache))
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
|
||||
class LevelListener(ClusterListener):
|
||||
"""Listener for the LevelControl Zigbee cluster."""
|
||||
|
||||
name = ATTR_LEVEL
|
||||
|
||||
CURRENT_LEVEL = 0
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
cmd = parse_and_log_command(
|
||||
self._entity.entity_id,
|
||||
self.unique_id,
|
||||
self._cluster,
|
||||
tsn,
|
||||
command_id,
|
||||
@ -96,21 +313,190 @@ class LevelListener(ClusterListener):
|
||||
)
|
||||
|
||||
if cmd in ('move_to_level', 'move_to_level_with_on_off'):
|
||||
self._entity.set_level(args[0])
|
||||
self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0])
|
||||
elif cmd in ('move', 'move_with_on_off'):
|
||||
# We should dim slowly -- for now, just step once
|
||||
rate = args[1]
|
||||
if args[0] == 0xff:
|
||||
rate = 10 # Should read default move rate
|
||||
self._entity.move_level(-rate if args[0] else rate)
|
||||
self.dispatch_level_change(
|
||||
SIGNAL_MOVE_LEVEL, -rate if args[0] else rate)
|
||||
elif cmd in ('step', 'step_with_on_off'):
|
||||
# Step (technically may change on/off)
|
||||
self._entity.move_level(-args[1] if args[0] else args[1])
|
||||
self.dispatch_level_change(
|
||||
SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1])
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
_LOGGER.debug("%s: received attribute: %s update with value: %i",
|
||||
self.unique_id, attrid, value)
|
||||
if attrid == self.CURRENT_LEVEL:
|
||||
self._entity.set_level(value)
|
||||
self.dispatch_level_change(SIGNAL_SET_LEVEL, value)
|
||||
|
||||
def dispatch_level_change(self, command, level):
|
||||
"""Dispatch level change."""
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, command),
|
||||
level
|
||||
)
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
await self.get_attribute_value(
|
||||
self.CURRENT_LEVEL, from_cache=from_cache)
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
|
||||
class IASZoneListener(ClusterListener):
|
||||
"""Listener for the IASZone Zigbee cluster."""
|
||||
|
||||
name = 'zone'
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize IASZoneListener."""
|
||||
super().__init__(cluster, device)
|
||||
self._cluster.add_listener(self)
|
||||
self._status = ListenerStatus.LISTENING
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
if command_id == 0:
|
||||
state = args[0] & 3
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
|
||||
state
|
||||
)
|
||||
_LOGGER.debug("Updated alarm state: %s", state)
|
||||
elif command_id == 1:
|
||||
_LOGGER.debug("Enroll requested")
|
||||
res = self._cluster.enroll_response(0, 0)
|
||||
self._zha_device.hass.async_create_task(res)
|
||||
|
||||
async def async_configure(self):
|
||||
"""Configure IAS device."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
_LOGGER.debug("%s: started IASZoneListener configuration",
|
||||
self._unique_id)
|
||||
try:
|
||||
res = await self._cluster.bind()
|
||||
_LOGGER.debug(
|
||||
"%s: bound '%s' cluster: %s",
|
||||
self.unique_id, self._cluster.ep_attribute, res[0]
|
||||
)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.debug(
|
||||
"%s: Failed to bind '%s' cluster: %s",
|
||||
self.unique_id, self._cluster.ep_attribute, str(ex)
|
||||
)
|
||||
|
||||
ieee = self._cluster.endpoint.device.application.ieee
|
||||
|
||||
try:
|
||||
res = await self._cluster.write_attributes({'cie_addr': ieee})
|
||||
_LOGGER.debug(
|
||||
"%s: wrote cie_addr: %s to '%s' cluster: %s",
|
||||
self.unique_id, str(ieee), self._cluster.ep_attribute,
|
||||
res[0]
|
||||
)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.debug(
|
||||
"%s: Failed to write cie_addr: %s to '%s' cluster: %s",
|
||||
self.unique_id, str(ieee), self._cluster.ep_attribute, str(ex)
|
||||
)
|
||||
_LOGGER.debug("%s: finished IASZoneListener configuration",
|
||||
self._unique_id)
|
||||
|
||||
await self.get_attribute_value('zone_type', from_cache=False)
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
if attrid == 2:
|
||||
value = value & 3
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
|
||||
value
|
||||
)
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
await self.get_attribute_value('zone_status', from_cache=from_cache)
|
||||
await self.get_attribute_value('zone_state', from_cache=from_cache)
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
async def accept_messages(self):
|
||||
"""Attach to the cluster so we can receive messages."""
|
||||
self._status = ListenerStatus.LISTENING
|
||||
|
||||
|
||||
class ActivePowerListener(AttributeListener):
|
||||
"""Listener that polls active power level."""
|
||||
|
||||
name = 'active_power'
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
_LOGGER.debug("%s async_update", self.unique_id)
|
||||
|
||||
# This is a polling listener. Don't allow cache.
|
||||
result = await self.get_attribute_value(
|
||||
'active_power', from_cache=False)
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
|
||||
result
|
||||
)
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
await self.get_attribute_value(
|
||||
'active_power', from_cache=from_cache)
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
|
||||
class BatteryListener(ClusterListener):
|
||||
"""Listener that polls active power level."""
|
||||
|
||||
name = 'battery'
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
attr = self._report_config[1].get('attr')
|
||||
if isinstance(attr, str):
|
||||
attr_id = get_attr_id_by_name(self.cluster, attr)
|
||||
else:
|
||||
attr_id = attr
|
||||
if attrid == attr_id:
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR),
|
||||
'battery_level',
|
||||
value
|
||||
)
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
await self.async_read_state(from_cache)
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
await self.async_read_state(True)
|
||||
|
||||
async def async_read_state(self, from_cache):
|
||||
"""Read data from the cluster."""
|
||||
await self.get_attribute_value(
|
||||
'battery_size', from_cache=from_cache)
|
||||
await self.get_attribute_value(
|
||||
'battery_percentage_remaining', from_cache=from_cache)
|
||||
await self.get_attribute_value(
|
||||
'active_power', from_cache=from_cache)
|
||||
|
||||
|
||||
class EventRelayListener(ClusterListener):
|
||||
@ -143,3 +529,137 @@ class EventRelayListener(ClusterListener):
|
||||
self._cluster.server_commands.get(command_id)[0],
|
||||
args
|
||||
)
|
||||
|
||||
|
||||
class ColorListener(ClusterListener):
|
||||
"""Color listener."""
|
||||
|
||||
name = 'color'
|
||||
|
||||
CAPABILITIES_COLOR_XY = 0x08
|
||||
CAPABILITIES_COLOR_TEMP = 0x10
|
||||
UNSUPPORTED_ATTRIBUTE = 0x86
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize ClusterListener."""
|
||||
super().__init__(cluster, device)
|
||||
self._color_capabilities = None
|
||||
|
||||
def get_color_capabilities(self):
|
||||
"""Return the color capabilities."""
|
||||
return self._color_capabilities
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
capabilities = await self.get_attribute_value(
|
||||
'color_capabilities', from_cache=from_cache)
|
||||
|
||||
if capabilities is None:
|
||||
# ZCL Version 4 devices don't support the color_capabilities
|
||||
# attribute. In this version XY support is mandatory, but we
|
||||
# need to probe to determine if the device supports color
|
||||
# temperature.
|
||||
capabilities = self.CAPABILITIES_COLOR_XY
|
||||
result = await self.get_attribute_value(
|
||||
'color_temperature', from_cache=from_cache)
|
||||
|
||||
if result is not self.UNSUPPORTED_ATTRIBUTE:
|
||||
capabilities |= self.CAPABILITIES_COLOR_TEMP
|
||||
self._color_capabilities = capabilities
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
|
||||
class FanListener(ClusterListener):
|
||||
"""Fan listener."""
|
||||
|
||||
name = 'fan'
|
||||
|
||||
_value_attribute = 0
|
||||
|
||||
async def async_set_speed(self, value) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
try:
|
||||
await self.cluster.write_attributes({'fan_mode': value})
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Could not set speed: %s", self.unique_id, ex)
|
||||
return
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await self.get_attribute_value('fan_mode', from_cache=True)
|
||||
|
||||
async_dispatcher_send(
|
||||
self._zha_device.hass,
|
||||
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
|
||||
result
|
||||
)
|
||||
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute update from fan 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 listener."""
|
||||
await self.get_attribute_value(
|
||||
self._value_attribute, from_cache=from_cache)
|
||||
await super().async_initialize(from_cache)
|
||||
|
||||
|
||||
class ZDOListener:
|
||||
"""Listener for ZDO events."""
|
||||
|
||||
name = 'zdo'
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize ClusterListener."""
|
||||
self._cluster = cluster
|
||||
self._zha_device = device
|
||||
self._status = ListenerStatus.CREATED
|
||||
self._unique_id = "{}_ZDO".format(device.name)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id for this listener."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Return the aigpy cluster for this listener."""
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the status of the listener."""
|
||||
return self._status
|
||||
|
||||
@callback
|
||||
def device_announce(self, zigpy_device):
|
||||
"""Device announce handler."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def permit_duration(self, duration):
|
||||
"""Permit handler."""
|
||||
pass
|
||||
|
||||
async def accept_messages(self):
|
||||
"""Attach to the cluster so we can receive messages."""
|
||||
self._cluster.add_listener(self)
|
||||
self._status = ListenerStatus.LISTENING
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize listener."""
|
||||
self._status = ListenerStatus.INITIALIZED
|
||||
|
||||
async def async_configure(self):
|
||||
"""Configure listener."""
|
||||
self._status = ListenerStatus.CONFIGURED
|
||||
|
@ -5,78 +5,134 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.util import slugify
|
||||
from .entity import ZhaEntity
|
||||
from .const import LISTENER_BATTERY, SIGNAL_STATE_ATTR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BATTERY_SIZES = {
|
||||
0: 'No battery',
|
||||
1: 'Built in',
|
||||
2: 'Other',
|
||||
3: 'AA',
|
||||
4: 'AAA',
|
||||
5: 'C',
|
||||
6: 'D',
|
||||
7: 'CR2',
|
||||
8: 'CR123A',
|
||||
9: 'CR2450',
|
||||
10: 'CR2032',
|
||||
11: 'CR1632',
|
||||
255: 'Unknown'
|
||||
}
|
||||
|
||||
|
||||
class ZhaDeviceEntity(entity.Entity):
|
||||
class ZhaDeviceEntity(ZhaEntity):
|
||||
"""A base class for ZHA devices."""
|
||||
|
||||
def __init__(self, device, manufacturer, model, application_listener,
|
||||
keepalive_interval=7200, **kwargs):
|
||||
def __init__(self, zha_device, listeners, keepalive_interval=7200,
|
||||
**kwargs):
|
||||
"""Init ZHA endpoint entity."""
|
||||
self._device_state_attributes = {
|
||||
'nwk': '0x{0:04x}'.format(device.nwk),
|
||||
'ieee': str(device.ieee),
|
||||
'lqi': device.lqi,
|
||||
'rssi': device.rssi,
|
||||
}
|
||||
|
||||
ieee = device.ieee
|
||||
ieee = zha_device.ieee
|
||||
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
||||
if manufacturer is not None and model is not None:
|
||||
self._unique_id = "{}_{}_{}".format(
|
||||
slugify(manufacturer),
|
||||
slugify(model),
|
||||
unique_id = None
|
||||
if zha_device.manufacturer is not None and \
|
||||
zha_device.model is not None:
|
||||
unique_id = "{}_{}_{}".format(
|
||||
slugify(zha_device.manufacturer),
|
||||
slugify(zha_device.model),
|
||||
ieeetail,
|
||||
)
|
||||
self._device_state_attributes['friendly_name'] = "{} {}".format(
|
||||
manufacturer,
|
||||
model,
|
||||
)
|
||||
else:
|
||||
self._unique_id = str(ieeetail)
|
||||
unique_id = str(ieeetail)
|
||||
|
||||
kwargs['component'] = 'zha'
|
||||
super().__init__(unique_id, zha_device, listeners, skip_entity_id=True,
|
||||
**kwargs)
|
||||
|
||||
self._device = device
|
||||
self._state = 'offline'
|
||||
self._keepalive_interval = keepalive_interval
|
||||
|
||||
application_listener.register_entity(ieee, self)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
self._device_state_attributes.update({
|
||||
'nwk': '0x{0:04x}'.format(zha_device.nwk),
|
||||
'ieee': str(zha_device.ieee),
|
||||
'lqi': zha_device.lqi,
|
||||
'rssi': zha_device.rssi,
|
||||
})
|
||||
self._should_poll = True
|
||||
self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if device is available."""
|
||||
return self._zha_device.available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
update_time = None
|
||||
if self._device.last_seen is not None and self._state == 'offline':
|
||||
time_struct = time.localtime(self._device.last_seen)
|
||||
device = self._zha_device
|
||||
if device.last_seen is not None and not self.available:
|
||||
time_struct = time.localtime(device.last_seen)
|
||||
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
|
||||
self._device_state_attributes['last_seen'] = update_time
|
||||
if ('last_seen' in self._device_state_attributes and
|
||||
self._state != 'offline'):
|
||||
self.available):
|
||||
del self._device_state_attributes['last_seen']
|
||||
self._device_state_attributes['lqi'] = self._device.lqi
|
||||
self._device_state_attributes['rssi'] = self._device.rssi
|
||||
self._device_state_attributes['lqi'] = device.lqi
|
||||
self._device_state_attributes['rssi'] = device.rssi
|
||||
return self._device_state_attributes
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self._battery_listener:
|
||||
await self.async_accept_signal(
|
||||
self._battery_listener, SIGNAL_STATE_ATTR,
|
||||
self.async_update_state_attribute)
|
||||
# only do this on add to HA because it is static
|
||||
await self._async_init_battery_values()
|
||||
|
||||
async def async_update(self):
|
||||
"""Handle polling."""
|
||||
if self._device.last_seen is None:
|
||||
self._state = 'offline'
|
||||
if self._zha_device.last_seen is None:
|
||||
self._zha_device.update_available(False)
|
||||
else:
|
||||
difference = time.time() - self._device.last_seen
|
||||
difference = time.time() - self._zha_device.last_seen
|
||||
if difference > self._keepalive_interval:
|
||||
self._state = 'offline'
|
||||
self._zha_device.update_available(False)
|
||||
self._state = None
|
||||
else:
|
||||
self._zha_device.update_available(True)
|
||||
self._state = 'online'
|
||||
if self._battery_listener:
|
||||
await self.async_get_latest_battery_reading()
|
||||
|
||||
async def _async_init_battery_values(self):
|
||||
"""Get initial battery level and battery info from listener cache."""
|
||||
battery_size = await self._battery_listener.get_attribute_value(
|
||||
'battery_size')
|
||||
if battery_size is not None:
|
||||
self._device_state_attributes['battery_size'] = BATTERY_SIZES.get(
|
||||
battery_size, 'Unknown')
|
||||
|
||||
battery_quantity = await self._battery_listener.get_attribute_value(
|
||||
'battery_quantity')
|
||||
if battery_quantity is not None:
|
||||
self._device_state_attributes['battery_quantity'] = \
|
||||
battery_quantity
|
||||
await self.async_get_latest_battery_reading()
|
||||
|
||||
async def async_get_latest_battery_reading(self):
|
||||
"""Get the latest battery reading from listeners cache."""
|
||||
battery = await self._battery_listener.get_attribute_value(
|
||||
'battery_percentage_remaining')
|
||||
if battery is not None:
|
||||
self._device_state_attributes['battery_level'] = battery
|
||||
|
@ -4,20 +4,18 @@ Entity for Zigbee Home Automation.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from random import uniform
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import callback
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .core.const import (
|
||||
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE,
|
||||
ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE,
|
||||
ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS)
|
||||
from .core.helpers import bind_configure_reporting
|
||||
DOMAIN, ATTR_MANUFACTURER, DATA_ZHA, DATA_ZHA_BRIDGE_ID, MODEL, NAME,
|
||||
SIGNAL_REMOVE
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -29,287 +27,155 @@ class ZhaEntity(entity.Entity):
|
||||
|
||||
_domain = None # Must be overridden by subclasses
|
||||
|
||||
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
|
||||
model, application_listener, unique_id, new_join=False,
|
||||
**kwargs):
|
||||
def __init__(self, unique_id, zha_device, listeners,
|
||||
skip_entity_id=False, **kwargs):
|
||||
"""Init ZHA entity."""
|
||||
self._device_state_attributes = {}
|
||||
self._name = None
|
||||
ieee = endpoint.device.ieee
|
||||
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
||||
if manufacturer and model is not None:
|
||||
self.entity_id = "{}.{}_{}_{}_{}{}".format(
|
||||
self._domain,
|
||||
slugify(manufacturer),
|
||||
slugify(model),
|
||||
ieeetail,
|
||||
endpoint.endpoint_id,
|
||||
kwargs.get(ENTITY_SUFFIX, ''),
|
||||
)
|
||||
self._name = "{} {}".format(manufacturer, model)
|
||||
else:
|
||||
self.entity_id = "{}.zha_{}_{}{}".format(
|
||||
self._domain,
|
||||
ieeetail,
|
||||
endpoint.endpoint_id,
|
||||
kwargs.get(ENTITY_SUFFIX, ''),
|
||||
)
|
||||
|
||||
self._endpoint = endpoint
|
||||
self._in_clusters = in_clusters
|
||||
self._out_clusters = out_clusters
|
||||
self._new_join = new_join
|
||||
self._state = None
|
||||
self._force_update = False
|
||||
self._should_poll = False
|
||||
self._unique_id = unique_id
|
||||
|
||||
# Normally the entity itself is the listener. Sub-classes may set this
|
||||
# to a dict of cluster ID -> listener to receive messages for specific
|
||||
# clusters separately
|
||||
self._in_listeners = {}
|
||||
self._out_listeners = {}
|
||||
|
||||
self._initialized = False
|
||||
self.manufacturer_code = None
|
||||
application_listener.register_entity(ieee, self)
|
||||
|
||||
async def get_clusters(self):
|
||||
"""Get zigbee clusters from this entity."""
|
||||
return {
|
||||
IN: self._in_clusters,
|
||||
OUT: self._out_clusters
|
||||
}
|
||||
|
||||
async def _get_cluster(self, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee cluster from this entity."""
|
||||
if cluster_type == IN:
|
||||
cluster = self._in_clusters[cluster_id]
|
||||
else:
|
||||
cluster = self._out_clusters[cluster_id]
|
||||
if cluster is None:
|
||||
_LOGGER.warning('in_cluster with id: %s not found on entity: %s',
|
||||
cluster_id, self.entity_id)
|
||||
return cluster
|
||||
|
||||
async def get_cluster_attributes(self, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee attributes for specified cluster."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
return cluster.attributes
|
||||
|
||||
async def write_zigbe_attribute(self, cluster_id, attribute, value,
|
||||
cluster_type=IN, manufacturer=None):
|
||||
"""Write a value to a zigbee attribute for a cluster in this entity."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
|
||||
from zigpy.exceptions import DeliveryError
|
||||
try:
|
||||
response = await cluster.write_attributes(
|
||||
{attribute: value},
|
||||
manufacturer=manufacturer
|
||||
self._name = None
|
||||
if zha_device.manufacturer and zha_device.model is not None:
|
||||
self._name = "{} {}".format(
|
||||
zha_device.manufacturer,
|
||||
zha_device.model
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'set: %s for attr: %s to cluster: %s for entity: %s - res: %s',
|
||||
value,
|
||||
attribute,
|
||||
cluster_id,
|
||||
self.entity_id,
|
||||
response
|
||||
)
|
||||
return response
|
||||
except DeliveryError as exc:
|
||||
_LOGGER.debug(
|
||||
'failed to set attribute: %s %s %s %s %s',
|
||||
'{}: {}'.format(ATTR_VALUE, value),
|
||||
'{}: {}'.format(ATTR_ATTRIBUTE, attribute),
|
||||
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_id),
|
||||
'{}: {}'.format(ATTR_ENTITY_ID, self.entity_id),
|
||||
exc
|
||||
)
|
||||
|
||||
async def get_cluster_commands(self, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee commands for specified cluster."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
return {
|
||||
CLIENT_COMMANDS: cluster.client_commands,
|
||||
SERVER_COMMANDS: cluster.server_commands,
|
||||
}
|
||||
|
||||
async def issue_cluster_command(self, cluster_id, command, command_type,
|
||||
args, cluster_type=IN,
|
||||
manufacturer=None):
|
||||
"""Issue a command against specified zigbee cluster on this entity."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
response = None
|
||||
if command_type == SERVER:
|
||||
response = await cluster.command(command, *args,
|
||||
manufacturer=manufacturer,
|
||||
expect_reply=True)
|
||||
else:
|
||||
response = await cluster.client_command(command, *args)
|
||||
|
||||
_LOGGER.debug(
|
||||
'Issued cluster command: %s %s %s %s %s %s %s',
|
||||
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_id),
|
||||
'{}: {}'.format(ATTR_COMMAND, command),
|
||||
'{}: {}'.format(ATTR_COMMAND_TYPE, command_type),
|
||||
'{}: {}'.format(ATTR_ARGS, args),
|
||||
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_type),
|
||||
'{}: {}'.format(ATTR_MANUFACTURER, manufacturer),
|
||||
'{}: {}'.format(ATTR_ENTITY_ID, self.entity_id)
|
||||
)
|
||||
return response
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity addition to hass.
|
||||
|
||||
It is now safe to update the entity state
|
||||
"""
|
||||
for cluster_id, cluster in self._in_clusters.items():
|
||||
cluster.add_listener(self._in_listeners.get(cluster_id, self))
|
||||
for cluster_id, cluster in self._out_clusters.items():
|
||||
cluster.add_listener(self._out_listeners.get(cluster_id, self))
|
||||
|
||||
self._endpoint.device.zdo.add_listener(self)
|
||||
|
||||
if self._new_join:
|
||||
self.hass.async_create_task(self.async_configure())
|
||||
|
||||
self._initialized = True
|
||||
|
||||
async def async_configure(self):
|
||||
"""Set cluster binding and attribute reporting."""
|
||||
for cluster_key, attrs in self.zcl_reporting_config.items():
|
||||
cluster = self._get_cluster_from_report_config(cluster_key)
|
||||
if cluster is None:
|
||||
continue
|
||||
|
||||
manufacturer = None
|
||||
if cluster.cluster_id >= 0xfc00 and self.manufacturer_code:
|
||||
manufacturer = self.manufacturer_code
|
||||
|
||||
skip_bind = False # bind cluster only for the 1st configured attr
|
||||
for attr, details in attrs.items():
|
||||
min_report_interval, max_report_interval, change = details
|
||||
await bind_configure_reporting(
|
||||
self.entity_id, cluster, attr,
|
||||
min_report=min_report_interval,
|
||||
max_report=max_report_interval,
|
||||
reportable_change=change,
|
||||
skip_bind=skip_bind,
|
||||
manufacturer=manufacturer
|
||||
if not skip_entity_id:
|
||||
ieee = zha_device.ieee
|
||||
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
||||
if zha_device.manufacturer and zha_device.model is not None:
|
||||
self.entity_id = "{}.{}_{}_{}_{}{}".format(
|
||||
self._domain,
|
||||
slugify(zha_device.manufacturer),
|
||||
slugify(zha_device.model),
|
||||
ieeetail,
|
||||
listeners[0].cluster.endpoint.endpoint_id,
|
||||
kwargs.get(ENTITY_SUFFIX, ''),
|
||||
)
|
||||
skip_bind = True
|
||||
await asyncio.sleep(uniform(0.1, 0.5))
|
||||
_LOGGER.debug("%s: finished configuration", self.entity_id)
|
||||
|
||||
def _get_cluster_from_report_config(self, cluster_key):
|
||||
"""Parse an entry from zcl_reporting_config dict."""
|
||||
from zigpy.zcl import Cluster as Zcl_Cluster
|
||||
|
||||
cluster = None
|
||||
if isinstance(cluster_key, Zcl_Cluster):
|
||||
cluster = cluster_key
|
||||
elif isinstance(cluster_key, str):
|
||||
cluster = getattr(self._endpoint, cluster_key, None)
|
||||
elif isinstance(cluster_key, int):
|
||||
if cluster_key in self._in_clusters:
|
||||
cluster = self._in_clusters[cluster_key]
|
||||
elif cluster_key in self._out_clusters:
|
||||
cluster = self._out_clusters[cluster_key]
|
||||
elif issubclass(cluster_key, Zcl_Cluster):
|
||||
cluster_id = cluster_key.cluster_id
|
||||
if cluster_id in self._in_clusters:
|
||||
cluster = self._in_clusters[cluster_id]
|
||||
elif cluster_id in self._out_clusters:
|
||||
cluster = self._out_clusters[cluster_id]
|
||||
return cluster
|
||||
else:
|
||||
self.entity_id = "{}.zha_{}_{}{}".format(
|
||||
self._domain,
|
||||
ieeetail,
|
||||
listeners[0].cluster.endpoint.endpoint_id,
|
||||
kwargs.get(ENTITY_SUFFIX, ''),
|
||||
)
|
||||
self._state = None
|
||||
self._device_state_attributes = {}
|
||||
self._zha_device = zha_device
|
||||
self.cluster_listeners = {}
|
||||
# this will get flipped to false once we enable the feature after the
|
||||
# reorg is merged
|
||||
self._available = True
|
||||
self._component = kwargs['component']
|
||||
self._unsubs = []
|
||||
for listener in listeners:
|
||||
self.cluster_listeners[listener.name] = listener
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return Entity's default name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self):
|
||||
"""Return a dict of ZCL attribute reporting configuration.
|
||||
|
||||
{
|
||||
Cluster_Class: {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
Cluster_Instance: {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
cluster_id: {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
'cluster_name': {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
}
|
||||
"""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def zha_device(self):
|
||||
"""Return the zha device this entity is attached to."""
|
||||
return self._zha_device
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return self._device_state_attributes
|
||||
|
||||
@property
|
||||
def force_update(self) -> bool:
|
||||
"""Force update this entity."""
|
||||
return self._force_update
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Let ZHA handle polling."""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle an attribute updated on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def zdo_command(self, tsn, command_id, args):
|
||||
"""Handle a ZDO command received on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def device_announce(self, device):
|
||||
"""Handle device_announce zdo event."""
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
@callback
|
||||
def permit_duration(self, permit_duration):
|
||||
"""Handle permit_duration zdo event."""
|
||||
pass
|
||||
"""Poll state from device."""
|
||||
return self._should_poll
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
ieee = str(self._endpoint.device.ieee)
|
||||
zha_device_info = self._zha_device.device_info
|
||||
ieee = zha_device_info['ieee']
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, ieee)},
|
||||
'identifiers': {(DOMAIN, ieee)},
|
||||
ATTR_MANUFACTURER: self._endpoint.manufacturer,
|
||||
'model': self._endpoint.model,
|
||||
'name': self.name or ieee,
|
||||
ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER],
|
||||
MODEL: zha_device_info[MODEL],
|
||||
NAME: zha_device_info[NAME],
|
||||
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
|
||||
}
|
||||
|
||||
@callback
|
||||
def zha_send_event(self, cluster, command, args):
|
||||
"""Relay entity events to hass."""
|
||||
pass # don't relay events from entities
|
||||
@property
|
||||
def available(self):
|
||||
"""Return entity availability."""
|
||||
return self._available
|
||||
|
||||
def async_set_available(self, available):
|
||||
"""Set entity availability."""
|
||||
self._available = available
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def async_update_state_attribute(self, key, value):
|
||||
"""Update a single device state attribute."""
|
||||
self._device_state_attributes.update({
|
||||
key: value
|
||||
})
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def async_set_state(self, state):
|
||||
"""Set the entity state."""
|
||||
pass
|
||||
|
||||
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(
|
||||
None, "{}_{}".format(self.zha_device.available_signal, 'entity'),
|
||||
self.async_set_available,
|
||||
signal_override=True)
|
||||
await self.async_accept_signal(
|
||||
None, "{}_{}".format(SIGNAL_REMOVE, str(self.zha_device.ieee)),
|
||||
self.async_remove,
|
||||
signal_override=True
|
||||
)
|
||||
self._zha_device.gateway.register_entity_reference(
|
||||
self._zha_device.ieee, self.entity_id, self._zha_device,
|
||||
self.cluster_listeners, self.device_info)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect entity object when removed."""
|
||||
for unsub in self._unsubs:
|
||||
unsub()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
for listener in self.cluster_listeners:
|
||||
if hasattr(listener, 'async_update'):
|
||||
await listener.async_update()
|
||||
|
||||
async def async_accept_signal(self, listener, signal, func,
|
||||
signal_override=False):
|
||||
"""Accept a signal from a listener."""
|
||||
unsub = None
|
||||
if signal_override:
|
||||
unsub = async_dispatcher_connect(
|
||||
self.hass,
|
||||
signal,
|
||||
func
|
||||
)
|
||||
else:
|
||||
unsub = async_dispatcher_connect(
|
||||
self.hass,
|
||||
"{}_{}".format(listener.unique_id, signal),
|
||||
func
|
||||
)
|
||||
self._unsubs.append(unsub)
|
||||
|
@ -1,99 +0,0 @@
|
||||
"""
|
||||
Event for Zigbee Home Automation.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.core import EventOrigin, callback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZhaEvent():
|
||||
"""A base class for ZHA events."""
|
||||
|
||||
def __init__(self, hass, cluster, **kwargs):
|
||||
"""Init ZHA event."""
|
||||
self._hass = hass
|
||||
self._cluster = cluster
|
||||
cluster.add_listener(self)
|
||||
ieee = cluster.endpoint.device.ieee
|
||||
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
||||
endpoint = cluster.endpoint
|
||||
if endpoint.manufacturer and endpoint.model is not None:
|
||||
self._unique_id = "{}.{}_{}_{}_{}{}".format(
|
||||
'zha_event',
|
||||
slugify(endpoint.manufacturer),
|
||||
slugify(endpoint.model),
|
||||
ieeetail,
|
||||
cluster.endpoint.endpoint_id,
|
||||
kwargs.get('entity_suffix', ''),
|
||||
)
|
||||
else:
|
||||
self._unique_id = "{}.zha_{}_{}{}".format(
|
||||
'zha_event',
|
||||
ieeetail,
|
||||
cluster.endpoint.endpoint_id,
|
||||
kwargs.get('entity_suffix', ''),
|
||||
)
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle an attribute updated on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def zdo_command(self, tsn, command_id, args):
|
||||
"""Handle a ZDO command received on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle a cluster command received on this cluster."""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def zha_send_event(self, cluster, command, args):
|
||||
"""Relay entity events to hass."""
|
||||
self._hass.bus.async_fire(
|
||||
'zha_event',
|
||||
{
|
||||
'unique_id': self._unique_id,
|
||||
'command': command,
|
||||
'args': args
|
||||
},
|
||||
EventOrigin.remote
|
||||
)
|
||||
|
||||
|
||||
class ZhaRelayEvent(ZhaEvent):
|
||||
"""Event relay that can be attached to zigbee clusters."""
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle an attribute updated on this cluster."""
|
||||
self.zha_send_event(
|
||||
self._cluster,
|
||||
'attribute_updated',
|
||||
{
|
||||
'attribute_id': attribute,
|
||||
'attribute_name': self._cluster.attributes.get(
|
||||
attribute,
|
||||
['Unknown'])[0],
|
||||
'value': value
|
||||
}
|
||||
)
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle a cluster command received on this cluster."""
|
||||
if self._cluster.server_commands is not None and\
|
||||
self._cluster.server_commands.get(command_id) is not None:
|
||||
self.zha_send_event(
|
||||
self._cluster,
|
||||
self._cluster.server_commands.get(command_id)[0],
|
||||
args
|
||||
)
|
@ -10,9 +10,10 @@ from homeassistant.components.fan import (
|
||||
DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
|
||||
FanEntity)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from .core import helpers
|
||||
from .core.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_FAN,
|
||||
SIGNAL_ATTR_UPDATED
|
||||
)
|
||||
from .entity import ZhaEntity
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
@ -79,19 +80,17 @@ class ZhaFan(ZhaEntity, FanEntity):
|
||||
"""Representation of a ZHA fan."""
|
||||
|
||||
_domain = DOMAIN
|
||||
value_attribute = 0 # fan_mode
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {self.value_attribute: REPORT_CONFIG_OP}
|
||||
}
|
||||
def __init__(self, unique_id, zha_device, listeners, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, listeners, **kwargs)
|
||||
self._fan_listener = self.cluster_listeners.get(LISTENER_FAN)
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Fan ZCL Cluster."""
|
||||
return self._endpoint.fan
|
||||
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._fan_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
@ -115,6 +114,16 @@ class ZhaFan(ZhaEntity, FanEntity):
|
||||
return False
|
||||
return self._state != SPEED_OFF
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return state attributes."""
|
||||
return self.state_attributes
|
||||
|
||||
def async_set_state(self, state):
|
||||
"""Handle state update from listener."""
|
||||
self._state = VALUE_TO_SPEED.get(state, self._state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
||||
"""Turn the entity on."""
|
||||
if speed is None:
|
||||
@ -128,31 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity):
|
||||
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
try:
|
||||
await self._endpoint.fan.write_attributes(
|
||||
{'fan_mode': SPEED_TO_VALUE[speed]}
|
||||
)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Could not set speed: %s", self.entity_id, ex)
|
||||
return
|
||||
|
||||
self._state = speed
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await helpers.safe_read(self.cluster, ['fan_mode'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
new_value = result.get('fan_mode', None)
|
||||
self._state = VALUE_TO_SPEED.get(new_value, None)
|
||||
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle attribute update from device."""
|
||||
attr_name = self.cluster.attributes.get(attribute, [attribute])[0]
|
||||
_LOGGER.debug("%s: Attribute report '%s'[%s] = %s",
|
||||
self.entity_id, self.cluster.name, attr_name, value)
|
||||
if attribute == self.value_attribute:
|
||||
self._state = VALUE_TO_SPEED.get(value, self._state)
|
||||
self.async_schedule_update_ha_state()
|
||||
await self._fan_listener.async_set_speed(SPEED_TO_VALUE[speed])
|
||||
self.async_set_state(speed)
|
||||
|
@ -9,14 +9,12 @@ import logging
|
||||
from homeassistant.components import light
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.color as color_util
|
||||
from .core import helpers
|
||||
from .core.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT,
|
||||
REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
||||
from .const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR,
|
||||
LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL
|
||||
)
|
||||
from .entity import ZhaEntity
|
||||
from .core.listeners import (
|
||||
OnOffListener, LevelListener
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -58,26 +56,6 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
"""Set up the ZHA lights."""
|
||||
entities = []
|
||||
for discovery_info in discovery_infos:
|
||||
endpoint = discovery_info['endpoint']
|
||||
if hasattr(endpoint, 'light_color'):
|
||||
caps = await helpers.safe_read(
|
||||
endpoint.light_color, ['color_capabilities'])
|
||||
discovery_info['color_capabilities'] = caps.get(
|
||||
'color_capabilities')
|
||||
if discovery_info['color_capabilities'] is None:
|
||||
# ZCL Version 4 devices don't support the color_capabilities
|
||||
# attribute. In this version XY support is mandatory, but we
|
||||
# need to probe to determine if the device supports color
|
||||
# temperature.
|
||||
discovery_info['color_capabilities'] = \
|
||||
CAPABILITIES_COLOR_XY
|
||||
result = await helpers.safe_read(
|
||||
endpoint.light_color, ['color_temperature'])
|
||||
if (result.get('color_temperature') is not
|
||||
UNSUPPORTED_ATTRIBUTE):
|
||||
discovery_info['color_capabilities'] |= \
|
||||
CAPABILITIES_COLOR_TEMP
|
||||
|
||||
zha_light = Light(**discovery_info)
|
||||
entities.append(zha_light)
|
||||
|
||||
@ -89,34 +67,24 @@ class Light(ZhaEntity, light.Light):
|
||||
|
||||
_domain = light.DOMAIN
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, listeners, **kwargs):
|
||||
"""Initialize the ZHA light."""
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(unique_id, zha_device, listeners, **kwargs)
|
||||
self._supported_features = 0
|
||||
self._color_temp = None
|
||||
self._hs_color = None
|
||||
self._brightness = None
|
||||
from zigpy.zcl.clusters.general import OnOff, LevelControl
|
||||
self._in_listeners = {
|
||||
OnOff.cluster_id: OnOffListener(
|
||||
self,
|
||||
self._in_clusters[OnOff.cluster_id]
|
||||
),
|
||||
}
|
||||
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
|
||||
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL)
|
||||
self._color_listener = self.cluster_listeners.get(LISTENER_COLOR)
|
||||
|
||||
if LevelControl.cluster_id in self._in_clusters:
|
||||
if self._level_listener:
|
||||
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
||||
self._supported_features |= light.SUPPORT_TRANSITION
|
||||
self._brightness = 0
|
||||
self._in_listeners.update({
|
||||
LevelControl.cluster_id: LevelListener(
|
||||
self,
|
||||
self._in_clusters[LevelControl.cluster_id]
|
||||
)
|
||||
})
|
||||
import zigpy.zcl.clusters as zcl_clusters
|
||||
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
|
||||
color_capabilities = kwargs['color_capabilities']
|
||||
|
||||
if self._color_listener:
|
||||
color_capabilities = self._color_listener.get_color_capabilities()
|
||||
if color_capabilities & CAPABILITIES_COLOR_TEMP:
|
||||
self._supported_features |= light.SUPPORT_COLOR_TEMP
|
||||
|
||||
@ -124,131 +92,28 @@ class Light(ZhaEntity, light.Light):
|
||||
self._supported_features |= light.SUPPORT_COLOR
|
||||
self._hs_color = (0, 0)
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return attribute reporting configuration."""
|
||||
return {
|
||||
'on_off': {'on_off': REPORT_CONFIG_IMMEDIATE},
|
||||
'level': {'current_level': REPORT_CONFIG_ASAP},
|
||||
'light_color': {
|
||||
'current_x': REPORT_CONFIG_DEFAULT,
|
||||
'current_y': REPORT_CONFIG_DEFAULT,
|
||||
'color_temperature': REPORT_CONFIG_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set the state."""
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
|
||||
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
|
||||
duration = duration * 10 # tenths of s
|
||||
if light.ATTR_COLOR_TEMP in kwargs and \
|
||||
self.supported_features & light.SUPPORT_COLOR_TEMP:
|
||||
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||
try:
|
||||
res = await self._endpoint.light_color.move_to_color_temp(
|
||||
temperature, duration)
|
||||
_LOGGER.debug("%s: moved to %i color temp: %s",
|
||||
self.entity_id, temperature, res)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Couldn't change color temp: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
self._color_temp = temperature
|
||||
|
||||
if light.ATTR_HS_COLOR in kwargs and \
|
||||
self.supported_features & light.SUPPORT_COLOR:
|
||||
self._hs_color = kwargs[light.ATTR_HS_COLOR]
|
||||
xy_color = color_util.color_hs_to_xy(*self._hs_color)
|
||||
try:
|
||||
res = await self._endpoint.light_color.move_to_color(
|
||||
int(xy_color[0] * 65535),
|
||||
int(xy_color[1] * 65535),
|
||||
duration,
|
||||
)
|
||||
_LOGGER.debug("%s: moved XY color to (%1.2f, %1.2f): %s",
|
||||
self.entity_id, xy_color[0], xy_color[1], res)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Couldn't change color temp: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
|
||||
if self._brightness is not None:
|
||||
brightness = kwargs.get(
|
||||
light.ATTR_BRIGHTNESS, self._brightness or 255)
|
||||
# Move to level with on/off:
|
||||
try:
|
||||
res = await self._endpoint.level.move_to_level_with_on_off(
|
||||
brightness,
|
||||
duration
|
||||
)
|
||||
_LOGGER.debug("%s: moved to %i level with on/off: %s",
|
||||
self.entity_id, brightness, res)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Couldn't change brightness level: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
self._state = 1
|
||||
self._brightness = brightness
|
||||
self.async_schedule_update_ha_state()
|
||||
return
|
||||
|
||||
try:
|
||||
res = await self._endpoint.on_off.on()
|
||||
_LOGGER.debug("%s was turned on: %s", self.entity_id, res)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Unable to turn the light on: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
|
||||
self._state = 1
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
duration = kwargs.get(light.ATTR_TRANSITION)
|
||||
try:
|
||||
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
|
||||
if duration and supports_level:
|
||||
res = await self._endpoint.level.move_to_level_with_on_off(
|
||||
0, duration*10
|
||||
)
|
||||
else:
|
||||
res = await self._endpoint.on_off.off()
|
||||
_LOGGER.debug("%s was turned off: %s", self.entity_id, res)
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Unable to turn the light off: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
|
||||
self._state = 0
|
||||
self.async_schedule_update_ha_state()
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
"""Return the brightness of this light."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return state attributes."""
|
||||
return self.state_attributes
|
||||
|
||||
def set_level(self, value):
|
||||
"""Set the brightness of this light between 0..255."""
|
||||
if value < 0 or value > 255:
|
||||
return
|
||||
value = max(0, min(255, value))
|
||||
self._brightness = value
|
||||
self.async_schedule_update_ha_state()
|
||||
self.async_set_state(value)
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
@ -265,40 +130,82 @@ class Light(ZhaEntity, light.Light):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await helpers.safe_read(self._endpoint.on_off, ['on_off'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
self._state = result.get('on_off', self._state)
|
||||
def async_set_state(self, state):
|
||||
"""Set the state."""
|
||||
self._state = bool(state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._supported_features & light.SUPPORT_BRIGHTNESS:
|
||||
result = await helpers.safe_read(self._endpoint.level,
|
||||
['current_level'],
|
||||
allow_cache=False,
|
||||
only_cache=(
|
||||
not self._initialized
|
||||
))
|
||||
self._brightness = result.get('current_level', self._brightness)
|
||||
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._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||
if self._level_listener:
|
||||
await self.async_accept_signal(
|
||||
self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
|
||||
|
||||
if self._supported_features & light.SUPPORT_COLOR_TEMP:
|
||||
result = await helpers.safe_read(self._endpoint.light_color,
|
||||
['color_temperature'],
|
||||
allow_cache=False,
|
||||
only_cache=(
|
||||
not self._initialized
|
||||
))
|
||||
self._color_temp = result.get('color_temperature',
|
||||
self._color_temp)
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
|
||||
duration = duration * 10 # tenths of s
|
||||
|
||||
if self._supported_features & light.SUPPORT_COLOR:
|
||||
result = await helpers.safe_read(self._endpoint.light_color,
|
||||
['current_x', 'current_y'],
|
||||
allow_cache=False,
|
||||
only_cache=(
|
||||
not self._initialized
|
||||
))
|
||||
if 'current_x' in result and 'current_y' in result:
|
||||
xy_color = (round(result['current_x']/65535, 3),
|
||||
round(result['current_y']/65535, 3))
|
||||
self._hs_color = color_util.color_xy_to_hs(*xy_color)
|
||||
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_listener.move_to_color_temp(
|
||||
temperature, duration)
|
||||
if not success:
|
||||
return
|
||||
self._color_temp = temperature
|
||||
|
||||
if light.ATTR_HS_COLOR in kwargs and \
|
||||
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_listener.move_to_color(
|
||||
int(xy_color[0] * 65535),
|
||||
int(xy_color[1] * 65535),
|
||||
duration,
|
||||
)
|
||||
if not success:
|
||||
return
|
||||
self._hs_color = hs_color
|
||||
|
||||
if self._brightness is not None:
|
||||
brightness = kwargs.get(
|
||||
light.ATTR_BRIGHTNESS, self._brightness or 255)
|
||||
success = await self._level_listener.move_to_level_with_on_off(
|
||||
brightness,
|
||||
duration
|
||||
)
|
||||
if not success:
|
||||
return
|
||||
self._state = True
|
||||
self._brightness = brightness
|
||||
self.async_schedule_update_ha_state()
|
||||
return
|
||||
|
||||
success = await self._on_off_listener.on()
|
||||
if not success:
|
||||
return
|
||||
|
||||
self._state = True
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
duration = kwargs.get(light.ATTR_TRANSITION)
|
||||
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
|
||||
success = None
|
||||
if duration and supports_level:
|
||||
success = await self._level_listener.move_to_level_with_on_off(
|
||||
0,
|
||||
duration*10
|
||||
)
|
||||
else:
|
||||
success = await self._on_off_listener.off()
|
||||
_LOGGER.debug("%s was turned off: %s", self.entity_id, success)
|
||||
if not success:
|
||||
return
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
@ -9,11 +9,11 @@ import logging
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from .core import helpers
|
||||
from .core.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
|
||||
ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
|
||||
POWER_CONFIGURATION, GENERIC, SENSOR_TYPE, LISTENER_ATTRIBUTE,
|
||||
LISTENER_ACTIVE_POWER, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR)
|
||||
from .entity import ZhaEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -21,6 +21,73 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
|
||||
# Formatter functions
|
||||
def pass_through_formatter(value):
|
||||
"""No op update function."""
|
||||
return value
|
||||
|
||||
|
||||
def temperature_formatter(value):
|
||||
"""Convert temperature data."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(value / 100, 1)
|
||||
|
||||
|
||||
def humidity_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(float(value) / 100, 1)
|
||||
|
||||
|
||||
def active_power_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(float(value) / 10, 1)
|
||||
|
||||
|
||||
def pressure_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return round(float(value))
|
||||
|
||||
|
||||
FORMATTER_FUNC_REGISTRY = {
|
||||
HUMIDITY: humidity_formatter,
|
||||
TEMPERATURE: temperature_formatter,
|
||||
PRESSURE: pressure_formatter,
|
||||
ELECTRICAL_MEASUREMENT: active_power_formatter,
|
||||
GENERIC: pass_through_formatter,
|
||||
}
|
||||
|
||||
UNIT_REGISTRY = {
|
||||
HUMIDITY: '%',
|
||||
TEMPERATURE: TEMP_CELSIUS,
|
||||
PRESSURE: 'hPa',
|
||||
ILLUMINANCE: 'lx',
|
||||
METERING: 'W',
|
||||
ELECTRICAL_MEASUREMENT: 'W',
|
||||
POWER_CONFIGURATION: '%',
|
||||
GENERIC: None
|
||||
}
|
||||
|
||||
LISTENER_REGISTRY = {
|
||||
ELECTRICAL_MEASUREMENT: LISTENER_ACTIVE_POWER,
|
||||
}
|
||||
|
||||
POLLING_REGISTRY = {
|
||||
ELECTRICAL_MEASUREMENT: True
|
||||
}
|
||||
|
||||
FORCE_UPDATE_REGISTRY = {
|
||||
ELECTRICAL_MEASUREMENT: True
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old way of setting up Zigbee Home Automation sensors."""
|
||||
@ -56,279 +123,59 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
|
||||
async def make_sensor(discovery_info):
|
||||
"""Create ZHA sensors factory."""
|
||||
from zigpy.zcl.clusters.measurement import (
|
||||
RelativeHumidity, TemperatureMeasurement, PressureMeasurement,
|
||||
IlluminanceMeasurement
|
||||
)
|
||||
from zigpy.zcl.clusters.smartenergy import Metering
|
||||
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
|
||||
from zigpy.zcl.clusters.general import PowerConfiguration
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
if 'sub_component' in discovery_info:
|
||||
sensor = discovery_info['sub_component'](**discovery_info)
|
||||
elif RelativeHumidity.cluster_id in in_clusters:
|
||||
sensor = RelativeHumiditySensor(**discovery_info)
|
||||
elif PowerConfiguration.cluster_id in in_clusters:
|
||||
sensor = GenericBatterySensor(**discovery_info)
|
||||
elif TemperatureMeasurement.cluster_id in in_clusters:
|
||||
sensor = TemperatureSensor(**discovery_info)
|
||||
elif PressureMeasurement.cluster_id in in_clusters:
|
||||
sensor = PressureSensor(**discovery_info)
|
||||
elif IlluminanceMeasurement.cluster_id in in_clusters:
|
||||
sensor = IlluminanceMeasurementSensor(**discovery_info)
|
||||
elif Metering.cluster_id in in_clusters:
|
||||
sensor = MeteringSensor(**discovery_info)
|
||||
elif ElectricalMeasurement.cluster_id in in_clusters:
|
||||
sensor = ElectricalMeasurementSensor(**discovery_info)
|
||||
return sensor
|
||||
else:
|
||||
sensor = Sensor(**discovery_info)
|
||||
|
||||
return sensor
|
||||
return Sensor(**discovery_info)
|
||||
|
||||
|
||||
class Sensor(ZhaEntity):
|
||||
"""Base ZHA sensor."""
|
||||
|
||||
_domain = DOMAIN
|
||||
value_attribute = 0
|
||||
min_report_interval = REPORT_CONFIG_MIN_INT
|
||||
max_report_interval = REPORT_CONFIG_MAX_INT
|
||||
min_reportable_change = REPORT_CONFIG_RPT_CHANGE
|
||||
report_config = (min_report_interval, max_report_interval,
|
||||
min_reportable_change)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Init ZHA Sensor instance."""
|
||||
super().__init__(**kwargs)
|
||||
self._cluster = list(kwargs['in_clusters'].values())[0]
|
||||
def __init__(self, unique_id, zha_device, listeners, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, listeners, **kwargs)
|
||||
sensor_type = kwargs.get(SENSOR_TYPE, GENERIC)
|
||||
self._unit = UNIT_REGISTRY.get(sensor_type)
|
||||
self._formatter_function = FORMATTER_FUNC_REGISTRY.get(
|
||||
sensor_type,
|
||||
pass_through_formatter
|
||||
)
|
||||
self._force_update = FORCE_UPDATE_REGISTRY.get(
|
||||
sensor_type,
|
||||
False
|
||||
)
|
||||
self._should_poll = POLLING_REGISTRY.get(
|
||||
sensor_type,
|
||||
False
|
||||
)
|
||||
self._listener = self.cluster_listeners.get(
|
||||
LISTENER_REGISTRY.get(sensor_type, LISTENER_ATTRIBUTE)
|
||||
)
|
||||
|
||||
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._listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||
await self.async_accept_signal(
|
||||
self._listener, SIGNAL_STATE_ATTR,
|
||||
self.async_update_state_attribute)
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {self.value_attribute: self.report_config}
|
||||
}
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Return Sensor's cluster."""
|
||||
return self._cluster
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the state of the entity."""
|
||||
if self._state is None:
|
||||
return None
|
||||
if isinstance(self._state, float):
|
||||
return str(round(self._state, 2))
|
||||
return self._state
|
||||
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle attribute update from device."""
|
||||
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
|
||||
if attribute == self.value_attribute:
|
||||
self._state = value
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await helpers.safe_read(
|
||||
self.cluster,
|
||||
[self.value_attribute],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized)
|
||||
)
|
||||
self._state = result.get(self.value_attribute, self._state)
|
||||
|
||||
|
||||
class GenericBatterySensor(Sensor):
|
||||
"""ZHA generic battery sensor."""
|
||||
|
||||
report_attribute = 32
|
||||
value_attribute = 33
|
||||
battery_sizes = {
|
||||
0: 'No battery',
|
||||
1: 'Built in',
|
||||
2: 'Other',
|
||||
3: 'AA',
|
||||
4: 'AAA',
|
||||
5: 'C',
|
||||
6: 'D',
|
||||
7: 'CR2',
|
||||
8: 'CR123A',
|
||||
9: 'CR2450',
|
||||
10: 'CR2032',
|
||||
11: 'CR1632',
|
||||
255: 'Unknown'
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return '%'
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {
|
||||
self.value_attribute: self.report_config,
|
||||
self.report_attribute: self.report_config
|
||||
}
|
||||
}
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
_LOGGER.debug("%s async_update", self.entity_id)
|
||||
|
||||
result = await helpers.safe_read(
|
||||
self._endpoint.power,
|
||||
[
|
||||
'battery_size',
|
||||
'battery_quantity',
|
||||
'battery_percentage_remaining'
|
||||
],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized)
|
||||
)
|
||||
self._device_state_attributes['battery_size'] = self.battery_sizes.get(
|
||||
result.get('battery_size', 255), 'Unknown')
|
||||
self._device_state_attributes['battery_quantity'] = result.get(
|
||||
'battery_quantity', 'Unknown')
|
||||
self._state = result.get('battery_percentage_remaining', self._state)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if self._state == 'unknown' or self._state is None:
|
||||
return None
|
||||
|
||||
return self._state
|
||||
|
||||
|
||||
class TemperatureSensor(Sensor):
|
||||
"""ZHA temperature sensor."""
|
||||
|
||||
min_reportable_change = 50 # 0.5'C
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if self._state is None:
|
||||
return None
|
||||
celsius = self._state / 100
|
||||
return round(convert_temperature(celsius,
|
||||
TEMP_CELSIUS,
|
||||
self.unit_of_measurement),
|
||||
1)
|
||||
|
||||
|
||||
class RelativeHumiditySensor(Sensor):
|
||||
"""ZHA relative humidity sensor."""
|
||||
|
||||
min_reportable_change = 50 # 0.5%
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return '%'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if self._state is None:
|
||||
return None
|
||||
|
||||
return round(float(self._state) / 100, 1)
|
||||
|
||||
|
||||
class PressureSensor(Sensor):
|
||||
"""ZHA pressure sensor."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return 'hPa'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if self._state is None:
|
||||
return None
|
||||
|
||||
return round(float(self._state))
|
||||
|
||||
|
||||
class IlluminanceMeasurementSensor(Sensor):
|
||||
"""ZHA lux sensor."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return 'lx'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
|
||||
class MeteringSensor(Sensor):
|
||||
"""ZHA Metering sensor."""
|
||||
|
||||
value_attribute = 1024
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return 'W'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if self._state is None:
|
||||
return None
|
||||
|
||||
return round(float(self._state))
|
||||
|
||||
|
||||
class ElectricalMeasurementSensor(Sensor):
|
||||
"""ZHA Electrical Measurement sensor."""
|
||||
|
||||
value_attribute = 1291
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return 'W'
|
||||
|
||||
@property
|
||||
def force_update(self) -> bool:
|
||||
"""Force update this entity."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if self._state is None:
|
||||
return None
|
||||
|
||||
return round(float(self._state) / 10, 1)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Poll state from device."""
|
||||
return True
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
_LOGGER.debug("%s async_update", self.entity_id)
|
||||
|
||||
result = await helpers.safe_read(
|
||||
self.cluster, ['active_power'],
|
||||
allow_cache=False, only_cache=(not self._initialized))
|
||||
self._state = result.get('active_power', self._state)
|
||||
def async_set_state(self, state):
|
||||
"""Handle state update from listener."""
|
||||
self._state = self._formatter_function(state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
@ -8,9 +8,10 @@ import logging
|
||||
|
||||
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from .core import helpers
|
||||
from .core.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF,
|
||||
SIGNAL_ATTR_UPDATED
|
||||
)
|
||||
from .entity import ZhaEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -55,69 +56,39 @@ class Switch(ZhaEntity, SwitchDevice):
|
||||
"""ZHA switch."""
|
||||
|
||||
_domain = DOMAIN
|
||||
value_attribute = 0
|
||||
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle attribute update from device."""
|
||||
cluster = self._endpoint.on_off
|
||||
attr_name = cluster.attributes.get(attribute, [attribute])[0]
|
||||
_LOGGER.debug("%s: Attribute '%s' on cluster '%s' updated to %s",
|
||||
self.entity_id, attr_name, cluster.ep_attribute, value)
|
||||
if attribute == self.value_attribute:
|
||||
self._state = value
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Retrun a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {'on_off': REPORT_CONFIG_IMMEDIATE}
|
||||
}
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Entity's cluster."""
|
||||
return self._endpoint.on_off
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the ZHA switch."""
|
||||
super().__init__(**kwargs)
|
||||
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if the switch is on based on the statemachine."""
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
return self._state
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
try:
|
||||
res = await self._endpoint.on_off.on()
|
||||
_LOGGER.debug("%s: turned 'on': %s", self.entity_id, res[1])
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Unable to turn the switch on: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
|
||||
self._state = 1
|
||||
self.async_schedule_update_ha_state()
|
||||
await self._on_off_listener.on()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
from zigpy.exceptions import DeliveryError
|
||||
try:
|
||||
res = await self._endpoint.on_off.off()
|
||||
_LOGGER.debug("%s: turned 'off': %s", self.entity_id, res[1])
|
||||
except DeliveryError as ex:
|
||||
_LOGGER.error("%s: Unable to turn the switch off: %s",
|
||||
self.entity_id, ex)
|
||||
return
|
||||
await self._on_off_listener.off()
|
||||
|
||||
self._state = 0
|
||||
def async_set_state(self, state):
|
||||
"""Handle state update from listener."""
|
||||
self._state = bool(state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await helpers.safe_read(self.cluster,
|
||||
['on_off'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
self._state = result.get('on_off', self._state)
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return state attributes."""
|
||||
return self.state_attributes
|
||||
|
||||
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._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||
|
@ -3,9 +3,12 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.zha.core.const import (
|
||||
DOMAIN, DATA_ZHA
|
||||
DOMAIN, DATA_ZHA, COMPONENTS
|
||||
)
|
||||
from homeassistant.components.zha.core.gateway import ZHAGateway
|
||||
from homeassistant.components.zha.core.gateway import establish_device_mappings
|
||||
from homeassistant.components.zha.core.listeners \
|
||||
import populate_listener_registry
|
||||
from .common import async_setup_entry
|
||||
|
||||
|
||||
@ -25,6 +28,12 @@ def zha_gateway_fixture(hass):
|
||||
Create a ZHAGateway object that can be used to interact with as if we
|
||||
had a real zigbee network running.
|
||||
"""
|
||||
populate_listener_registry()
|
||||
establish_device_mappings()
|
||||
for component in COMPONENTS:
|
||||
hass.data[DATA_ZHA][component] = (
|
||||
hass.data[DATA_ZHA].get(component, {})
|
||||
)
|
||||
return ZHAGateway(hass, {})
|
||||
|
||||
|
||||
|
@ -48,6 +48,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway):
|
||||
# load up binary_sensor domain
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, DOMAIN)
|
||||
await zha_gateway.accept_zigbee_messages({})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# on off binary_sensor
|
||||
|
@ -26,6 +26,7 @@ async def test_fan(hass, config_entry, zha_gateway):
|
||||
# load up fan domain
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, DOMAIN)
|
||||
await zha_gateway.accept_zigbee_messages({})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cluster = zigpy_device.endpoints.get(1).fan
|
||||
|
@ -40,6 +40,7 @@ async def test_light(hass, config_entry, zha_gateway):
|
||||
# load up light domain
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, DOMAIN)
|
||||
await zha_gateway.accept_zigbee_messages({})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# on off light
|
||||
|
@ -92,6 +92,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
|
||||
# load up sensor domain
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, DOMAIN)
|
||||
await zha_gateway.accept_zigbee_messages({})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# put the other relevant info in the device info dict
|
||||
|
@ -24,6 +24,7 @@ async def test_switch(hass, config_entry, zha_gateway):
|
||||
# load up switch domain
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, DOMAIN)
|
||||
await zha_gateway.accept_zigbee_messages({})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cluster = zigpy_device.endpoints.get(1).on_off
|
||||
@ -44,6 +45,7 @@ async def test_switch(hass, config_entry, zha_gateway):
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
# turn on from HA
|
||||
with patch(
|
||||
'zigpy.zcl.Cluster.request',
|
||||
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
|
||||
@ -55,6 +57,7 @@ async def test_switch(hass, config_entry, zha_gateway):
|
||||
assert cluster.request.call_args == call(
|
||||
False, ON, (), expect_reply=True, manufacturer=None)
|
||||
|
||||
# turn off from HA
|
||||
with patch(
|
||||
'zigpy.zcl.Cluster.request',
|
||||
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
|
||||
@ -66,5 +69,6 @@ async def test_switch(hass, config_entry, zha_gateway):
|
||||
assert cluster.request.call_args == call(
|
||||
False, OFF, (), expect_reply=True, manufacturer=None)
|
||||
|
||||
# test joining a new switch to the network and HA
|
||||
await async_test_device_join(
|
||||
hass, zha_gateway, OnOff.cluster_id, DOMAIN, expected_state=STATE_OFF)
|
||||
|
Loading…
x
Reference in New Issue
Block a user