mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +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/zeroconf/*
|
||||||
homeassistant/components/zha/__init__.py
|
homeassistant/components/zha/__init__.py
|
||||||
homeassistant/components/zha/api.py
|
homeassistant/components/zha/api.py
|
||||||
homeassistant/components/zha/binary_sensor.py
|
|
||||||
homeassistant/components/zha/const.py
|
homeassistant/components/zha/const.py
|
||||||
homeassistant/components/zha/core/const.py
|
homeassistant/components/zha/core/const.py
|
||||||
homeassistant/components/zha/core/device.py
|
homeassistant/components/zha/core/device.py
|
||||||
@ -667,11 +666,8 @@ omit =
|
|||||||
homeassistant/components/zha/core/listeners.py
|
homeassistant/components/zha/core/listeners.py
|
||||||
homeassistant/components/zha/device_entity.py
|
homeassistant/components/zha/device_entity.py
|
||||||
homeassistant/components/zha/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/light.py
|
||||||
homeassistant/components/zha/sensor.py
|
homeassistant/components/zha/sensor.py
|
||||||
homeassistant/components/zha/switch.py
|
|
||||||
homeassistant/components/zigbee/*
|
homeassistant/components/zigbee/*
|
||||||
homeassistant/components/zoneminder/*
|
homeassistant/components/zoneminder/*
|
||||||
homeassistant/components/zwave/util.py
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/zha/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import types
|
import types
|
||||||
@ -17,14 +18,15 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
|||||||
# Loading the config flow file will register the flow
|
# Loading the config flow file will register the flow
|
||||||
from . import config_flow # noqa # pylint: disable=unused-import
|
from . import config_flow # noqa # pylint: disable=unused-import
|
||||||
from . import api
|
from . import api
|
||||||
from .core.gateway import ZHAGateway
|
from .core import ZHAGateway
|
||||||
from .const import (
|
from .core.const import (
|
||||||
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
|
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
|
||||||
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
|
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
|
||||||
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
|
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
|
||||||
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
|
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
|
||||||
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS,
|
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS)
|
||||||
ENABLE_QUIRKS)
|
from .core.gateway import establish_device_mappings
|
||||||
|
from .core.listeners import populate_listener_registry
|
||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'bellows==0.7.0',
|
'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.
|
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] = hass.data.get(DATA_ZHA, {})
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
|
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
|
||||||
|
|
||||||
config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
|
config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
|
||||||
|
|
||||||
if config.get(ENABLE_QUIRKS, True):
|
if config.get(ENABLE_QUIRKS, True):
|
||||||
@ -137,14 +146,32 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
ClusterPersistingListener
|
ClusterPersistingListener
|
||||||
)
|
)
|
||||||
|
|
||||||
application_controller = ControllerApplication(radio, database)
|
|
||||||
zha_gateway = ZHAGateway(hass, config)
|
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)
|
application_controller.add_listener(zha_gateway)
|
||||||
await application_controller.startup(auto_form=True)
|
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():
|
for device in application_controller.devices.values():
|
||||||
hass.async_create_task(
|
init_tasks.append(zha_gateway.async_device_initialized(device, False))
|
||||||
zha_gateway.async_device_initialized(device, False))
|
await asyncio.gather(*init_tasks)
|
||||||
|
|
||||||
device_registry = await \
|
device_registry = await \
|
||||||
hass.helpers.device_registry.async_get_registry()
|
hass.helpers.device_registry.async_get_registry()
|
||||||
@ -157,8 +184,6 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
model=radio_description,
|
model=radio_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee)
|
|
||||||
|
|
||||||
for component in COMPONENTS:
|
for component in COMPONENTS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.async_forward_entry_setup(
|
hass.config_entries.async_forward_entry_setup(
|
||||||
|
@ -11,8 +11,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from .device_entity import ZhaDeviceEntity
|
from .core.const import (
|
||||||
from .const import (
|
|
||||||
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
|
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
|
||||||
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
|
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
|
||||||
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER)
|
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER)
|
||||||
@ -118,115 +117,7 @@ SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.async_response
|
def async_load_api(hass, application_controller, zha_gateway):
|
||||||
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):
|
|
||||||
"""Set up the web socket API."""
|
"""Set up the web socket API."""
|
||||||
async def permit(service):
|
async def permit(service):
|
||||||
"""Allow devices to join this network."""
|
"""Allow devices to join this network."""
|
||||||
@ -256,11 +147,12 @@ def async_load_api(hass, application_controller, listener):
|
|||||||
attribute = service.data.get(ATTR_ATTRIBUTE)
|
attribute = service.data.get(ATTR_ATTRIBUTE)
|
||||||
value = service.data.get(ATTR_VALUE)
|
value = service.data.get(ATTR_VALUE)
|
||||||
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
||||||
component = hass.data.get(entity_id.split('.')[0])
|
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||||
entity = component.get_entity(entity_id)
|
|
||||||
response = None
|
response = None
|
||||||
if entity is not None:
|
if entity_ref is not None:
|
||||||
response = await entity.write_zigbe_attribute(
|
response = await entity_ref.zha_device.write_zigbee_attribute(
|
||||||
|
list(entity_ref.cluster_listeners.values())[
|
||||||
|
0].cluster.endpoint.endpoint_id,
|
||||||
cluster_id,
|
cluster_id,
|
||||||
attribute,
|
attribute,
|
||||||
value,
|
value,
|
||||||
@ -292,11 +184,13 @@ def async_load_api(hass, application_controller, listener):
|
|||||||
command_type = service.data.get(ATTR_COMMAND_TYPE)
|
command_type = service.data.get(ATTR_COMMAND_TYPE)
|
||||||
args = service.data.get(ATTR_ARGS)
|
args = service.data.get(ATTR_ARGS)
|
||||||
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
||||||
component = hass.data.get(entity_id.split('.')[0])
|
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||||
entity = component.get_entity(entity_id)
|
zha_device = entity_ref.zha_device
|
||||||
response = None
|
response = None
|
||||||
if entity is not None:
|
if entity_ref is not None:
|
||||||
response = await entity.issue_cluster_command(
|
response = await zha_device.issue_cluster_command(
|
||||||
|
list(entity_ref.cluster_listeners.values())[
|
||||||
|
0].cluster.endpoint.endpoint_id,
|
||||||
cluster_id,
|
cluster_id,
|
||||||
command,
|
command,
|
||||||
command_type,
|
command_type,
|
||||||
@ -325,11 +219,9 @@ def async_load_api(hass, application_controller, listener):
|
|||||||
async def websocket_reconfigure_node(hass, connection, msg):
|
async def websocket_reconfigure_node(hass, connection, msg):
|
||||||
"""Reconfigure a ZHA nodes entities by its ieee address."""
|
"""Reconfigure a ZHA nodes entities by its ieee address."""
|
||||||
ieee = msg[ATTR_IEEE]
|
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)
|
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
|
||||||
for entity in entities:
|
hass.async_create_task(device.async_configure())
|
||||||
if hasattr(entity, 'async_configure'):
|
|
||||||
hass.async_create_task(entity.async_configure())
|
|
||||||
|
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_RECONFIGURE_NODE, websocket_reconfigure_node,
|
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):
|
async def websocket_entities_by_ieee(hass, connection, msg):
|
||||||
"""Return a dict of all zha entities grouped by ieee."""
|
"""Return a dict of all zha entities grouped by ieee."""
|
||||||
entities_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)
|
ieee_string = str(ieee)
|
||||||
entities_by_ieee[ieee_string] = []
|
entities_by_ieee[ieee_string] = []
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
if not isinstance(entity, ZhaDeviceEntity):
|
entities_by_ieee[ieee_string].append({
|
||||||
entities_by_ieee[ieee_string].append({
|
ATTR_ENTITY_ID: entity.reference_id,
|
||||||
ATTR_ENTITY_ID: entity.entity_id,
|
DEVICE_INFO: entity.device_info
|
||||||
DEVICE_INFO: entity.device_info
|
})
|
||||||
})
|
|
||||||
connection.send_message(websocket_api.result_message(
|
connection.send_message(websocket_api.result_message(
|
||||||
msg[ID],
|
msg[ID],
|
||||||
entities_by_ieee
|
entities_by_ieee
|
||||||
@ -363,24 +255,25 @@ def async_load_api(hass, application_controller, listener):
|
|||||||
async def websocket_entity_clusters(hass, connection, msg):
|
async def websocket_entity_clusters(hass, connection, msg):
|
||||||
"""Return a list of entity clusters."""
|
"""Return a list of entity clusters."""
|
||||||
entity_id = msg[ATTR_ENTITY_ID]
|
entity_id = msg[ATTR_ENTITY_ID]
|
||||||
entities = listener.get_entities_for_ieee(msg[ATTR_IEEE])
|
entity_ref = zha_gateway.get_entity_reference(entity_id)
|
||||||
entity = next(
|
|
||||||
ent for ent in entities if ent.entity_id == entity_id)
|
|
||||||
entity_clusters = await entity.get_clusters()
|
|
||||||
clusters = []
|
clusters = []
|
||||||
|
if entity_ref is not None:
|
||||||
for cluster_id, cluster in entity_clusters[IN].items():
|
for listener in entity_ref.cluster_listeners.values():
|
||||||
clusters.append({
|
cluster = listener.cluster
|
||||||
TYPE: IN,
|
in_clusters = cluster.endpoint.in_clusters.values()
|
||||||
ID: cluster_id,
|
out_clusters = cluster.endpoint.out_clusters.values()
|
||||||
NAME: cluster.__class__.__name__
|
if cluster in in_clusters:
|
||||||
})
|
clusters.append({
|
||||||
for cluster_id, cluster in entity_clusters[OUT].items():
|
TYPE: IN,
|
||||||
clusters.append({
|
ID: cluster.cluster_id,
|
||||||
TYPE: OUT,
|
NAME: cluster.__class__.__name__
|
||||||
ID: 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(
|
connection.send_message(websocket_api.result_message(
|
||||||
msg[ID],
|
msg[ID],
|
||||||
@ -392,16 +285,141 @@ def async_load_api(hass, application_controller, listener):
|
|||||||
SCHEMA_WS_CLUSTERS
|
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(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes,
|
WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes,
|
||||||
SCHEMA_WS_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(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands,
|
WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands,
|
||||||
SCHEMA_WS_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(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes,
|
WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes,
|
||||||
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE
|
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE
|
||||||
|
@ -7,16 +7,13 @@ at https://home-assistant.io/components/binary_sensor.zha/
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
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.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
|
||||||
from .core import helpers
|
|
||||||
from .core.const import (
|
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 .entity import ZhaEntity
|
||||||
from .core.listeners import (
|
|
||||||
OnOffListener, LevelListener
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -31,7 +28,20 @@ CLASS_MAPPING = {
|
|||||||
0x002b: 'gas',
|
0x002b: 'gas',
|
||||||
0x002d: 'vibration',
|
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,
|
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,
|
async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||||
discovery_infos):
|
discovery_infos):
|
||||||
"""Set up the ZHA binary sensors."""
|
"""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 = []
|
entities = []
|
||||||
for discovery_info in discovery_infos:
|
for discovery_info in discovery_infos:
|
||||||
if IasZone.cluster_id in discovery_info['in_clusters']:
|
entities.append(BinarySensor(**discovery_info))
|
||||||
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))
|
|
||||||
|
|
||||||
async_add_entities(entities, update_before_add=True)
|
async_add_entities(entities, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
async def _async_setup_iaszone(discovery_info):
|
class BinarySensor(ZhaEntity, BinarySensorDevice):
|
||||||
device_class = None
|
"""ZHA BinarySensor."""
|
||||||
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."""
|
|
||||||
|
|
||||||
_domain = DOMAIN
|
_domain = DOMAIN
|
||||||
_device_class = None
|
_device_class = None
|
||||||
value_attribute = 0
|
|
||||||
|
|
||||||
def __init__(self, device_class, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Initialize the ZHA binary sensor."""
|
"""Initialize the ZHA binary sensor."""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._device_class = device_class
|
self._device_state_attributes = {}
|
||||||
self._cluster = list(kwargs['in_clusters'].values())[0]
|
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):
|
async def _determine_device_class(self):
|
||||||
"""Handle attribute update from device."""
|
"""Determine the device class for this binary sensor."""
|
||||||
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
|
device_class_supplier = DEVICE_CLASS_REGISTRY.get(
|
||||||
if attribute == self.value_attribute:
|
self._zha_sensor_type)
|
||||||
self._state = bool(value)
|
if callable(device_class_supplier):
|
||||||
self.async_schedule_update_ha_state()
|
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):
|
async def async_added_to_hass(self):
|
||||||
"""Run when about to be added to hass."""
|
"""Run when about to be added to hass."""
|
||||||
|
self._device_class = await self._determine_device_class()
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
old_state = await self.async_get_last_state()
|
if self._level_listener:
|
||||||
if self._state is not None or old_state is None:
|
await self.async_accept_signal(
|
||||||
return
|
self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
|
||||||
|
await self.async_accept_signal(
|
||||||
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
|
self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level)
|
||||||
self._state = old_state.state == STATE_ON
|
if self._on_off_listener:
|
||||||
|
await self.async_accept_signal(
|
||||||
@property
|
self._on_off_listener, SIGNAL_ATTR_UPDATED,
|
||||||
def cluster(self):
|
self.async_set_state)
|
||||||
"""Zigbee cluster for this entity."""
|
if self._zone_listener:
|
||||||
return self._cluster
|
await self.async_accept_signal(
|
||||||
|
self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||||
@property
|
if self._attr_listener:
|
||||||
def zcl_reporting_config(self):
|
await self.async_accept_signal(
|
||||||
"""ZHA reporting configuration."""
|
self._attr_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||||
return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -315,3 +136,32 @@ class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
|
|||||||
def device_class(self) -> str:
|
def device_class(self) -> str:
|
||||||
"""Return device class from component DEVICE_CLASSES."""
|
"""Return device class from component DEVICE_CLASSES."""
|
||||||
return self._device_class
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/zha/
|
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'
|
MODEL = 'model'
|
||||||
NAME = 'name'
|
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_BATTERY = 'battery'
|
||||||
|
LISTENER_EVENT_RELAY = 'event_relay'
|
||||||
|
|
||||||
SIGNAL_ATTR_UPDATED = 'attribute_updated'
|
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_AVAILABLE = 'available'
|
||||||
|
SIGNAL_REMOVE = 'remove'
|
||||||
|
|
||||||
|
|
||||||
class RadioType(enum.Enum):
|
class RadioType(enum.Enum):
|
||||||
@ -78,9 +106,10 @@ DISCOVERY_KEY = 'zha_discovery_info'
|
|||||||
DEVICE_CLASS = {}
|
DEVICE_CLASS = {}
|
||||||
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
|
CLUSTER_REPORT_CONFIGS = {}
|
||||||
CUSTOM_CLUSTER_MAPPINGS = {}
|
CUSTOM_CLUSTER_MAPPINGS = {}
|
||||||
COMPONENT_CLUSTERS = {}
|
COMPONENT_CLUSTERS = {}
|
||||||
EVENTABLE_CLUSTERS = []
|
EVENT_RELAY_CLUSTERS = []
|
||||||
|
|
||||||
REPORT_CONFIG_MAX_INT = 900
|
REPORT_CONFIG_MAX_INT = 900
|
||||||
REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800
|
REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800
|
||||||
|
@ -14,7 +14,7 @@ from .const import (
|
|||||||
ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT,
|
ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT,
|
||||||
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
|
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
|
||||||
ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS,
|
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
|
from .listeners import EventRelayListener
|
||||||
|
|
||||||
@ -30,11 +30,14 @@ class ZHADevice:
|
|||||||
self._zigpy_device = zigpy_device
|
self._zigpy_device = zigpy_device
|
||||||
# Get first non ZDO endpoint id to use to get manufacturer and model
|
# Get first non ZDO endpoint id to use to get manufacturer and model
|
||||||
endpoint_ids = zigpy_device.endpoints.keys()
|
endpoint_ids = zigpy_device.endpoints.keys()
|
||||||
ept_id = next(ept_id for ept_id in endpoint_ids if ept_id != 0)
|
self._manufacturer = UNKNOWN
|
||||||
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer
|
self._model = UNKNOWN
|
||||||
self._model = zigpy_device.endpoints[ept_id].model
|
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._zha_gateway = zha_gateway
|
||||||
self._cluster_listeners = {}
|
self.cluster_listeners = {}
|
||||||
self._relay_listeners = []
|
self._relay_listeners = []
|
||||||
self._all_listeners = []
|
self._all_listeners = []
|
||||||
self._name = "{} {}".format(
|
self._name = "{} {}".format(
|
||||||
@ -101,21 +104,11 @@ class ZHADevice:
|
|||||||
"""Return the gateway for this device."""
|
"""Return the gateway for this device."""
|
||||||
return self._zha_gateway
|
return self._zha_gateway
|
||||||
|
|
||||||
@property
|
|
||||||
def cluster_listeners(self):
|
|
||||||
"""Return cluster listeners for device."""
|
|
||||||
return self._cluster_listeners.values()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_listeners(self):
|
def all_listeners(self):
|
||||||
"""Return cluster listeners and relay listeners for device."""
|
"""Return cluster listeners and relay listeners for device."""
|
||||||
return self._all_listeners
|
return self._all_listeners
|
||||||
|
|
||||||
@property
|
|
||||||
def cluster_listener_keys(self):
|
|
||||||
"""Return cluster listeners for device."""
|
|
||||||
return self._cluster_listeners.keys()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_signal(self):
|
def available_signal(self):
|
||||||
"""Signal to use to subscribe to device availability changes."""
|
"""Signal to use to subscribe to device availability changes."""
|
||||||
@ -157,17 +150,13 @@ class ZHADevice:
|
|||||||
"""Add cluster listener to device."""
|
"""Add cluster listener to device."""
|
||||||
# only keep 1 power listener
|
# only keep 1 power listener
|
||||||
if cluster_listener.name is LISTENER_BATTERY and \
|
if cluster_listener.name is LISTENER_BATTERY and \
|
||||||
LISTENER_BATTERY in self._cluster_listeners:
|
LISTENER_BATTERY in self.cluster_listeners:
|
||||||
return
|
return
|
||||||
self._all_listeners.append(cluster_listener)
|
self._all_listeners.append(cluster_listener)
|
||||||
if isinstance(cluster_listener, EventRelayListener):
|
if isinstance(cluster_listener, EventRelayListener):
|
||||||
self._relay_listeners.append(cluster_listener)
|
self._relay_listeners.append(cluster_listener)
|
||||||
else:
|
else:
|
||||||
self._cluster_listeners[cluster_listener.name] = cluster_listener
|
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)
|
|
||||||
|
|
||||||
async def async_configure(self):
|
async def async_configure(self):
|
||||||
"""Configure the device."""
|
"""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/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import collections
|
import collections
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from homeassistant import const as ha_const
|
from homeassistant import const as ha_const
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
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 . import const as zha_const
|
||||||
from .const import (
|
from .const import (
|
||||||
COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN,
|
COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN,
|
||||||
ZHA_DISCOVERY_NEW, EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, DEVICE_CLASS,
|
ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
|
||||||
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
|
||||||
CUSTOM_CLUSTER_MAPPINGS, COMPONENT_CLUSTERS)
|
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 ..device_entity import ZhaDeviceEntity
|
||||||
from ..event import ZhaEvent, ZhaRelayEvent
|
from .listeners import (
|
||||||
|
LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener)
|
||||||
from .helpers import convert_ieee
|
from .helpers import convert_ieee
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SENSOR_TYPES = {}
|
||||||
|
BINARY_SENSOR_TYPES = {}
|
||||||
|
EntityReference = collections.namedtuple(
|
||||||
|
'EntityReference', 'reference_id zha_device cluster_listeners device_info')
|
||||||
|
|
||||||
|
|
||||||
class ZHAGateway:
|
class ZHAGateway:
|
||||||
"""Gateway that handles events that happen on the ZHA Zigbee network."""
|
"""Gateway that handles events that happen on the ZHA Zigbee network."""
|
||||||
@ -31,16 +45,9 @@ class ZHAGateway:
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._config = config
|
self._config = config
|
||||||
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
self._devices = {}
|
||||||
self._device_registry = collections.defaultdict(list)
|
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_COMPONENT] = self._component
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events
|
|
||||||
|
|
||||||
def device_joined(self, device):
|
def device_joined(self, device):
|
||||||
"""Handle device joined.
|
"""Handle device joined.
|
||||||
@ -67,197 +74,310 @@ class ZHAGateway:
|
|||||||
|
|
||||||
def device_removed(self, device):
|
def device_removed(self, device):
|
||||||
"""Handle device being removed from the network."""
|
"""Handle device being removed from the network."""
|
||||||
for device_entity in self._device_registry[device.ieee]:
|
device = self._devices.pop(device.ieee, None)
|
||||||
self._hass.async_create_task(device_entity.async_remove())
|
self._device_registry.pop(device.ieee, None)
|
||||||
if device.ieee in self._events:
|
if device is not None:
|
||||||
self._events.pop(device.ieee)
|
self._hass.async_create_task(device.async_unsub_dispatcher())
|
||||||
|
|
||||||
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:
|
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self._hass,
|
self._hass,
|
||||||
ZHA_DISCOVERY_NEW.format(component),
|
"{}_{}".format(SIGNAL_REMOVE, str(device.ieee))
|
||||||
discovery_info
|
|
||||||
)
|
)
|
||||||
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():
|
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
|
These cannot be module level, as importing bellows must be done in a
|
||||||
in a function.
|
in a function.
|
||||||
"""
|
"""
|
||||||
from zigpy import zcl, quirks
|
from zigpy import zcl
|
||||||
from zigpy.profiles import PROFILES, zha, zll
|
from zigpy.profiles import PROFILES, zha, zll
|
||||||
from ..sensor import RelativeHumiditySensor
|
|
||||||
|
|
||||||
if zha.PROFILE_ID not in DEVICE_CLASS:
|
if zha.PROFILE_ID not in DEVICE_CLASS:
|
||||||
DEVICE_CLASS[zha.PROFILE_ID] = {}
|
DEVICE_CLASS[zha.PROFILE_ID] = {}
|
||||||
if zll.PROFILE_ID not in DEVICE_CLASS:
|
if zll.PROFILE_ID not in DEVICE_CLASS:
|
||||||
DEVICE_CLASS[zll.PROFILE_ID] = {}
|
DEVICE_CLASS[zll.PROFILE_ID] = {}
|
||||||
|
|
||||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id)
|
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
|
||||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
|
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
||||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id)
|
|
||||||
EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
|
||||||
|
|
||||||
DEVICE_CLASS[zha.PROFILE_ID].update({
|
DEVICE_CLASS[zha.PROFILE_ID].update({
|
||||||
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
|
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
|
||||||
@ -293,6 +410,7 @@ def establish_device_mappings():
|
|||||||
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
|
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
|
||||||
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
|
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
|
||||||
})
|
})
|
||||||
|
|
||||||
DEVICE_CLASS[zll.PROFILE_ID].update({
|
DEVICE_CLASS[zll.PROFILE_ID].update({
|
||||||
zll.DeviceType.ON_OFF_LIGHT: 'light',
|
zll.DeviceType.ON_OFF_LIGHT: 'light',
|
||||||
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
|
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
|
||||||
@ -321,14 +439,97 @@ def establish_device_mappings():
|
|||||||
zcl.clusters.measurement.OccupancySensing: 'binary_sensor',
|
zcl.clusters.measurement.OccupancySensing: 'binary_sensor',
|
||||||
zcl.clusters.hvac.Fan: 'fan',
|
zcl.clusters.hvac.Fan: 'fan',
|
||||||
})
|
})
|
||||||
|
|
||||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
|
||||||
zcl.clusters.general.OnOff: 'binary_sensor',
|
zcl.clusters.general.OnOff: 'binary_sensor',
|
||||||
})
|
})
|
||||||
|
|
||||||
# A map of device/cluster to component/sub-component
|
SENSOR_TYPES.update({
|
||||||
CUSTOM_CLUSTER_MAPPINGS.update({
|
zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY,
|
||||||
(quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581):
|
zcl.clusters.measurement.TemperatureMeasurement.cluster_id:
|
||||||
('sensor', RelativeHumiditySensor)
|
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
|
# 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/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from enum import Enum
|
||||||
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
|
from random import uniform
|
||||||
|
|
||||||
from homeassistant.core import callback
|
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__)
|
_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."""
|
"""Parse and log a zigbee cluster command."""
|
||||||
cmd = cluster.server_commands.get(command_id, [command_id])[0]
|
cmd = cluster.server_commands.get(command_id, [command_id])[0]
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'",
|
"%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'",
|
||||||
entity_id,
|
unique_id,
|
||||||
cmd,
|
cmd,
|
||||||
args,
|
args,
|
||||||
cluster.cluster_id,
|
cluster.cluster_id,
|
||||||
@ -27,40 +55,214 @@ def parse_and_log_command(entity_id, cluster, tsn, command_id, args):
|
|||||||
return cmd
|
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:
|
class ClusterListener:
|
||||||
"""Listener for a Zigbee cluster."""
|
"""Listener for a Zigbee cluster."""
|
||||||
|
|
||||||
def __init__(self, entity, cluster):
|
def __init__(self, cluster, device):
|
||||||
"""Initialize ClusterListener."""
|
"""Initialize ClusterListener."""
|
||||||
self._entity = entity
|
|
||||||
self._cluster = cluster
|
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):
|
def cluster_command(self, tsn, command_id, args):
|
||||||
"""Handle commands received to this cluster."""
|
"""Handle commands received to this cluster."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@callback
|
||||||
def attribute_updated(self, attrid, value):
|
def attribute_updated(self, attrid, value):
|
||||||
"""Handle attribute updates on this cluster."""
|
"""Handle attribute updates on this cluster."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@callback
|
||||||
def zdo_command(self, *args, **kwargs):
|
def zdo_command(self, *args, **kwargs):
|
||||||
"""Handle ZDO commands on this cluster."""
|
"""Handle ZDO commands on this cluster."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@callback
|
||||||
def zha_send_event(self, cluster, command, args):
|
def zha_send_event(self, cluster, command, args):
|
||||||
"""Relay entity events to hass."""
|
"""Relay events to hass."""
|
||||||
pass # don't let entities fire events
|
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):
|
class OnOffListener(ClusterListener):
|
||||||
"""Listener for the OnOff Zigbee cluster."""
|
"""Listener for the OnOff Zigbee cluster."""
|
||||||
|
|
||||||
|
name = 'on_off'
|
||||||
|
|
||||||
ON_OFF = 0
|
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):
|
def cluster_command(self, tsn, command_id, args):
|
||||||
"""Handle commands received to this cluster."""
|
"""Handle commands received to this cluster."""
|
||||||
cmd = parse_and_log_command(
|
cmd = parse_and_log_command(
|
||||||
self._entity.entity_id,
|
self.unique_id,
|
||||||
self._cluster,
|
self._cluster,
|
||||||
tsn,
|
tsn,
|
||||||
command_id,
|
command_id,
|
||||||
@ -68,27 +270,42 @@ class OnOffListener(ClusterListener):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if cmd in ('off', 'off_with_effect'):
|
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'):
|
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':
|
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):
|
def attribute_updated(self, attrid, value):
|
||||||
"""Handle attribute updates on this cluster."""
|
"""Handle attribute updates on this cluster."""
|
||||||
if attrid == self.ON_OFF:
|
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):
|
class LevelListener(ClusterListener):
|
||||||
"""Listener for the LevelControl Zigbee cluster."""
|
"""Listener for the LevelControl Zigbee cluster."""
|
||||||
|
|
||||||
|
name = ATTR_LEVEL
|
||||||
|
|
||||||
CURRENT_LEVEL = 0
|
CURRENT_LEVEL = 0
|
||||||
|
|
||||||
|
@callback
|
||||||
def cluster_command(self, tsn, command_id, args):
|
def cluster_command(self, tsn, command_id, args):
|
||||||
"""Handle commands received to this cluster."""
|
"""Handle commands received to this cluster."""
|
||||||
cmd = parse_and_log_command(
|
cmd = parse_and_log_command(
|
||||||
self._entity.entity_id,
|
self.unique_id,
|
||||||
self._cluster,
|
self._cluster,
|
||||||
tsn,
|
tsn,
|
||||||
command_id,
|
command_id,
|
||||||
@ -96,21 +313,190 @@ class LevelListener(ClusterListener):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if cmd in ('move_to_level', 'move_to_level_with_on_off'):
|
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'):
|
elif cmd in ('move', 'move_with_on_off'):
|
||||||
# We should dim slowly -- for now, just step once
|
# We should dim slowly -- for now, just step once
|
||||||
rate = args[1]
|
rate = args[1]
|
||||||
if args[0] == 0xff:
|
if args[0] == 0xff:
|
||||||
rate = 10 # Should read default move rate
|
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'):
|
elif cmd in ('step', 'step_with_on_off'):
|
||||||
# Step (technically may change 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):
|
def attribute_updated(self, attrid, value):
|
||||||
"""Handle attribute updates on this cluster."""
|
"""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:
|
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):
|
class EventRelayListener(ClusterListener):
|
||||||
@ -143,3 +529,137 @@ class EventRelayListener(ClusterListener):
|
|||||||
self._cluster.server_commands.get(command_id)[0],
|
self._cluster.server_commands.get(command_id)[0],
|
||||||
args
|
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/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from homeassistant.helpers import entity
|
|
||||||
from homeassistant.util import slugify
|
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."""
|
"""A base class for ZHA devices."""
|
||||||
|
|
||||||
def __init__(self, device, manufacturer, model, application_listener,
|
def __init__(self, zha_device, listeners, keepalive_interval=7200,
|
||||||
keepalive_interval=7200, **kwargs):
|
**kwargs):
|
||||||
"""Init ZHA endpoint entity."""
|
"""Init ZHA endpoint entity."""
|
||||||
self._device_state_attributes = {
|
ieee = zha_device.ieee
|
||||||
'nwk': '0x{0:04x}'.format(device.nwk),
|
|
||||||
'ieee': str(device.ieee),
|
|
||||||
'lqi': device.lqi,
|
|
||||||
'rssi': device.rssi,
|
|
||||||
}
|
|
||||||
|
|
||||||
ieee = device.ieee
|
|
||||||
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
||||||
if manufacturer is not None and model is not None:
|
unique_id = None
|
||||||
self._unique_id = "{}_{}_{}".format(
|
if zha_device.manufacturer is not None and \
|
||||||
slugify(manufacturer),
|
zha_device.model is not None:
|
||||||
slugify(model),
|
unique_id = "{}_{}_{}".format(
|
||||||
|
slugify(zha_device.manufacturer),
|
||||||
|
slugify(zha_device.model),
|
||||||
ieeetail,
|
ieeetail,
|
||||||
)
|
)
|
||||||
self._device_state_attributes['friendly_name'] = "{} {}".format(
|
|
||||||
manufacturer,
|
|
||||||
model,
|
|
||||||
)
|
|
||||||
else:
|
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
|
self._keepalive_interval = keepalive_interval
|
||||||
|
self._device_state_attributes.update({
|
||||||
application_listener.register_entity(ieee, self)
|
'nwk': '0x{0:04x}'.format(zha_device.nwk),
|
||||||
|
'ieee': str(zha_device.ieee),
|
||||||
@property
|
'lqi': zha_device.lqi,
|
||||||
def unique_id(self) -> str:
|
'rssi': zha_device.rssi,
|
||||||
"""Return a unique ID."""
|
})
|
||||||
return self._unique_id
|
self._should_poll = True
|
||||||
|
self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if device is available."""
|
||||||
|
return self._zha_device.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
update_time = None
|
update_time = None
|
||||||
if self._device.last_seen is not None and self._state == 'offline':
|
device = self._zha_device
|
||||||
time_struct = time.localtime(self._device.last_seen)
|
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)
|
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
|
||||||
self._device_state_attributes['last_seen'] = update_time
|
self._device_state_attributes['last_seen'] = update_time
|
||||||
if ('last_seen' in self._device_state_attributes and
|
if ('last_seen' in self._device_state_attributes and
|
||||||
self._state != 'offline'):
|
self.available):
|
||||||
del self._device_state_attributes['last_seen']
|
del self._device_state_attributes['last_seen']
|
||||||
self._device_state_attributes['lqi'] = self._device.lqi
|
self._device_state_attributes['lqi'] = device.lqi
|
||||||
self._device_state_attributes['rssi'] = self._device.rssi
|
self._device_state_attributes['rssi'] = device.rssi
|
||||||
return self._device_state_attributes
|
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):
|
async def async_update(self):
|
||||||
"""Handle polling."""
|
"""Handle polling."""
|
||||||
if self._device.last_seen is None:
|
if self._zha_device.last_seen is None:
|
||||||
self._state = 'offline'
|
self._zha_device.update_available(False)
|
||||||
else:
|
else:
|
||||||
difference = time.time() - self._device.last_seen
|
difference = time.time() - self._zha_device.last_seen
|
||||||
if difference > self._keepalive_interval:
|
if difference > self._keepalive_interval:
|
||||||
self._state = 'offline'
|
self._zha_device.update_available(False)
|
||||||
|
self._state = None
|
||||||
else:
|
else:
|
||||||
|
self._zha_device.update_available(True)
|
||||||
self._state = 'online'
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/zha/
|
https://home-assistant.io/components/zha/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from random import uniform
|
|
||||||
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
import logging
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers import entity
|
from homeassistant.helpers import entity
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .core.const import (
|
from .core.const import (
|
||||||
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE,
|
DOMAIN, ATTR_MANUFACTURER, DATA_ZHA, DATA_ZHA_BRIDGE_ID, MODEL, NAME,
|
||||||
ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE,
|
SIGNAL_REMOVE
|
||||||
ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS)
|
)
|
||||||
from .core.helpers import bind_configure_reporting
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,287 +27,155 @@ class ZhaEntity(entity.Entity):
|
|||||||
|
|
||||||
_domain = None # Must be overridden by subclasses
|
_domain = None # Must be overridden by subclasses
|
||||||
|
|
||||||
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
|
def __init__(self, unique_id, zha_device, listeners,
|
||||||
model, application_listener, unique_id, new_join=False,
|
skip_entity_id=False, **kwargs):
|
||||||
**kwargs):
|
|
||||||
"""Init ZHA entity."""
|
"""Init ZHA entity."""
|
||||||
self._device_state_attributes = {}
|
self._force_update = False
|
||||||
self._name = None
|
self._should_poll = False
|
||||||
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._unique_id = unique_id
|
self._unique_id = unique_id
|
||||||
|
self._name = None
|
||||||
# Normally the entity itself is the listener. Sub-classes may set this
|
if zha_device.manufacturer and zha_device.model is not None:
|
||||||
# to a dict of cluster ID -> listener to receive messages for specific
|
self._name = "{} {}".format(
|
||||||
# clusters separately
|
zha_device.manufacturer,
|
||||||
self._in_listeners = {}
|
zha_device.model
|
||||||
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
|
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
if not skip_entity_id:
|
||||||
'set: %s for attr: %s to cluster: %s for entity: %s - res: %s',
|
ieee = zha_device.ieee
|
||||||
value,
|
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
||||||
attribute,
|
if zha_device.manufacturer and zha_device.model is not None:
|
||||||
cluster_id,
|
self.entity_id = "{}.{}_{}_{}_{}{}".format(
|
||||||
self.entity_id,
|
self._domain,
|
||||||
response
|
slugify(zha_device.manufacturer),
|
||||||
)
|
slugify(zha_device.model),
|
||||||
return response
|
ieeetail,
|
||||||
except DeliveryError as exc:
|
listeners[0].cluster.endpoint.endpoint_id,
|
||||||
_LOGGER.debug(
|
kwargs.get(ENTITY_SUFFIX, ''),
|
||||||
'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
|
|
||||||
)
|
)
|
||||||
skip_bind = True
|
else:
|
||||||
await asyncio.sleep(uniform(0.1, 0.5))
|
self.entity_id = "{}.zha_{}_{}{}".format(
|
||||||
_LOGGER.debug("%s: finished configuration", self.entity_id)
|
self._domain,
|
||||||
|
ieeetail,
|
||||||
def _get_cluster_from_report_config(self, cluster_key):
|
listeners[0].cluster.endpoint.endpoint_id,
|
||||||
"""Parse an entry from zcl_reporting_config dict."""
|
kwargs.get(ENTITY_SUFFIX, ''),
|
||||||
from zigpy.zcl import Cluster as Zcl_Cluster
|
)
|
||||||
|
self._state = None
|
||||||
cluster = None
|
self._device_state_attributes = {}
|
||||||
if isinstance(cluster_key, Zcl_Cluster):
|
self._zha_device = zha_device
|
||||||
cluster = cluster_key
|
self.cluster_listeners = {}
|
||||||
elif isinstance(cluster_key, str):
|
# this will get flipped to false once we enable the feature after the
|
||||||
cluster = getattr(self._endpoint, cluster_key, None)
|
# reorg is merged
|
||||||
elif isinstance(cluster_key, int):
|
self._available = True
|
||||||
if cluster_key in self._in_clusters:
|
self._component = kwargs['component']
|
||||||
cluster = self._in_clusters[cluster_key]
|
self._unsubs = []
|
||||||
elif cluster_key in self._out_clusters:
|
for listener in listeners:
|
||||||
cluster = self._out_clusters[cluster_key]
|
self.cluster_listeners[listener.name] = listener
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return Entity's default name."""
|
"""Return Entity's default name."""
|
||||||
return self._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
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
return self._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
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
return self._device_state_attributes
|
return self._device_state_attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def force_update(self) -> bool:
|
||||||
|
"""Force update this entity."""
|
||||||
|
return self._force_update
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
"""Let ZHA handle polling."""
|
"""Poll state from device."""
|
||||||
return False
|
return self._should_poll
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device registry."""
|
"""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 {
|
return {
|
||||||
'connections': {(CONNECTION_ZIGBEE, ieee)},
|
'connections': {(CONNECTION_ZIGBEE, ieee)},
|
||||||
'identifiers': {(DOMAIN, ieee)},
|
'identifiers': {(DOMAIN, ieee)},
|
||||||
ATTR_MANUFACTURER: self._endpoint.manufacturer,
|
ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER],
|
||||||
'model': self._endpoint.model,
|
MODEL: zha_device_info[MODEL],
|
||||||
'name': self.name or ieee,
|
NAME: zha_device_info[NAME],
|
||||||
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
|
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
|
||||||
}
|
}
|
||||||
|
|
||||||
@callback
|
@property
|
||||||
def zha_send_event(self, cluster, command, args):
|
def available(self):
|
||||||
"""Relay entity events to hass."""
|
"""Return entity availability."""
|
||||||
pass # don't relay events from entities
|
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,
|
DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
|
||||||
FanEntity)
|
FanEntity)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from .core import helpers
|
|
||||||
from .core.const import (
|
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
|
from .entity import ZhaEntity
|
||||||
|
|
||||||
DEPENDENCIES = ['zha']
|
DEPENDENCIES = ['zha']
|
||||||
@ -79,19 +80,17 @@ class ZhaFan(ZhaEntity, FanEntity):
|
|||||||
"""Representation of a ZHA fan."""
|
"""Representation of a ZHA fan."""
|
||||||
|
|
||||||
_domain = DOMAIN
|
_domain = DOMAIN
|
||||||
value_attribute = 0 # fan_mode
|
|
||||||
|
|
||||||
@property
|
def __init__(self, unique_id, zha_device, listeners, **kwargs):
|
||||||
def zcl_reporting_config(self) -> dict:
|
"""Init this sensor."""
|
||||||
"""Return a dict of attribute reporting configuration."""
|
super().__init__(unique_id, zha_device, listeners, **kwargs)
|
||||||
return {
|
self._fan_listener = self.cluster_listeners.get(LISTENER_FAN)
|
||||||
self.cluster: {self.value_attribute: REPORT_CONFIG_OP}
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
async def async_added_to_hass(self):
|
||||||
def cluster(self):
|
"""Run when about to be added to hass."""
|
||||||
"""Fan ZCL Cluster."""
|
await super().async_added_to_hass()
|
||||||
return self._endpoint.fan
|
await self.async_accept_signal(
|
||||||
|
self._fan_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
@ -115,6 +114,16 @@ class ZhaFan(ZhaEntity, FanEntity):
|
|||||||
return False
|
return False
|
||||||
return self._state != SPEED_OFF
|
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:
|
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
if speed is None:
|
if speed is None:
|
||||||
@ -128,31 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity):
|
|||||||
|
|
||||||
async def async_set_speed(self, speed: str) -> None:
|
async def async_set_speed(self, speed: str) -> None:
|
||||||
"""Set the speed of the fan."""
|
"""Set the speed of the fan."""
|
||||||
from zigpy.exceptions import DeliveryError
|
await self._fan_listener.async_set_speed(SPEED_TO_VALUE[speed])
|
||||||
try:
|
self.async_set_state(speed)
|
||||||
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()
|
|
||||||
|
@ -9,14 +9,12 @@ import logging
|
|||||||
from homeassistant.components import light
|
from homeassistant.components import light
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
from .core import helpers
|
from .const import (
|
||||||
from .core.const import (
|
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR,
|
||||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT,
|
LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL
|
||||||
REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
)
|
||||||
from .entity import ZhaEntity
|
from .entity import ZhaEntity
|
||||||
from .core.listeners import (
|
|
||||||
OnOffListener, LevelListener
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -58,26 +56,6 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
|
|||||||
"""Set up the ZHA lights."""
|
"""Set up the ZHA lights."""
|
||||||
entities = []
|
entities = []
|
||||||
for discovery_info in discovery_infos:
|
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)
|
zha_light = Light(**discovery_info)
|
||||||
entities.append(zha_light)
|
entities.append(zha_light)
|
||||||
|
|
||||||
@ -89,34 +67,24 @@ class Light(ZhaEntity, light.Light):
|
|||||||
|
|
||||||
_domain = light.DOMAIN
|
_domain = light.DOMAIN
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, unique_id, zha_device, listeners, **kwargs):
|
||||||
"""Initialize the ZHA light."""
|
"""Initialize the ZHA light."""
|
||||||
super().__init__(**kwargs)
|
super().__init__(unique_id, zha_device, listeners, **kwargs)
|
||||||
self._supported_features = 0
|
self._supported_features = 0
|
||||||
self._color_temp = None
|
self._color_temp = None
|
||||||
self._hs_color = None
|
self._hs_color = None
|
||||||
self._brightness = None
|
self._brightness = None
|
||||||
from zigpy.zcl.clusters.general import OnOff, LevelControl
|
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
|
||||||
self._in_listeners = {
|
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL)
|
||||||
OnOff.cluster_id: OnOffListener(
|
self._color_listener = self.cluster_listeners.get(LISTENER_COLOR)
|
||||||
self,
|
|
||||||
self._in_clusters[OnOff.cluster_id]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if LevelControl.cluster_id in self._in_clusters:
|
if self._level_listener:
|
||||||
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
||||||
self._supported_features |= light.SUPPORT_TRANSITION
|
self._supported_features |= light.SUPPORT_TRANSITION
|
||||||
self._brightness = 0
|
self._brightness = 0
|
||||||
self._in_listeners.update({
|
|
||||||
LevelControl.cluster_id: LevelListener(
|
if self._color_listener:
|
||||||
self,
|
color_capabilities = self._color_listener.get_color_capabilities()
|
||||||
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 color_capabilities & CAPABILITIES_COLOR_TEMP:
|
if color_capabilities & CAPABILITIES_COLOR_TEMP:
|
||||||
self._supported_features |= light.SUPPORT_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._supported_features |= light.SUPPORT_COLOR
|
||||||
self._hs_color = (0, 0)
|
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
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if entity is on."""
|
"""Return true if entity is on."""
|
||||||
if self._state is None:
|
if self._state is None:
|
||||||
return False
|
return False
|
||||||
return bool(self._state)
|
return 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()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Return the brightness of this light between 0..255."""
|
"""Return the brightness of this light."""
|
||||||
return self._brightness
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return state attributes."""
|
||||||
|
return self.state_attributes
|
||||||
|
|
||||||
def set_level(self, value):
|
def set_level(self, value):
|
||||||
"""Set the brightness of this light between 0..255."""
|
"""Set the brightness of this light between 0..255."""
|
||||||
if value < 0 or value > 255:
|
value = max(0, min(255, value))
|
||||||
return
|
|
||||||
self._brightness = value
|
self._brightness = value
|
||||||
self.async_schedule_update_ha_state()
|
self.async_set_state(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hs_color(self):
|
def hs_color(self):
|
||||||
@ -265,40 +130,82 @@ class Light(ZhaEntity, light.Light):
|
|||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
return self._supported_features
|
return self._supported_features
|
||||||
|
|
||||||
async def async_update(self):
|
def async_set_state(self, state):
|
||||||
"""Retrieve latest state."""
|
"""Set the state."""
|
||||||
result = await helpers.safe_read(self._endpoint.on_off, ['on_off'],
|
self._state = bool(state)
|
||||||
allow_cache=False,
|
self.async_schedule_update_ha_state()
|
||||||
only_cache=(not self._initialized))
|
|
||||||
self._state = result.get('on_off', self._state)
|
|
||||||
|
|
||||||
if self._supported_features & light.SUPPORT_BRIGHTNESS:
|
async def async_added_to_hass(self):
|
||||||
result = await helpers.safe_read(self._endpoint.level,
|
"""Run when about to be added to hass."""
|
||||||
['current_level'],
|
await super().async_added_to_hass()
|
||||||
allow_cache=False,
|
await self.async_accept_signal(
|
||||||
only_cache=(
|
self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||||
not self._initialized
|
if self._level_listener:
|
||||||
))
|
await self.async_accept_signal(
|
||||||
self._brightness = result.get('current_level', self._brightness)
|
self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
|
||||||
|
|
||||||
if self._supported_features & light.SUPPORT_COLOR_TEMP:
|
async def async_turn_on(self, **kwargs):
|
||||||
result = await helpers.safe_read(self._endpoint.light_color,
|
"""Turn the entity on."""
|
||||||
['color_temperature'],
|
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
|
||||||
allow_cache=False,
|
duration = duration * 10 # tenths of s
|
||||||
only_cache=(
|
|
||||||
not self._initialized
|
|
||||||
))
|
|
||||||
self._color_temp = result.get('color_temperature',
|
|
||||||
self._color_temp)
|
|
||||||
|
|
||||||
if self._supported_features & light.SUPPORT_COLOR:
|
if light.ATTR_COLOR_TEMP in kwargs and \
|
||||||
result = await helpers.safe_read(self._endpoint.light_color,
|
self.supported_features & light.SUPPORT_COLOR_TEMP:
|
||||||
['current_x', 'current_y'],
|
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||||
allow_cache=False,
|
success = await self._color_listener.move_to_color_temp(
|
||||||
only_cache=(
|
temperature, duration)
|
||||||
not self._initialized
|
if not success:
|
||||||
))
|
return
|
||||||
if 'current_x' in result and 'current_y' in result:
|
self._color_temp = temperature
|
||||||
xy_color = (round(result['current_x']/65535, 3),
|
|
||||||
round(result['current_y']/65535, 3))
|
if light.ATTR_HS_COLOR in kwargs and \
|
||||||
self._hs_color = color_util.color_xy_to_hs(*xy_color)
|
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.components.sensor import DOMAIN
|
||||||
from homeassistant.const import TEMP_CELSIUS
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
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 (
|
from .core.const import (
|
||||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT,
|
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
|
||||||
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW)
|
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
|
from .entity import ZhaEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -21,6 +21,73 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DEPENDENCIES = ['zha']
|
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,
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Old way of setting up Zigbee Home Automation sensors."""
|
"""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):
|
async def make_sensor(discovery_info):
|
||||||
"""Create ZHA sensors factory."""
|
"""Create ZHA sensors factory."""
|
||||||
from zigpy.zcl.clusters.measurement import (
|
return Sensor(**discovery_info)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Sensor(ZhaEntity):
|
class Sensor(ZhaEntity):
|
||||||
"""Base ZHA sensor."""
|
"""Base ZHA sensor."""
|
||||||
|
|
||||||
_domain = DOMAIN
|
_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):
|
def __init__(self, unique_id, zha_device, listeners, **kwargs):
|
||||||
"""Init ZHA Sensor instance."""
|
"""Init this sensor."""
|
||||||
super().__init__(**kwargs)
|
super().__init__(unique_id, zha_device, listeners, **kwargs)
|
||||||
self._cluster = list(kwargs['in_clusters'].values())[0]
|
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
|
@property
|
||||||
def zcl_reporting_config(self) -> dict:
|
def unit_of_measurement(self):
|
||||||
"""Return a dict of attribute reporting configuration."""
|
"""Return the unit of measurement of this entity."""
|
||||||
return {
|
return self._unit
|
||||||
self.cluster: {self.value_attribute: self.report_config}
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cluster(self):
|
|
||||||
"""Return Sensor's cluster."""
|
|
||||||
return self._cluster
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
"""Return the state of the entity."""
|
"""Return the state of the entity."""
|
||||||
|
if self._state is None:
|
||||||
|
return None
|
||||||
if isinstance(self._state, float):
|
if isinstance(self._state, float):
|
||||||
return str(round(self._state, 2))
|
return str(round(self._state, 2))
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
def attribute_updated(self, attribute, value):
|
def async_set_state(self, state):
|
||||||
"""Handle attribute update from device."""
|
"""Handle state update from listener."""
|
||||||
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
|
self._state = self._formatter_function(state)
|
||||||
if attribute == self.value_attribute:
|
self.async_schedule_update_ha_state()
|
||||||
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)
|
|
||||||
|
@ -8,9 +8,10 @@ import logging
|
|||||||
|
|
||||||
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from .core import helpers
|
|
||||||
from .core.const import (
|
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
|
from .entity import ZhaEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -55,69 +56,39 @@ class Switch(ZhaEntity, SwitchDevice):
|
|||||||
"""ZHA switch."""
|
"""ZHA switch."""
|
||||||
|
|
||||||
_domain = DOMAIN
|
_domain = DOMAIN
|
||||||
value_attribute = 0
|
|
||||||
|
|
||||||
def attribute_updated(self, attribute, value):
|
def __init__(self, **kwargs):
|
||||||
"""Handle attribute update from device."""
|
"""Initialize the ZHA switch."""
|
||||||
cluster = self._endpoint.on_off
|
super().__init__(**kwargs)
|
||||||
attr_name = cluster.attributes.get(attribute, [attribute])[0]
|
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
|
||||||
_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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return if the switch is on based on the statemachine."""
|
"""Return if the switch is on based on the statemachine."""
|
||||||
if self._state is None:
|
if self._state is None:
|
||||||
return False
|
return False
|
||||||
return bool(self._state)
|
return self._state
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
async def async_turn_on(self, **kwargs):
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
from zigpy.exceptions import DeliveryError
|
await self._on_off_listener.on()
|
||||||
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()
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs):
|
async def async_turn_off(self, **kwargs):
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
from zigpy.exceptions import DeliveryError
|
await self._on_off_listener.off()
|
||||||
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
|
|
||||||
|
|
||||||
self._state = 0
|
def async_set_state(self, state):
|
||||||
|
"""Handle state update from listener."""
|
||||||
|
self._state = bool(state)
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
async def async_update(self):
|
@property
|
||||||
"""Retrieve latest state."""
|
def device_state_attributes(self):
|
||||||
result = await helpers.safe_read(self.cluster,
|
"""Return state attributes."""
|
||||||
['on_off'],
|
return self.state_attributes
|
||||||
allow_cache=False,
|
|
||||||
only_cache=(not self._initialized))
|
async def async_added_to_hass(self):
|
||||||
self._state = result.get('on_off', self._state)
|
"""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
|
import pytest
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.zha.core.const import (
|
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 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
|
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
|
Create a ZHAGateway object that can be used to interact with as if we
|
||||||
had a real zigbee network running.
|
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, {})
|
return ZHAGateway(hass, {})
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway):
|
|||||||
# load up binary_sensor domain
|
# load up binary_sensor domain
|
||||||
await hass.config_entries.async_forward_entry_setup(
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, DOMAIN)
|
config_entry, DOMAIN)
|
||||||
|
await zha_gateway.accept_zigbee_messages({})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# on off binary_sensor
|
# on off binary_sensor
|
||||||
|
@ -26,6 +26,7 @@ async def test_fan(hass, config_entry, zha_gateway):
|
|||||||
# load up fan domain
|
# load up fan domain
|
||||||
await hass.config_entries.async_forward_entry_setup(
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, DOMAIN)
|
config_entry, DOMAIN)
|
||||||
|
await zha_gateway.accept_zigbee_messages({})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
cluster = zigpy_device.endpoints.get(1).fan
|
cluster = zigpy_device.endpoints.get(1).fan
|
||||||
|
@ -40,6 +40,7 @@ async def test_light(hass, config_entry, zha_gateway):
|
|||||||
# load up light domain
|
# load up light domain
|
||||||
await hass.config_entries.async_forward_entry_setup(
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, DOMAIN)
|
config_entry, DOMAIN)
|
||||||
|
await zha_gateway.accept_zigbee_messages({})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# on off light
|
# on off light
|
||||||
|
@ -92,6 +92,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
|
|||||||
# load up sensor domain
|
# load up sensor domain
|
||||||
await hass.config_entries.async_forward_entry_setup(
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, DOMAIN)
|
config_entry, DOMAIN)
|
||||||
|
await zha_gateway.accept_zigbee_messages({})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# put the other relevant info in the device info dict
|
# 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
|
# load up switch domain
|
||||||
await hass.config_entries.async_forward_entry_setup(
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, DOMAIN)
|
config_entry, DOMAIN)
|
||||||
|
await zha_gateway.accept_zigbee_messages({})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
cluster = zigpy_device.endpoints.get(1).on_off
|
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()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get(entity_id).state == STATE_OFF
|
assert hass.states.get(entity_id).state == STATE_OFF
|
||||||
|
|
||||||
|
# turn on from HA
|
||||||
with patch(
|
with patch(
|
||||||
'zigpy.zcl.Cluster.request',
|
'zigpy.zcl.Cluster.request',
|
||||||
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
|
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(
|
assert cluster.request.call_args == call(
|
||||||
False, ON, (), expect_reply=True, manufacturer=None)
|
False, ON, (), expect_reply=True, manufacturer=None)
|
||||||
|
|
||||||
|
# turn off from HA
|
||||||
with patch(
|
with patch(
|
||||||
'zigpy.zcl.Cluster.request',
|
'zigpy.zcl.Cluster.request',
|
||||||
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
|
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(
|
assert cluster.request.call_args == call(
|
||||||
False, OFF, (), expect_reply=True, manufacturer=None)
|
False, OFF, (), expect_reply=True, manufacturer=None)
|
||||||
|
|
||||||
|
# test joining a new switch to the network and HA
|
||||||
await async_test_device_join(
|
await async_test_device_join(
|
||||||
hass, zha_gateway, OnOff.cluster_id, DOMAIN, expected_state=STATE_OFF)
|
hass, zha_gateway, OnOff.cluster_id, DOMAIN, expected_state=STATE_OFF)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user