Remove ZHA device entity (#24909)
* move availability handling to device * update last_seen format * add battery sensor * fix interval * fix battery reporting now that it is a sensor * remove zha entities and add battery sensor
This commit is contained in:
parent
eec67d8b1a
commit
a9459c6d92
10 changed files with 118 additions and 205 deletions
|
@ -15,8 +15,8 @@ from .core.channels.registry import populate_channel_registry
|
||||||
from .core.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_CONFIG,
|
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_CONFIG,
|
||||||
DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY,
|
DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DEFAULT_BAUDRATE,
|
||||||
DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, ENABLE_QUIRKS, RadioType)
|
DEFAULT_RADIO_TYPE, DOMAIN, ENABLE_QUIRKS, RadioType)
|
||||||
from .core.registries import establish_device_mappings
|
from .core.registries import establish_device_mappings
|
||||||
|
|
||||||
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
||||||
|
@ -147,11 +147,5 @@ async def async_unload_entry(hass, config_entry):
|
||||||
await hass.config_entries.async_forward_entry_unload(
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
config_entry, component)
|
config_entry, component)
|
||||||
|
|
||||||
# clean up device entities
|
|
||||||
component = hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT]
|
|
||||||
entity_ids = [entity.entity_id for entity in component.entities]
|
|
||||||
for entity_id in entity_ids:
|
|
||||||
await component.async_remove_entity(entity_id)
|
|
||||||
|
|
||||||
del hass.data[DATA_ZHA]
|
del hass.data[DATA_ZHA]
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -22,7 +22,6 @@ from ..const import (
|
||||||
)
|
)
|
||||||
from ..registries import CLUSTER_REPORT_CONFIGS
|
from ..registries import CLUSTER_REPORT_CONFIGS
|
||||||
|
|
||||||
ZIGBEE_CHANNEL_REGISTRY = {}
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,7 @@ from homeassistant.helpers.event import async_call_later
|
||||||
from . import ZigbeeChannel, parse_and_log_command
|
from . import ZigbeeChannel, parse_and_log_command
|
||||||
from ..helpers import get_attr_id_by_name
|
from ..helpers import get_attr_id_by_name
|
||||||
from ..const import (
|
from ..const import (
|
||||||
SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL,
|
SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL
|
||||||
SIGNAL_STATE_ATTR
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -202,8 +201,7 @@ class PowerConfigurationChannel(ZigbeeChannel):
|
||||||
if attrid == attr_id:
|
if attrid == attr_id:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self._zha_device.hass,
|
self._zha_device.hass,
|
||||||
"{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR),
|
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
|
||||||
'battery_level',
|
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ DATA_ZHA = 'zha'
|
||||||
DATA_ZHA_CONFIG = 'config'
|
DATA_ZHA_CONFIG = 'config'
|
||||||
DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
|
DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
|
||||||
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
|
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
|
||||||
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
|
|
||||||
DATA_ZHA_CORE_EVENTS = 'zha_core_events'
|
DATA_ZHA_CORE_EVENTS = 'zha_core_events'
|
||||||
DATA_ZHA_GATEWAY = 'zha_gateway'
|
DATA_ZHA_GATEWAY = 'zha_gateway'
|
||||||
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
|
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
|
||||||
|
@ -67,6 +66,9 @@ SERVER = 'server'
|
||||||
IEEE = 'ieee'
|
IEEE = 'ieee'
|
||||||
MODEL = 'model'
|
MODEL = 'model'
|
||||||
NAME = 'name'
|
NAME = 'name'
|
||||||
|
LQI = 'lqi'
|
||||||
|
RSSI = 'rssi'
|
||||||
|
LAST_SEEN = 'last_seen'
|
||||||
|
|
||||||
SENSOR_TYPE = 'sensor_type'
|
SENSOR_TYPE = 'sensor_type'
|
||||||
HUMIDITY = 'humidity'
|
HUMIDITY = 'humidity'
|
||||||
|
@ -76,6 +78,7 @@ PRESSURE = 'pressure'
|
||||||
METERING = 'metering'
|
METERING = 'metering'
|
||||||
ELECTRICAL_MEASUREMENT = 'electrical_measurement'
|
ELECTRICAL_MEASUREMENT = 'electrical_measurement'
|
||||||
GENERIC = 'generic'
|
GENERIC = 'generic'
|
||||||
|
BATTERY = 'battery'
|
||||||
UNKNOWN = 'unknown'
|
UNKNOWN = 'unknown'
|
||||||
UNKNOWN_MANUFACTURER = 'unk_manufacturer'
|
UNKNOWN_MANUFACTURER = 'unk_manufacturer'
|
||||||
UNKNOWN_MODEL = 'unk_model'
|
UNKNOWN_MODEL = 'unk_model'
|
||||||
|
|
|
@ -5,12 +5,15 @@ 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 asyncio
|
||||||
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect, async_dispatcher_send)
|
async_dispatcher_connect, async_dispatcher_send)
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .channels import EventRelayChannel
|
from .channels import EventRelayChannel
|
||||||
from .const import (
|
from .const import (
|
||||||
|
@ -19,9 +22,12 @@ from .const import (
|
||||||
BATTERY_OR_UNKNOWN, CLIENT_COMMANDS, IEEE, IN, MAINS_POWERED,
|
BATTERY_OR_UNKNOWN, CLIENT_COMMANDS, IEEE, IN, MAINS_POWERED,
|
||||||
MANUFACTURER_CODE, MODEL, NAME, NWK, OUT, POWER_CONFIGURATION_CHANNEL,
|
MANUFACTURER_CODE, MODEL, NAME, NWK, OUT, POWER_CONFIGURATION_CHANNEL,
|
||||||
POWER_SOURCE, QUIRK_APPLIED, QUIRK_CLASS, SERVER, SERVER_COMMANDS,
|
POWER_SOURCE, QUIRK_APPLIED, QUIRK_CLASS, SERVER, SERVER_COMMANDS,
|
||||||
SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZDO_CHANNEL)
|
SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZDO_CHANNEL,
|
||||||
|
LQI, RSSI, LAST_SEEN)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_KEEP_ALIVE_INTERVAL = 7200
|
||||||
|
_UPDATE_ALIVE_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
|
|
||||||
class DeviceStatus(Enum):
|
class DeviceStatus(Enum):
|
||||||
|
@ -56,6 +62,11 @@ class ZHADevice:
|
||||||
self._zigpy_device.__class__.__module__,
|
self._zigpy_device.__class__.__module__,
|
||||||
self._zigpy_device.__class__.__name__
|
self._zigpy_device.__class__.__name__
|
||||||
)
|
)
|
||||||
|
self._available_check = async_track_time_interval(
|
||||||
|
self.hass,
|
||||||
|
self._check_available,
|
||||||
|
_UPDATE_ALIVE_INTERVAL
|
||||||
|
)
|
||||||
self.status = DeviceStatus.CREATED
|
self.status = DeviceStatus.CREATED
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -158,6 +169,16 @@ class ZHADevice:
|
||||||
"""Set availability from restore and prevent signals."""
|
"""Set availability from restore and prevent signals."""
|
||||||
self._available = available
|
self._available = available
|
||||||
|
|
||||||
|
def _check_available(self, *_):
|
||||||
|
if self.last_seen is None:
|
||||||
|
self.update_available(False)
|
||||||
|
else:
|
||||||
|
difference = time.time() - self.last_seen
|
||||||
|
if difference > _KEEP_ALIVE_INTERVAL:
|
||||||
|
self.update_available(False)
|
||||||
|
else:
|
||||||
|
self.update_available(True)
|
||||||
|
|
||||||
def update_available(self, available):
|
def update_available(self, available):
|
||||||
"""Set sensor availability."""
|
"""Set sensor availability."""
|
||||||
if self._available != available and available:
|
if self._available != available and available:
|
||||||
|
@ -178,6 +199,8 @@ class ZHADevice:
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device."""
|
"""Return a device description for device."""
|
||||||
ieee = str(self.ieee)
|
ieee = str(self.ieee)
|
||||||
|
time_struct = time.localtime(self.last_seen)
|
||||||
|
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
|
||||||
return {
|
return {
|
||||||
IEEE: ieee,
|
IEEE: ieee,
|
||||||
NWK: self.nwk,
|
NWK: self.nwk,
|
||||||
|
@ -187,7 +210,10 @@ class ZHADevice:
|
||||||
QUIRK_APPLIED: self.quirk_applied,
|
QUIRK_APPLIED: self.quirk_applied,
|
||||||
QUIRK_CLASS: self.quirk_class,
|
QUIRK_CLASS: self.quirk_class,
|
||||||
MANUFACTURER_CODE: self.manufacturer_code,
|
MANUFACTURER_CODE: self.manufacturer_code,
|
||||||
POWER_SOURCE: self.power_source
|
POWER_SOURCE: self.power_source,
|
||||||
|
LQI: self.lqi,
|
||||||
|
RSSI: self.rssi,
|
||||||
|
LAST_SEEN: update_time
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_cluster_channel(self, cluster_channel):
|
def add_cluster_channel(self, cluster_channel):
|
||||||
|
|
|
@ -18,7 +18,7 @@ from .channels import (
|
||||||
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
|
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_DEVICE_CONFIG, COMPONENTS, ZHA_DISCOVERY_NEW, DATA_ZHA,
|
CONF_DEVICE_CONFIG, COMPONENTS, ZHA_DISCOVERY_NEW, DATA_ZHA,
|
||||||
SENSOR_TYPE, UNKNOWN, GENERIC, POWER_CONFIGURATION_CHANNEL
|
SENSOR_TYPE, UNKNOWN, GENERIC
|
||||||
)
|
)
|
||||||
from .registries import (
|
from .registries import (
|
||||||
BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, EVENT_RELAY_CLUSTERS,
|
BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, EVENT_RELAY_CLUSTERS,
|
||||||
|
@ -26,7 +26,6 @@ from .registries import (
|
||||||
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
||||||
OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES
|
OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES
|
||||||
)
|
)
|
||||||
from ..device_entity import ZhaDeviceEntity
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -168,9 +167,10 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
|
||||||
profile_clusters, device_key,
|
profile_clusters, device_key,
|
||||||
is_new_join):
|
is_new_join):
|
||||||
"""Dispatch single cluster matches to HA components."""
|
"""Dispatch single cluster matches to HA components."""
|
||||||
from zigpy.zcl.clusters.general import OnOff
|
from zigpy.zcl.clusters.general import OnOff, PowerConfiguration
|
||||||
cluster_matches = []
|
cluster_matches = []
|
||||||
cluster_match_results = []
|
cluster_match_results = []
|
||||||
|
matched_power_configuration = False
|
||||||
for cluster in endpoint.in_clusters.values():
|
for cluster in endpoint.in_clusters.values():
|
||||||
if cluster.cluster_id in CHANNEL_ONLY_CLUSTERS:
|
if cluster.cluster_id in CHANNEL_ONLY_CLUSTERS:
|
||||||
cluster_match_results.append(
|
cluster_match_results.append(
|
||||||
|
@ -182,6 +182,14 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cluster.cluster_id not in profile_clusters:
|
if cluster.cluster_id not in profile_clusters:
|
||||||
|
# Only create one battery sensor per device
|
||||||
|
if cluster.cluster_id == PowerConfiguration.cluster_id and \
|
||||||
|
(zha_device.is_mains_powered or
|
||||||
|
matched_power_configuration):
|
||||||
|
continue
|
||||||
|
elif cluster.cluster_id == PowerConfiguration.cluster_id and not \
|
||||||
|
zha_device.is_mains_powered:
|
||||||
|
matched_power_configuration = True
|
||||||
cluster_match_results.append(_async_handle_single_cluster_match(
|
cluster_match_results.append(_async_handle_single_cluster_match(
|
||||||
hass,
|
hass,
|
||||||
zha_device,
|
zha_device,
|
||||||
|
@ -279,13 +287,3 @@ def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key,
|
||||||
})
|
})
|
||||||
|
|
||||||
return discovery_info
|
return discovery_info
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_create_device_entity(zha_device):
|
|
||||||
"""Create ZHADeviceEntity."""
|
|
||||||
device_entity_channels = []
|
|
||||||
if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels:
|
|
||||||
channel = zha_device.cluster_channels.get(POWER_CONFIGURATION_CHANNEL)
|
|
||||||
device_entity_channels.append(channel)
|
|
||||||
return ZhaDeviceEntity(zha_device, device_entity_channels)
|
|
||||||
|
|
|
@ -17,22 +17,21 @@ from homeassistant.core import callback
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import (
|
||||||
async_get_registry as get_dev_reg)
|
async_get_registry as get_dev_reg)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
|
||||||
|
|
||||||
from ..api import async_get_device_info
|
from ..api import async_get_device_info
|
||||||
from .const import (
|
from .const import (
|
||||||
ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE,
|
ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE,
|
||||||
CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT,
|
CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT,
|
||||||
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_GATEWAY,
|
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, DEBUG_LEVELS,
|
||||||
DEBUG_LEVELS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEVICE_FULL_INIT,
|
DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEVICE_FULL_INIT,
|
||||||
DEVICE_INFO, DEVICE_JOINED, DEVICE_REMOVED, DOMAIN, IEEE, LOG_ENTRY,
|
DEVICE_INFO, DEVICE_JOINED, DEVICE_REMOVED, DOMAIN, IEEE, LOG_ENTRY,
|
||||||
LOG_OUTPUT, MODEL, NWK, ORIGINAL, RADIO, RADIO_DESCRIPTION, RAW_INIT,
|
LOG_OUTPUT, MODEL, NWK, ORIGINAL, RADIO, RADIO_DESCRIPTION, RAW_INIT,
|
||||||
SIGNAL_REMOVE, SIGNATURE, TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA,
|
SIGNAL_REMOVE, SIGNATURE, TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA,
|
||||||
ZHA_GW_MSG, ZIGPY, ZIGPY_DECONZ, ZIGPY_XBEE)
|
ZHA_GW_MSG, ZIGPY, ZIGPY_DECONZ, ZIGPY_XBEE)
|
||||||
from .device import DeviceStatus, ZHADevice
|
from .device import DeviceStatus, ZHADevice
|
||||||
from .discovery import (
|
from .discovery import (
|
||||||
async_create_device_entity, async_dispatch_discovery_info,
|
async_dispatch_discovery_info, async_process_endpoint
|
||||||
async_process_endpoint)
|
)
|
||||||
from .patches import apply_application_controller_patch
|
from .patches import apply_application_controller_patch
|
||||||
from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES
|
from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES
|
||||||
from .store import async_get_registry
|
from .store import async_get_registry
|
||||||
|
@ -51,13 +50,11 @@ class ZHAGateway:
|
||||||
"""Initialize the gateway."""
|
"""Initialize the gateway."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._config = config
|
self._config = config
|
||||||
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
|
||||||
self._devices = {}
|
self._devices = {}
|
||||||
self._device_registry = collections.defaultdict(list)
|
self._device_registry = collections.defaultdict(list)
|
||||||
self.zha_storage = None
|
self.zha_storage = None
|
||||||
self.application_controller = None
|
self.application_controller = None
|
||||||
self.radio_description = None
|
self.radio_description = None
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
|
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
|
hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
|
||||||
self._log_levels = {
|
self._log_levels = {
|
||||||
ORIGINAL: async_capture_log_levels(),
|
ORIGINAL: async_capture_log_levels(),
|
||||||
|
@ -324,9 +321,6 @@ class ZHAGateway:
|
||||||
discovery_info
|
discovery_info
|
||||||
)
|
)
|
||||||
|
|
||||||
device_entity = async_create_device_entity(zha_device)
|
|
||||||
await self._component.async_add_entities([device_entity])
|
|
||||||
|
|
||||||
if is_new_join:
|
if is_new_join:
|
||||||
device_info = async_get_device_info(self._hass, zha_device)
|
device_info = async_get_device_info(self._hass, zha_device)
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
|
|
|
@ -18,7 +18,7 @@ from .const import (
|
||||||
OCCUPANCY, REPORT_CONFIG_IMMEDIATE, OPENING, ZONE, RADIO_DESCRIPTION,
|
OCCUPANCY, REPORT_CONFIG_IMMEDIATE, OPENING, ZONE, RADIO_DESCRIPTION,
|
||||||
REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
|
REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
|
||||||
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, ACCELERATION, RadioType, RADIO,
|
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, ACCELERATION, RadioType, RADIO,
|
||||||
CONTROLLER
|
CONTROLLER, BATTERY
|
||||||
)
|
)
|
||||||
|
|
||||||
SMARTTHINGS_HUMIDITY_CLUSTER = 64581
|
SMARTTHINGS_HUMIDITY_CLUSTER = 64581
|
||||||
|
@ -110,8 +110,6 @@ def establish_device_mappings():
|
||||||
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
||||||
|
|
||||||
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id)
|
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id)
|
||||||
CHANNEL_ONLY_CLUSTERS.append(
|
|
||||||
zcl.clusters.general.PowerConfiguration.cluster_id)
|
|
||||||
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id)
|
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id)
|
||||||
|
|
||||||
OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id)
|
OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id)
|
||||||
|
@ -166,7 +164,8 @@ def establish_device_mappings():
|
||||||
SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
|
SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
|
||||||
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
|
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
|
||||||
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
|
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
|
||||||
zcl.clusters.closures.DoorLock: LOCK
|
zcl.clusters.closures.DoorLock: LOCK,
|
||||||
|
zcl.clusters.general.PowerConfiguration: SENSOR
|
||||||
})
|
})
|
||||||
|
|
||||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
|
||||||
|
@ -184,6 +183,7 @@ def establish_device_mappings():
|
||||||
zcl.clusters.smartenergy.Metering.cluster_id: METERING,
|
zcl.clusters.smartenergy.Metering.cluster_id: METERING,
|
||||||
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
|
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
|
||||||
ELECTRICAL_MEASUREMENT,
|
ELECTRICAL_MEASUREMENT,
|
||||||
|
zcl.clusters.general.PowerConfiguration.cluster_id: BATTERY
|
||||||
})
|
})
|
||||||
|
|
||||||
BINARY_SENSOR_TYPES.update({
|
BINARY_SENSOR_TYPES.update({
|
||||||
|
|
|
@ -1,158 +0,0 @@
|
||||||
"""Device entity for Zigbee Home Automation."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import numbers
|
|
||||||
import time
|
|
||||||
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from .core.const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR
|
|
||||||
from .entity import ZhaEntity
|
|
||||||
|
|
||||||
_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'
|
|
||||||
}
|
|
||||||
|
|
||||||
STATE_ONLINE = 'online'
|
|
||||||
STATE_OFFLINE = 'offline'
|
|
||||||
|
|
||||||
|
|
||||||
class ZhaDeviceEntity(ZhaEntity):
|
|
||||||
"""A base class for ZHA devices."""
|
|
||||||
|
|
||||||
def __init__(self, zha_device, channels, keepalive_interval=7200,
|
|
||||||
**kwargs):
|
|
||||||
"""Init ZHA endpoint entity."""
|
|
||||||
ieee = zha_device.ieee
|
|
||||||
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
|
|
||||||
unique_id = "{}_{}_{}".format(
|
|
||||||
slugify(zha_device.manufacturer),
|
|
||||||
slugify(zha_device.model),
|
|
||||||
ieeetail,
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs['component'] = 'zha'
|
|
||||||
super().__init__(unique_id, zha_device, channels, skip_entity_id=True,
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
self._keepalive_interval = keepalive_interval
|
|
||||||
self._device_state_attributes.update({
|
|
||||||
'nwk': '0x{0:04x}'.format(zha_device.nwk),
|
|
||||||
'ieee': str(zha_device.ieee),
|
|
||||||
'lqi': zha_device.lqi,
|
|
||||||
'rssi': zha_device.rssi,
|
|
||||||
})
|
|
||||||
self._should_poll = True
|
|
||||||
self._battery_channel = self.cluster_channels.get(
|
|
||||||
POWER_CONFIGURATION_CHANNEL)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> str:
|
|
||||||
"""Return the state of the entity."""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return True if device is available."""
|
|
||||||
return self._zha_device.available
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return device specific state attributes."""
|
|
||||||
update_time = None
|
|
||||||
device = self._zha_device
|
|
||||||
if device.last_seen is not None and not self.available:
|
|
||||||
time_struct = time.localtime(device.last_seen)
|
|
||||||
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
|
|
||||||
self._device_state_attributes['last_seen'] = update_time
|
|
||||||
if ('last_seen' in self._device_state_attributes and
|
|
||||||
self.available):
|
|
||||||
del self._device_state_attributes['last_seen']
|
|
||||||
self._device_state_attributes['lqi'] = device.lqi
|
|
||||||
self._device_state_attributes['rssi'] = device.rssi
|
|
||||||
return self._device_state_attributes
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
|
||||||
"""Run when about to be added to hass."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
await self.async_check_recently_seen()
|
|
||||||
if self._battery_channel:
|
|
||||||
await self.async_accept_signal(
|
|
||||||
self._battery_channel, 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()
|
|
||||||
|
|
||||||
def async_update_state_attribute(self, key, value):
|
|
||||||
"""Update a single device state attribute."""
|
|
||||||
if key == 'battery_level':
|
|
||||||
if not isinstance(value, numbers.Number) or value == -1:
|
|
||||||
return
|
|
||||||
value = value / 2
|
|
||||||
value = int(round(value))
|
|
||||||
self._device_state_attributes.update({
|
|
||||||
key: value
|
|
||||||
})
|
|
||||||
self.async_schedule_update_ha_state()
|
|
||||||
|
|
||||||
async def async_update(self):
|
|
||||||
"""Handle polling."""
|
|
||||||
if self._zha_device.last_seen is None:
|
|
||||||
self._zha_device.update_available(False)
|
|
||||||
else:
|
|
||||||
difference = time.time() - self._zha_device.last_seen
|
|
||||||
if difference > self._keepalive_interval:
|
|
||||||
self._zha_device.update_available(False)
|
|
||||||
else:
|
|
||||||
self._zha_device.update_available(True)
|
|
||||||
if self._battery_channel:
|
|
||||||
await self.async_get_latest_battery_reading()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_set_available(self, available):
|
|
||||||
"""Set entity availability."""
|
|
||||||
if available:
|
|
||||||
self._state = STATE_ONLINE
|
|
||||||
else:
|
|
||||||
self._state = STATE_OFFLINE
|
|
||||||
super().async_set_available(available)
|
|
||||||
|
|
||||||
async def _async_init_battery_values(self):
|
|
||||||
"""Get initial battery level and battery info from channel cache."""
|
|
||||||
battery_size = await self._battery_channel.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_channel.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 channels cache."""
|
|
||||||
battery = await self._battery_channel.get_attribute_value(
|
|
||||||
'battery_percentage_remaining')
|
|
||||||
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
|
|
||||||
if battery is not None and battery != -1:
|
|
||||||
battery = battery / 2
|
|
||||||
battery = int(round(battery))
|
|
||||||
self._device_state_attributes['battery_level'] = battery
|
|
|
@ -1,10 +1,12 @@
|
||||||
"""Sensors on Zigbee Home Automation networks."""
|
"""Sensors on Zigbee Home Automation networks."""
|
||||||
import logging
|
import logging
|
||||||
|
import numbers
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
DOMAIN, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE,
|
DOMAIN, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE,
|
||||||
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_POWER
|
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_POWER,
|
||||||
|
DEVICE_CLASS_BATTERY
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS, POWER_WATT, ATTR_UNIT_OF_MEASUREMENT
|
TEMP_CELSIUS, POWER_WATT, ATTR_UNIT_OF_MEASUREMENT
|
||||||
|
@ -14,12 +16,29 @@ from .core.const import (
|
||||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
|
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
|
||||||
ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
|
ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
|
||||||
GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL,
|
GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL,
|
||||||
SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, UNKNOWN)
|
SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, UNKNOWN, BATTERY,
|
||||||
|
POWER_CONFIGURATION_CHANNEL)
|
||||||
from .entity import ZhaEntity
|
from .entity import ZhaEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 5
|
PARALLEL_UPDATES = 5
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Formatter functions
|
# Formatter functions
|
||||||
def pass_through_formatter(value):
|
def pass_through_formatter(value):
|
||||||
|
@ -63,6 +82,29 @@ def pressure_formatter(value):
|
||||||
return round(float(value))
|
return round(float(value))
|
||||||
|
|
||||||
|
|
||||||
|
def battery_percentage_remaining_formatter(value):
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
|
||||||
|
if not isinstance(value, numbers.Number) or value == -1:
|
||||||
|
return value
|
||||||
|
value = value / 2
|
||||||
|
value = int(round(value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
async def async_battery_device_state_attr_provider(channel):
|
||||||
|
"""Return device statr attrs for battery sensors."""
|
||||||
|
state_attrs = {}
|
||||||
|
battery_size = await channel.get_attribute_value('battery_size')
|
||||||
|
if battery_size is not None:
|
||||||
|
state_attrs['battery_size'] = BATTERY_SIZES.get(
|
||||||
|
battery_size, 'Unknown')
|
||||||
|
battery_quantity = await channel.get_attribute_value('battery_quantity')
|
||||||
|
if battery_quantity is not None:
|
||||||
|
state_attrs['battery_quantity'] = battery_quantity
|
||||||
|
return state_attrs
|
||||||
|
|
||||||
|
|
||||||
FORMATTER_FUNC_REGISTRY = {
|
FORMATTER_FUNC_REGISTRY = {
|
||||||
HUMIDITY: humidity_formatter,
|
HUMIDITY: humidity_formatter,
|
||||||
TEMPERATURE: temperature_formatter,
|
TEMPERATURE: temperature_formatter,
|
||||||
|
@ -70,6 +112,7 @@ FORMATTER_FUNC_REGISTRY = {
|
||||||
ELECTRICAL_MEASUREMENT: active_power_formatter,
|
ELECTRICAL_MEASUREMENT: active_power_formatter,
|
||||||
ILLUMINANCE: illuminance_formatter,
|
ILLUMINANCE: illuminance_formatter,
|
||||||
GENERIC: pass_through_formatter,
|
GENERIC: pass_through_formatter,
|
||||||
|
BATTERY: battery_percentage_remaining_formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
UNIT_REGISTRY = {
|
UNIT_REGISTRY = {
|
||||||
|
@ -79,11 +122,13 @@ UNIT_REGISTRY = {
|
||||||
ILLUMINANCE: 'lx',
|
ILLUMINANCE: 'lx',
|
||||||
METERING: POWER_WATT,
|
METERING: POWER_WATT,
|
||||||
ELECTRICAL_MEASUREMENT: POWER_WATT,
|
ELECTRICAL_MEASUREMENT: POWER_WATT,
|
||||||
GENERIC: None
|
GENERIC: None,
|
||||||
|
BATTERY: '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANNEL_REGISTRY = {
|
CHANNEL_REGISTRY = {
|
||||||
ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL,
|
ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL,
|
||||||
|
BATTERY: POWER_CONFIGURATION_CHANNEL
|
||||||
}
|
}
|
||||||
|
|
||||||
POLLING_REGISTRY = {
|
POLLING_REGISTRY = {
|
||||||
|
@ -101,7 +146,13 @@ DEVICE_CLASS_REGISTRY = {
|
||||||
PRESSURE: DEVICE_CLASS_PRESSURE,
|
PRESSURE: DEVICE_CLASS_PRESSURE,
|
||||||
ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE,
|
ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE,
|
||||||
METERING: DEVICE_CLASS_POWER,
|
METERING: DEVICE_CLASS_POWER,
|
||||||
ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER
|
ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER,
|
||||||
|
BATTERY: DEVICE_CLASS_BATTERY
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DEVICE_STATE_ATTR_PROVIDER_REGISTRY = {
|
||||||
|
BATTERY: async_battery_device_state_attr_provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,10 +223,18 @@ class Sensor(ZhaEntity):
|
||||||
self._sensor_type,
|
self._sensor_type,
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get(
|
||||||
|
self._sensor_type,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
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."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
if self.state_attr_provider is not None:
|
||||||
|
self._device_state_attributes = await self.state_attr_provider(
|
||||||
|
self._channel
|
||||||
|
)
|
||||||
await self.async_accept_signal(
|
await self.async_accept_signal(
|
||||||
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
|
||||||
await self.async_accept_signal(
|
await self.async_accept_signal(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue