diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 3a8144d9188..71c64a017f7 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -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 diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index e069c5a1e17..a9524729a9f 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -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): diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 2d995ca7aca..131ce795d93 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -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): diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 0c5cf1d081e..36ef7eca21d 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -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): diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 3b01138ccb2..86ded53bae9 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -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): diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index e220825d526..03f85ddbda4 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -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): diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 1a844ebcfe0..9942743d326 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -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): diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index f0c5e54bae0..033cedac705 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -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.""" diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index e9a17395735..881f20cd0fc 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -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" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 71f5a258cdc..bf46fd33619 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -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