Add storage helper to ZHA and use it for the device node descriptor (#21500)
* node descriptor implementation add info to device info disable pylint rule check for success * review comments * send manufacturer code for get attr value for mfg clusters * ST report configs * do zdo task first * add guard * use faster reporting config * disable false positive pylintpull/21651/head
parent
ee6f09dd29
commit
fc07d3a159
|
@ -18,8 +18,13 @@ from ..helpers import (
|
|||
safe_read, get_attr_id_by_name)
|
||||
from ..const import (
|
||||
CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED,
|
||||
ATTRIBUTE_CHANNEL, EVENT_RELAY_CHANNEL
|
||||
ATTRIBUTE_CHANNEL, EVENT_RELAY_CHANNEL, ZDO_CHANNEL
|
||||
)
|
||||
from ..store import async_get_registry
|
||||
|
||||
NODE_DESCRIPTOR_REQUEST = 0x0002
|
||||
MAINS_POWERED = 1
|
||||
BATTERY_OR_UNKNOWN = 0
|
||||
|
||||
ZIGBEE_CHANNEL_REGISTRY = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -181,11 +186,16 @@ class ZigbeeChannel:
|
|||
|
||||
async def get_attribute_value(self, attribute, from_cache=True):
|
||||
"""Get the value for an attribute."""
|
||||
manufacturer = None
|
||||
manufacturer_code = self._zha_device.manufacturer_code
|
||||
if self.cluster.cluster_id >= 0xfc00 and manufacturer_code:
|
||||
manufacturer = manufacturer_code
|
||||
result = await safe_read(
|
||||
self._cluster,
|
||||
[attribute],
|
||||
allow_cache=from_cache,
|
||||
only_cache=from_cache
|
||||
only_cache=from_cache,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
return result.get(attribute)
|
||||
|
||||
|
@ -235,14 +245,21 @@ class AttributeListeningChannel(ZigbeeChannel):
|
|||
class ZDOChannel:
|
||||
"""Channel for ZDO events."""
|
||||
|
||||
POWER_SOURCES = {
|
||||
MAINS_POWERED: 'Mains',
|
||||
BATTERY_OR_UNKNOWN: 'Battery or Unknown'
|
||||
}
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize ZDOChannel."""
|
||||
self.name = 'zdo'
|
||||
self.name = ZDO_CHANNEL
|
||||
self._cluster = cluster
|
||||
self._zha_device = device
|
||||
self._status = ChannelStatus.CREATED
|
||||
self._unique_id = "{}_ZDO".format(device.name)
|
||||
self._cluster.add_listener(self)
|
||||
self.power_source = None
|
||||
self.manufacturer_code = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -271,10 +288,52 @@ class ZDOChannel:
|
|||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize channel."""
|
||||
entry = (await async_get_registry(
|
||||
self._zha_device.hass)).async_get_or_create(self._zha_device)
|
||||
_LOGGER.debug("entry loaded from storage: %s", entry)
|
||||
if entry is not None:
|
||||
self.power_source = entry.power_source
|
||||
self.manufacturer_code = entry.manufacturer_code
|
||||
|
||||
if self.power_source is None:
|
||||
self.power_source = BATTERY_OR_UNKNOWN
|
||||
|
||||
if self.manufacturer_code is None and not from_cache:
|
||||
# this should always be set. This is from us not doing
|
||||
# this previously so lets set it up so users don't have
|
||||
# to reconfigure every device.
|
||||
await self.async_get_node_descriptor(False)
|
||||
entry = (await async_get_registry(
|
||||
self._zha_device.hass)).async_update(self._zha_device)
|
||||
_LOGGER.debug("entry after getting node desc in init: %s", entry)
|
||||
self._status = ChannelStatus.INITIALIZED
|
||||
|
||||
async def async_get_node_descriptor(self, from_cache):
|
||||
"""Request the node descriptor from the device."""
|
||||
from zigpy.zdo.types import Status
|
||||
|
||||
if from_cache:
|
||||
return
|
||||
|
||||
node_descriptor = await self._cluster.request(
|
||||
NODE_DESCRIPTOR_REQUEST,
|
||||
self._cluster.device.nwk, tries=3, delay=2)
|
||||
|
||||
def get_bit(byteval, idx):
|
||||
return int(((byteval & (1 << idx)) != 0))
|
||||
|
||||
if node_descriptor is not None and\
|
||||
node_descriptor[0] == Status.SUCCESS:
|
||||
mac_capability_flags = node_descriptor[2].mac_capability_flags
|
||||
|
||||
self.power_source = get_bit(mac_capability_flags, 2)
|
||||
self.manufacturer_code = node_descriptor[2].manufacturer_code
|
||||
|
||||
_LOGGER.debug("node descriptor: %s", node_descriptor)
|
||||
|
||||
async def async_configure(self):
|
||||
"""Configure channel."""
|
||||
await self.async_get_node_descriptor(False)
|
||||
self._status = ChannelStatus.CONFIGURED
|
||||
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@ OCCUPANCY = 'occupancy'
|
|||
|
||||
ATTR_LEVEL = 'level'
|
||||
|
||||
ZDO_CHANNEL = 'zdo'
|
||||
ON_OFF_CHANNEL = 'on_off'
|
||||
ATTRIBUTE_CHANNEL = 'attribute'
|
||||
BASIC_CHANNEL = 'basic'
|
||||
|
@ -91,6 +92,8 @@ SIGNAL_REMOVE = 'remove'
|
|||
|
||||
QUIRK_APPLIED = 'quirk_applied'
|
||||
QUIRK_CLASS = 'quirk_class'
|
||||
MANUFACTURER_CODE = 'manufacturer_code'
|
||||
POWER_SOURCE = 'power_source'
|
||||
|
||||
|
||||
class RadioType(enum.Enum):
|
||||
|
|
|
@ -17,10 +17,10 @@ from .const import (
|
|||
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
|
||||
ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS,
|
||||
ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED,
|
||||
QUIRK_CLASS, BASIC_CHANNEL
|
||||
QUIRK_CLASS, ZDO_CHANNEL, MANUFACTURER_CODE, POWER_SOURCE
|
||||
)
|
||||
from .channels import EventRelayChannel
|
||||
from .channels.general import BasicChannel
|
||||
from .channels import EventRelayChannel, ZDOChannel
|
||||
from .store import async_get_registry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -69,7 +69,6 @@ class ZHADevice:
|
|||
self._zigpy_device.__class__.__module__,
|
||||
self._zigpy_device.__class__.__name__
|
||||
)
|
||||
self.power_source = None
|
||||
self.status = DeviceStatus.CREATED
|
||||
|
||||
@property
|
||||
|
@ -84,12 +83,12 @@ class ZHADevice:
|
|||
|
||||
@property
|
||||
def manufacturer(self):
|
||||
"""Return ieee address for device."""
|
||||
"""Return manufacturer for device."""
|
||||
return self._manufacturer
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return ieee address for device."""
|
||||
"""Return model for device."""
|
||||
return self._model
|
||||
|
||||
@property
|
||||
|
@ -115,7 +114,15 @@ class ZHADevice:
|
|||
@property
|
||||
def manufacturer_code(self):
|
||||
"""Return manufacturer code for device."""
|
||||
# will eventually get this directly from Zigpy
|
||||
if ZDO_CHANNEL in self.cluster_channels:
|
||||
return self.cluster_channels.get(ZDO_CHANNEL).manufacturer_code
|
||||
return None
|
||||
|
||||
@property
|
||||
def power_source(self):
|
||||
"""Return True if sensor is available."""
|
||||
if ZDO_CHANNEL in self.cluster_channels:
|
||||
return self.cluster_channels.get(ZDO_CHANNEL).power_source
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -164,7 +171,9 @@ class ZHADevice:
|
|||
MODEL: self.model,
|
||||
NAME: self.name or ieee,
|
||||
QUIRK_APPLIED: self.quirk_applied,
|
||||
QUIRK_CLASS: self.quirk_class
|
||||
QUIRK_CLASS: self.quirk_class,
|
||||
MANUFACTURER_CODE: self.manufacturer_code,
|
||||
POWER_SOURCE: ZDOChannel.POWER_SOURCES.get(self.power_source)
|
||||
}
|
||||
|
||||
def add_cluster_channel(self, cluster_channel):
|
||||
|
@ -186,19 +195,19 @@ class ZHADevice:
|
|||
_LOGGER.debug('%s: started configuration', self.name)
|
||||
await self._execute_channel_tasks('async_configure')
|
||||
_LOGGER.debug('%s: completed configuration', self.name)
|
||||
entry = (await async_get_registry(
|
||||
self.hass)).async_create_or_update(self)
|
||||
_LOGGER.debug('%s: stored in registry: %s', self.name, entry)
|
||||
|
||||
async def async_initialize(self, from_cache=False):
|
||||
"""Initialize channels."""
|
||||
_LOGGER.debug('%s: started initialization', self.name)
|
||||
await self._execute_channel_tasks('async_initialize', from_cache)
|
||||
if BASIC_CHANNEL in self.cluster_channels:
|
||||
self.power_source = self.cluster_channels.get(
|
||||
BASIC_CHANNEL).get_power_source()
|
||||
_LOGGER.debug(
|
||||
'%s: power source: %s',
|
||||
self.name,
|
||||
BasicChannel.POWER_SOURCES.get(self.power_source)
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'%s: power source: %s',
|
||||
self.name,
|
||||
ZDOChannel.POWER_SOURCES.get(self.power_source)
|
||||
)
|
||||
self.status = DeviceStatus.INITIALIZED
|
||||
_LOGGER.debug('%s: completed initialization', self.name)
|
||||
|
||||
|
@ -206,9 +215,18 @@ class ZHADevice:
|
|||
"""Gather and execute a set of CHANNEL tasks."""
|
||||
channel_tasks = []
|
||||
semaphore = asyncio.Semaphore(3)
|
||||
zdo_task = None
|
||||
for channel in self.all_channels:
|
||||
channel_tasks.append(
|
||||
self._async_create_task(semaphore, channel, task_name, *args))
|
||||
if channel.name == ZDO_CHANNEL:
|
||||
# pylint: disable=E1111
|
||||
zdo_task = self._async_create_task(
|
||||
semaphore, channel, task_name, *args)
|
||||
else:
|
||||
channel_tasks.append(
|
||||
self._async_create_task(
|
||||
semaphore, channel, task_name, *args))
|
||||
if zdo_task is not None:
|
||||
await zdo_task
|
||||
await asyncio.gather(*channel_tasks)
|
||||
|
||||
async def _async_create_task(self, semaphore, channel, func_name, *args):
|
||||
|
|
|
@ -27,9 +27,8 @@ from .const import (
|
|||
from .device import ZHADevice, DeviceStatus
|
||||
from ..device_entity import ZhaDeviceEntity
|
||||
from .channels import (
|
||||
AttributeListeningChannel, EventRelayChannel, ZDOChannel
|
||||
AttributeListeningChannel, EventRelayChannel, ZDOChannel, MAINS_POWERED
|
||||
)
|
||||
from .channels.general import BasicChannel
|
||||
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
|
||||
from .helpers import convert_ieee
|
||||
|
||||
|
@ -38,6 +37,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
SENSOR_TYPES = {}
|
||||
BINARY_SENSOR_TYPES = {}
|
||||
SMARTTHINGS_HUMIDITY_CLUSTER = 64581
|
||||
SMARTTHINGS_ACCELERATION_CLUSTER = 64514
|
||||
EntityReference = collections.namedtuple(
|
||||
'EntityReference', 'reference_id zha_device cluster_channels device_info')
|
||||
|
||||
|
@ -163,15 +163,14 @@ class ZHAGateway:
|
|||
# configure the device
|
||||
await zha_device.async_configure()
|
||||
elif not zha_device.available and zha_device.power_source is not None\
|
||||
and zha_device.power_source != BasicChannel.BATTERY\
|
||||
and zha_device.power_source != BasicChannel.UNKNOWN:
|
||||
and zha_device.power_source == MAINS_POWERED:
|
||||
# the device is currently marked unavailable and it isn't a battery
|
||||
# powered device so we should be able to update it now
|
||||
_LOGGER.debug(
|
||||
"attempting to request fresh state for %s %s",
|
||||
zha_device.name,
|
||||
"with power source: {}".format(
|
||||
BasicChannel.POWER_SOURCES.get(zha_device.power_source)
|
||||
ZDOChannel.POWER_SOURCES.get(zha_device.power_source)
|
||||
)
|
||||
)
|
||||
await zha_device.async_initialize(from_cache=False)
|
||||
|
@ -453,6 +452,7 @@ def establish_device_mappings():
|
|||
NO_SENSOR_CLUSTERS.append(
|
||||
zcl.clusters.general.PowerConfiguration.cluster_id)
|
||||
NO_SENSOR_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id)
|
||||
NO_SENSOR_CLUSTERS.append(SMARTTHINGS_ACCELERATION_CLUSTER)
|
||||
|
||||
BINDABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
|
||||
BINDABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
||||
|
@ -575,6 +575,27 @@ def establish_device_mappings():
|
|||
50
|
||||
)
|
||||
}],
|
||||
SMARTTHINGS_ACCELERATION_CLUSTER: [{
|
||||
'attr': 'acceleration',
|
||||
'config': REPORT_CONFIG_ASAP
|
||||
}, {
|
||||
'attr': 'x_axis',
|
||||
'config': REPORT_CONFIG_ASAP
|
||||
}, {
|
||||
'attr': 'y_axis',
|
||||
'config': REPORT_CONFIG_ASAP
|
||||
}, {
|
||||
'attr': 'z_axis',
|
||||
'config': REPORT_CONFIG_ASAP
|
||||
}],
|
||||
SMARTTHINGS_HUMIDITY_CLUSTER: [{
|
||||
'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
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
"""Data storage helper for ZHA."""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
# pylint: disable=W0611
|
||||
from typing import MutableMapping # noqa: F401
|
||||
from typing import cast
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_REGISTRY = 'zha_storage'
|
||||
|
||||
STORAGE_KEY = 'zha.storage'
|
||||
STORAGE_VERSION = 1
|
||||
SAVE_DELAY = 10
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class ZhaDeviceEntry:
|
||||
"""Zha Device storage Entry."""
|
||||
|
||||
name = attr.ib(type=str, default=None)
|
||||
ieee = attr.ib(type=str, default=None)
|
||||
power_source = attr.ib(type=int, default=None)
|
||||
manufacturer_code = attr.ib(type=int, default=None)
|
||||
|
||||
|
||||
class ZhaDeviceStorage:
|
||||
"""Class to hold a registry of zha devices."""
|
||||
|
||||
def __init__(self, hass: HomeAssistantType) -> None:
|
||||
"""Initialize the zha device storage."""
|
||||
self.hass = hass
|
||||
self.devices = {} # type: MutableMapping[str, ZhaDeviceEntry]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@callback
|
||||
def async_create(self, device) -> ZhaDeviceEntry:
|
||||
"""Create a new ZhaDeviceEntry."""
|
||||
device_entry = ZhaDeviceEntry(
|
||||
name=device.name,
|
||||
ieee=str(device.ieee),
|
||||
power_source=device.power_source,
|
||||
manufacturer_code=device.manufacturer_code
|
||||
|
||||
)
|
||||
self.devices[device_entry.ieee] = device_entry
|
||||
|
||||
return self.async_update(device)
|
||||
|
||||
@callback
|
||||
def async_get_or_create(self, device) -> ZhaDeviceEntry:
|
||||
"""Create a new ZhaDeviceEntry."""
|
||||
ieee_str = str(device.ieee)
|
||||
if ieee_str in self.devices:
|
||||
return self.devices[ieee_str]
|
||||
return self.async_create(device)
|
||||
|
||||
@callback
|
||||
def async_create_or_update(self, device) -> ZhaDeviceEntry:
|
||||
"""Create or update a ZhaDeviceEntry."""
|
||||
if str(device.ieee) in self.devices:
|
||||
return self.async_update(device)
|
||||
return self.async_create(device)
|
||||
|
||||
async def async_delete(self, ieee: str) -> None:
|
||||
"""Delete ZhaDeviceEntry."""
|
||||
del self.devices[ieee]
|
||||
self.async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_update(self, device) -> ZhaDeviceEntry:
|
||||
"""Update name of ZhaDeviceEntry."""
|
||||
ieee_str = str(device.ieee)
|
||||
old = self.devices[ieee_str]
|
||||
|
||||
changes = {}
|
||||
|
||||
if device.power_source != old.power_source:
|
||||
changes['power_source'] = device.power_source
|
||||
|
||||
if device.manufacturer_code != old.manufacturer_code:
|
||||
changes['manufacturer_code'] = device.manufacturer_code
|
||||
|
||||
new = self.devices[ieee_str] = attr.evolve(old, **changes)
|
||||
self.async_schedule_save()
|
||||
return new
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the registry of zha device entries."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
devices = OrderedDict() # type: OrderedDict[str, ZhaDeviceEntry]
|
||||
|
||||
if data is not None:
|
||||
for device in data['devices']:
|
||||
devices[device['ieee']] = ZhaDeviceEntry(
|
||||
name=device['name'],
|
||||
ieee=device['ieee'],
|
||||
power_source=device['power_source'],
|
||||
manufacturer_code=device['manufacturer_code']
|
||||
)
|
||||
|
||||
self.devices = devices
|
||||
|
||||
@callback
|
||||
def async_schedule_save(self) -> None:
|
||||
"""Schedule saving the registry of zha devices."""
|
||||
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict:
|
||||
"""Return data for the registry of zha devices to store in a file."""
|
||||
data = {}
|
||||
|
||||
data['devices'] = [
|
||||
{
|
||||
'name': entry.name,
|
||||
'ieee': entry.ieee,
|
||||
'power_source': entry.power_source,
|
||||
'manufacturer_code': entry.manufacturer_code,
|
||||
} for entry in self.devices.values()
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_registry(hass: HomeAssistantType) -> ZhaDeviceStorage:
|
||||
"""Return zha device storage instance."""
|
||||
task = hass.data.get(DATA_REGISTRY)
|
||||
|
||||
if task is None:
|
||||
async def _load_reg() -> ZhaDeviceStorage:
|
||||
registry = ZhaDeviceStorage(hass)
|
||||
await registry.async_load()
|
||||
return registry
|
||||
|
||||
task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg())
|
||||
|
||||
return cast(ZhaDeviceStorage, await task)
|
Loading…
Reference in New Issue