Create zwave devices on OZW thread and only add them during discovery ()

* Create zwave devices on OZW thread and only add them during discovery.

* Read and write devices dict from loop thread.

* More async

* replace callback with coroutine

* import common function instead of callin git
pull/6049/merge
Andrey 2017-02-23 23:06:28 +02:00 committed by Paulus Schoutsen
parent f2a2d6bfa1
commit 1d32bced1c
10 changed files with 114 additions and 130 deletions
homeassistant/components
binary_sensor
climate
sensor
switch
tests/components/zwave

View File

@ -10,6 +10,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave
from homeassistant.components.zwave import workaround
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.components.binary_sensor import (
DOMAIN,
BinarySensorDevice)
@ -18,31 +19,22 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Z-Wave platform for binary sensors."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
def get_device(value, **kwargs):
"""Create zwave entity device."""
value.set_change_verified(False)
device_mapping = workaround.get_device_mapping(value)
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
# Default the multiplier to 4
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
add_devices([
ZWaveTriggerSensor(value, "motion",
hass, re_arm_multiplier * 8)
])
return
return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8)
if workaround.get_device_component_mapping(value) == DOMAIN:
add_devices([ZWaveBinarySensor(value, None)])
return
return ZWaveBinarySensor(value, None)
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
add_devices([ZWaveBinarySensor(value, None)])
return ZWaveBinarySensor(value, None)
return None
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
@ -77,26 +69,23 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
class ZWaveTriggerSensor(ZWaveBinarySensor):
"""Representation of a stateless sensor within Z-Wave."""
def __init__(self, value, device_class, hass, re_arm_sec=60):
def __init__(self, value, device_class, re_arm_sec=60):
"""Initialize the sensor."""
super(ZWaveTriggerSensor, self).__init__(value, device_class)
self._hass = hass
self.re_arm_sec = re_arm_sec
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec)
# If it's active make sure that we set the timeout tracker
track_point_in_time(
self._hass, self.async_update_ha_state,
self.invalidate_after)
self.invalidate_after = None
def update_properties(self):
"""Called when a value for this entity's node has changed."""
self._state = self._value.data
# only allow this value to be true for re_arm secs
if not self.hass:
return
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec)
track_point_in_time(
self._hass, self.async_update_ha_state,
self.hass, self.async_update_ha_state,
self.invalidate_after)
@property

View File

@ -11,6 +11,7 @@ from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
@ -32,19 +33,11 @@ DEVICE_MAPPINGS = {
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Z-Wave Climate devices."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
def get_device(hass, value, **kwargs):
"""Create zwave entity device."""
temp_unit = hass.config.units.temperature_unit
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
value.set_change_verified(False)
add_devices([ZWaveClimate(value, temp_unit)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
return ZWaveClimate(value, temp_unit)
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):

View File

@ -11,6 +11,7 @@ from homeassistant.components.cover import (
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.components.zwave import workaround
from homeassistant.components.cover import CoverDevice
@ -19,27 +20,20 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave covers."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
def get_device(value, **kwargs):
"""Create zwave entity device."""
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and value.index == 0):
value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)])
return ZwaveRollershutter(value)
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if (value.type != zwave.const.TYPE_BOOL and
value.genre != zwave.const.GENRE_USER):
return
return None
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
else:
return
return ZwaveGarageDoor(value)
return None
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):

View File

@ -13,6 +13,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
SUPPORT_RGB_COLOR, DOMAIN, Light
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
@ -48,32 +49,27 @@ SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
| SUPPORT_COLOR_TEMP)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and add Z-Wave lights."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
def get_device(node, value, node_config, **kwargs):
"""Create zwave entity device."""
name = '{}.{}'.format(DOMAIN, zwave.object_id(value))
node_config = hass.data[zwave.DATA_DEVICE_CONFIG].get(name)
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
' CONF_REFRESH_DELAY=%s', name, node_config,
refresh, delay)
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
return
return None
if value.type != zwave.const.TYPE_BYTE:
return
return None
if value.genre != zwave.const.GENRE_USER:
return
return None
value.set_change_verified(False)
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
add_devices([ZwaveColorLight(value, refresh, delay)])
return ZwaveColorLight(value, refresh, delay)
else:
add_devices([ZwaveDimmer(value, refresh, delay)])
return ZwaveDimmer(value, refresh, delay)
def brightness_state(value):

View File

@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.components.lock import DOMAIN, LockDevice
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
@ -119,15 +120,8 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave locks."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
def get_device(hass, node, value, **kwargs):
"""Create zwave entity device."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
@ -182,11 +176,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
break
if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK:
return
return None
if value.type != zwave.const.TYPE_BOOL:
return
return None
if value.genre != zwave.const.GENRE_USER:
return
return None
if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
hass.services.register(DOMAIN,
SERVICE_SET_USERCODE,
@ -204,7 +198,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
descriptions.get(SERVICE_CLEAR_USERCODE),
schema=CLEAR_USERCODE_SCHEMA)
value.set_change_verified(False)
add_devices([ZwaveLock(value)])
return ZwaveLock(value)
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):

View File

@ -10,41 +10,25 @@ import logging
from homeassistant.components.sensor import DOMAIN
from homeassistant.components import zwave
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Z-Wave sensors."""
# Return on empty `discovery_info`. Given you configure HA with:
#
# sensor:
# platform: zwave
#
# `setup_platform` will be called without `discovery_info`.
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
def get_device(node, value, **kwargs):
"""Create zwave entity device."""
value.set_change_verified(False)
# if 1 in groups and (NETWORK.controller.node_id not in
# groups[1].associations):
# node.groups[1].add_association(NETWORK.controller.node_id)
# Generic Device mappings
if node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL):
add_devices([ZWaveMultilevelSensor(value)])
elif node.has_command_class(zwave.const.COMMAND_CLASS_METER) and \
return ZWaveMultilevelSensor(value)
if node.has_command_class(zwave.const.COMMAND_CLASS_METER) and \
value.type == zwave.const.TYPE_DECIMAL:
add_devices([ZWaveMultilevelSensor(value)])
elif node.has_command_class(zwave.const.COMMAND_CLASS_ALARM) or \
return ZWaveMultilevelSensor(value)
if node.has_command_class(zwave.const.COMMAND_CLASS_ALARM) or \
node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_ALARM):
add_devices([ZWaveAlarmSensor(value)])
return ZWaveAlarmSensor(value)
return None
class ZWaveSensor(zwave.ZWaveDeviceEntity):

View File

@ -9,27 +9,21 @@ import logging
# pylint: disable=import-error
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Z-Wave platform."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
def get_device(node, value, **kwargs):
"""Create zwave entity device."""
if not node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY):
return
return None
if value.type != zwave.const.TYPE_BOOL or value.genre != \
zwave.const.GENRE_USER:
return
zwave.const.GENRE_USER:
return None
value.set_change_verified(False)
add_devices([ZwaveSwitch(value)])
return ZwaveSwitch(value)
class ZwaveSwitch(zwave.ZWaveDeviceEntity, SwitchDevice):

View File

@ -4,6 +4,7 @@ Support for Z-Wave.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zwave/
"""
import asyncio
import logging
import os.path
import time
@ -11,6 +12,7 @@ from pprint import pprint
import voluptuous as vol
from homeassistant.loader import get_platform
from homeassistant.helpers import discovery
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_LOCATION, ATTR_ENTITY_ID, ATTR_WAKEUP,
@ -54,8 +56,10 @@ DEFAULT_CONF_REFRESH_VALUE = False
DEFAULT_CONF_REFRESH_DELAY = 5
DOMAIN = 'zwave'
DATA_ZWAVE_DICT = 'zwave_devices'
NETWORK = None
DATA_DEVICE_CONFIG = 'zwave_device_config'
# List of tuple (DOMAIN, discovered service, supported command classes,
# value type, genre type, specific device class).
@ -264,6 +268,20 @@ def get_config_value(node, value_index, tries=5):
return None
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Generic Z-Wave platform setup."""
if discovery_info is None or NETWORK is None:
return False
device = hass.data[DATA_ZWAVE_DICT].pop(
discovery_info[const.DISCOVERY_DEVICE])
if device:
yield from async_add_devices([device])
return True
else:
return False
# pylint: disable=R0914
def setup(hass, config):
"""Setup Z-Wave.
@ -294,7 +312,7 @@ def setup(hass, config):
# Load configuration
use_debug = config[DOMAIN].get(CONF_DEBUG)
autoheal = config[DOMAIN].get(CONF_AUTOHEAL)
hass.data[DATA_DEVICE_CONFIG] = EntityValues(
device_config = EntityValues(
config[DOMAIN][CONF_DEVICE_CONFIG],
config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN],
config[DOMAIN][CONF_DEVICE_CONFIG_GLOB])
@ -310,6 +328,7 @@ def setup(hass, config):
options.lock()
NETWORK = ZWaveNetwork(options, autostart=False)
hass.data[DATA_ZWAVE_DICT] = {}
if use_debug:
def log_all(signal, value=None):
@ -386,7 +405,7 @@ def setup(hass, config):
component = workaround_component
name = "{}.{}".format(component, object_id(value))
node_config = hass.data[DATA_DEVICE_CONFIG].get(name)
node_config = device_config.get(name)
if node_config.get(CONF_IGNORED):
_LOGGER.info(
@ -399,11 +418,21 @@ def setup(hass, config):
value.enable_poll(polling_intensity)
else:
value.disable_poll()
platform = get_platform(component, DOMAIN)
device = platform.get_device(
node=node, value=value, node_config=node_config, hass=hass)
if not device:
continue
dict_id = value.value_id
discovery.load_platform(hass, component, DOMAIN, {
const.ATTR_NODE_ID: node.node_id,
const.ATTR_VALUE_ID: value.value_id,
}, config)
@asyncio.coroutine
def discover_device(component, device, dict_id):
"""Put device in a dictionary and call discovery on it."""
hass.data[DATA_ZWAVE_DICT][dict_id] = device
yield from discovery.async_load_platform(
hass, component, DOMAIN,
{const.DISCOVERY_DEVICE: dict_id}, config)
hass.add_job(discover_device, component, device, dict_id)
def scene_activated(node, scene_id):
"""Called when a scene is activated on any node in the network."""
@ -694,7 +723,10 @@ class ZWaveDeviceEntity(Entity):
"""Called when a value for this entity's node has changed."""
self._update_attributes()
self.update_properties()
self.schedule_update_ha_state()
# If value changed after device was created but before setup_platform
# was called - skip updating state.
if self.hass:
self.schedule_update_ha_state()
def _update_attributes(self):
"""Update the node attributes. May only be used inside callback."""

View File

@ -15,6 +15,8 @@ ATTR_CONFIG_SIZE = "size"
ATTR_CONFIG_VALUE = "value"
NETWORK_READY_WAIT_SECS = 30
DISCOVERY_DEVICE = 'device'
SERVICE_CHANGE_ASSOCIATION = "change_association"
SERVICE_ADD_NODE = "add_node"
SERVICE_ADD_NODE_SECURE = "add_node_secure"

View File

@ -5,8 +5,6 @@ from unittest.mock import MagicMock, patch
import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.zwave import (
DATA_DEVICE_CONFIG, DEVICE_CONFIG_SCHEMA_ENTRY)
@pytest.fixture(autouse=True)
@ -24,24 +22,32 @@ def mock_openzwave():
@asyncio.coroutine
def test_device_config(hass):
"""Test device config stored in hass."""
def test_valid_device_config(hass):
"""Test valid device config."""
device_config = {
'light.kitchen': {
'ignored': 'true'
}
}
yield from async_setup_component(hass, 'zwave', {
result = yield from async_setup_component(hass, 'zwave', {
'zwave': {
'device_config': device_config
}})
assert DATA_DEVICE_CONFIG in hass.data
assert result
test_data = {
key: DEVICE_CONFIG_SCHEMA_ENTRY(value)
for key, value in device_config.items()
@asyncio.coroutine
def test_invalid_device_config(hass):
"""Test invalid device config."""
device_config = {
'light.kitchen': {
'some_ignored': 'true'
}
}
result = yield from async_setup_component(hass, 'zwave', {
'zwave': {
'device_config': device_config
}})
assert hass.data[DATA_DEVICE_CONFIG].get('light.kitchen') == \
test_data.get('light.kitchen')
assert not result