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 pylint
pull/21651/head
David F. Mulcahey 2019-03-04 00:22:42 -05:00 committed by Paulus Schoutsen
parent ee6f09dd29
commit fc07d3a159
5 changed files with 273 additions and 26 deletions

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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)