From 4339e9aab1e008f986a8e2c1250cb296c7142f86 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Jun 2017 22:51:13 -0700 Subject: [PATCH 01/12] version bump to 0.47 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c50fa3034c6..85c82a60728 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 47 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From d67f3b8060992e1f68e218e9adc65eed5a0a094a Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 16 Jun 2017 13:07:17 -0400 Subject: [PATCH 02/12] Use standard entity_ids for zwave entities (#7786) * Use standard entity_ids for zwave entities * Include temporary opt-in for new entity ids * Update link to blog post * Update tests * Add old entity_id as state attribute * Expose ZWave value details * Update tests * Also show new_entity_id * Just can't win with this one --- homeassistant/components/light/zwave.py | 6 +- homeassistant/components/zwave/__init__.py | 69 +++++---- homeassistant/components/zwave/api.py | 2 + homeassistant/components/zwave/const.py | 2 + homeassistant/components/zwave/node_entity.py | 53 ++++++- tests/components/zwave/test_api.py | 5 +- tests/components/zwave/test_init.py | 138 +++++++----------- tests/components/zwave/test_node_entity.py | 95 +++++++++++- 8 files changed, 247 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 9488ad38a59..752928f71a4 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -47,11 +47,11 @@ TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN def get_device(node, values, node_config, **kwargs): """Create Z-Wave entity device.""" - name = '{}.{}'.format(DOMAIN, zwave.object_id(values.primary)) 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) + _LOGGER.debug("node=%d value=%d node_config=%s CONF_REFRESH_VALUE=%s" + " CONF_REFRESH_DELAY=%s", node.node_id, + values.primary.value_id, node_config, refresh, delay) if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR): return ZwaveColorLight(values, refresh, delay) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index e82ce286e01..a79a986dc7d 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -16,6 +16,7 @@ import voluptuous as vol from homeassistant.core import CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) @@ -55,6 +56,7 @@ CONF_DEVICE_CONFIG = 'device_config' CONF_DEVICE_CONFIG_GLOB = 'device_config_glob' CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain' CONF_NETWORK_KEY = 'network_key' +CONF_NEW_ENTITY_IDS = 'new_entity_ids' ATTR_POWER = 'power_consumption' @@ -149,6 +151,7 @@ CONFIG_SCHEMA = vol.Schema({ cv.positive_int, vol.Optional(CONF_USB_STICK_PATH, default=DEFAULT_CONF_USB_STICK_PATH): cv.string, + vol.Optional(CONF_NEW_ENTITY_IDS, default=False): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -162,7 +165,7 @@ def _obj_to_dict(obj): def _value_name(value): """Return the name of the value.""" - return '{} {}'.format(node_name(value.node), value.label) + return '{} {}'.format(node_name(value.node), value.label).strip() def _node_object_id(node): @@ -250,6 +253,13 @@ def setup(hass, config): config[DOMAIN][CONF_DEVICE_CONFIG], config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN], config[DOMAIN][CONF_DEVICE_CONFIG_GLOB]) + new_entity_ids = config[DOMAIN][CONF_NEW_ENTITY_IDS] + if not new_entity_ids: + _LOGGER.warning( + "ZWave entity_ids will soon be changing. To opt in to new " + "entity_ids now, set `new_entity_ids: true` under zwave in your " + "configuration.yaml. See the following blog post for details: " + "https://home-assistant.io/blog/2017/06/15/zwave-entity-ids/") # Setup options options = ZWaveOption( @@ -311,30 +321,20 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" - entity = ZWaveNodeEntity(node, network) - node_config = device_config.get(entity.entity_id) + entity = ZWaveNodeEntity(node, network, new_entity_ids) + name = node_name(node) + if new_entity_ids: + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) + else: + generated_id = entity.entity_id + node_config = device_config.get(generated_id) if node_config.get(CONF_IGNORED): _LOGGER.info( "Ignoring node entity %s due to device settings", - entity.entity_id) + generated_id) return component.add_entities([entity]) - def scene_activated(node, scene_id): - """Handle an activated scene on any node in the network.""" - hass.bus.fire(const.EVENT_SCENE_ACTIVATED, { - ATTR_ENTITY_ID: _node_object_id(node), - const.ATTR_OBJECT_ID: _node_object_id(node), - const.ATTR_SCENE_ID: scene_id - }) - - def node_event_activated(node, value): - """Handle a nodeevent on any node in the network.""" - hass.bus.fire(const.EVENT_NODE_EVENT, { - const.ATTR_OBJECT_ID: _node_object_id(node), - const.ATTR_BASIC_LEVEL: value - }) - def network_ready(): """Handle the query of all awake nodes.""" _LOGGER.info("Zwave network is ready for use. All awake nodes " @@ -352,10 +352,6 @@ def setup(hass, config): value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) dispatcher.connect( node_added, ZWaveNetwork.SIGNAL_NODE_ADDED, weak=False) - dispatcher.connect( - scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT, weak=False) - dispatcher.connect( - node_event_activated, ZWaveNetwork.SIGNAL_NODE_EVENT, weak=False) dispatcher.connect( network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False) dispatcher.connect( @@ -757,9 +753,8 @@ class ZWaveDeviceEntityValues(): self.primary) if workaround_component and workaround_component != component: if workaround_component == workaround.WORKAROUND_IGNORE: - _LOGGER.info("Ignoring device %s due to workaround.", - "{}.{}".format( - component, object_id(self.primary))) + _LOGGER.info("Ignoring Node %d Value %d due to workaround.", + self.primary.node.node_id, self.primary.value_id) # No entity will be created for this value self._workaround_ignore = True return @@ -767,8 +762,13 @@ class ZWaveDeviceEntityValues(): workaround_component, component) component = workaround_component - name = "{}.{}".format(component, object_id(self.primary)) - node_config = self._device_config.get(name) + value_name = _value_name(self.primary) + if self._zwave_config[DOMAIN][CONF_NEW_ENTITY_IDS]: + generated_id = generate_entity_id( + component + '.{}', value_name, []) + else: + generated_id = "{}.{}".format(component, object_id(self.primary)) + node_config = self._device_config.get(generated_id) # Configure node _LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, " @@ -781,7 +781,7 @@ class ZWaveDeviceEntityValues(): if node_config.get(CONF_IGNORED): _LOGGER.info( - "Ignoring entity %s due to device settings", name) + "Ignoring entity %s due to device settings", generated_id) # No entity will be created for this value self._workaround_ignore = True return @@ -802,6 +802,12 @@ class ZWaveDeviceEntityValues(): self._workaround_ignore = True return + device.old_entity_id = "{}.{}".format( + component, object_id(self.primary)) + device.new_entity_id = "{}.{}".format(component, slugify(device.name)) + if not self._zwave_config[DOMAIN][CONF_NEW_ENTITY_IDS]: + device.entity_id = device.old_entity_id + self._entity = device dict_id = id(self) @@ -828,7 +834,6 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values = values self.node = values.primary.node self.values.primary.set_change_verified(False) - self.entity_id = "{}.{}".format(domain, object_id(values.primary)) self._name = _value_name(self.values.primary) self._unique_id = "ZWAVE-{}-{}".format(self.node.node_id, @@ -895,6 +900,10 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): """Return the device specific state attributes.""" attrs = { const.ATTR_NODE_ID: self.node_id, + const.ATTR_VALUE_INDEX: self.values.primary.index, + const.ATTR_VALUE_INSTANCE: self.values.primary.instance, + 'old_entity_id': self.old_entity_id, + 'new_entity_id': self.new_entity_id, } if self.power_consumption is not None: diff --git a/homeassistant/components/zwave/api.py b/homeassistant/components/zwave/api.py index 85e7b9c0f8f..181ab4ae18c 100644 --- a/homeassistant/components/zwave/api.py +++ b/homeassistant/components/zwave/api.py @@ -31,6 +31,8 @@ class ZWaveNodeValueView(HomeAssistantView): values_data[entity_values.primary.value_id] = { 'label': entity_values.primary.label, + 'index': entity_values.primary.index, + 'instance': entity_values.primary.instance, } return self.json(values_data) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 57e77fe49cb..4b18ef46475 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -14,6 +14,8 @@ ATTR_BASIC_LEVEL = "basic_level" ATTR_CONFIG_PARAMETER = "parameter" ATTR_CONFIG_SIZE = "size" ATTR_CONFIG_VALUE = "value" +ATTR_VALUE_INDEX = "value_index" +ATTR_VALUE_INSTANCE = "value_instance" NETWORK_READY_WAIT_SECS = 30 DISCOVERY_DEVICE = 'device' diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 5a441114f55..3a810d00d2d 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -2,11 +2,13 @@ import logging from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .const import ATTR_NODE_ID, DOMAIN, COMMAND_CLASS_WAKE_UP +from .const import ( + ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_BASIC_LEVEL, + EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, DOMAIN) from .util import node_name _LOGGER = logging.getLogger(__name__) @@ -38,6 +40,8 @@ class ZWaveBaseEntity(Entity): def __init__(self): """Initialize the base Z-Wave class.""" self._update_scheduled = False + self.old_entity_id = None + self.new_entity_id = None def maybe_schedule_update(self): """Maybe schedule state update. @@ -72,7 +76,7 @@ def sub_status(status, stage): class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" - def __init__(self, node, network): + def __init__(self, node, network, new_entity_ids): """Initialize node.""" # pylint: disable=import-error super().__init__() @@ -84,8 +88,11 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name - self.entity_id = "{}.{}_{}".format( + self.old_entity_id = "{}.{}_{}".format( DOMAIN, slugify(self._name), self.node_id) + self.new_entity_id = "{}.{}".format(DOMAIN, slugify(self._name)) + if not new_entity_ids: + self.entity_id = self.old_entity_id self._attributes = {} self.wakeup_interval = None self.location = None @@ -95,6 +102,10 @@ class ZWaveNodeEntity(ZWaveBaseEntity): dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NODE) dispatcher.connect( self.network_node_changed, ZWaveNetwork.SIGNAL_NOTIFICATION) + dispatcher.connect( + self.network_node_event, ZWaveNetwork.SIGNAL_NODE_EVENT) + dispatcher.connect( + self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT) def network_node_changed(self, node=None, args=None): """Handle a changed node on the network.""" @@ -134,6 +145,38 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.maybe_schedule_update() + def network_node_event(self, node, value): + """Handle a node activated event on the network.""" + if node.node_id == self.node.node_id: + self.node_event(value) + + def node_event(self, value): + """Handle a node activated event for this node.""" + if self.hass is None: + return + + self.hass.bus.fire(EVENT_NODE_EVENT, { + ATTR_ENTITY_ID: self.entity_id, + ATTR_NODE_ID: self.node.node_id, + ATTR_BASIC_LEVEL: value + }) + + def network_scene_activated(self, node, scene_id): + """Handle a scene activated event on the network.""" + if node.node_id == self.node.node_id: + self.scene_activated(scene_id) + + def scene_activated(self, scene_id): + """Handle an activated scene for this node.""" + if self.hass is None: + return + + self.hass.bus.fire(EVENT_SCENE_ACTIVATED, { + ATTR_ENTITY_ID: self.entity_id, + ATTR_NODE_ID: self.node.node_id, + ATTR_SCENE_ID: scene_id + }) + @property def state(self): """Return the state.""" @@ -169,6 +212,8 @@ class ZWaveNodeEntity(ZWaveBaseEntity): ATTR_NODE_NAME: self._name, ATTR_MANUFACTURER_NAME: self._manufacturer_name, ATTR_PRODUCT_NAME: self._product_name, + 'old_entity_id': self.old_entity_id, + 'new_entity_id': self.new_entity_id, } attrs.update(self._attributes) if self.battery_level is not None: diff --git a/tests/components/zwave/test_api.py b/tests/components/zwave/test_api.py index cf597f4104c..5fae8b0f317 100644 --- a/tests/components/zwave/test_api.py +++ b/tests/components/zwave/test_api.py @@ -16,7 +16,8 @@ def test_get_values(hass, test_client): ZWaveNodeValueView().register(app.router) node = MockNode(node_id=1) - value = MockValue(value_id=123456, node=node, label='Test Label') + value = MockValue(value_id=123456, node=node, label='Test Label', + instance=1, index=2) values = MockEntityValues(primary=value) node2 = MockNode(node_id=2) value2 = MockValue(value_id=234567, node=node2, label='Test Label 2') @@ -33,6 +34,8 @@ def test_get_values(hass, test_client): assert result == { '123456': { 'label': 'Test Label', + 'instance': 1, + 'index': 2, } } diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index eac33168fb7..0baa299c27c 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -214,7 +214,9 @@ def test_node_discovery(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) assert len(mock_receivers) == 1 @@ -222,7 +224,7 @@ def test_node_discovery(hass, mock_openzwave): hass.async_add_job(mock_receivers[0], node) yield from hass.async_block_till_done() - assert hass.states.get('zwave.mock_node_14').state is 'unknown' + assert hass.states.get('zwave.mock_node').state is 'unknown' @asyncio.coroutine @@ -236,8 +238,9 @@ def test_node_ignored(hass, mock_openzwave): with patch('pydispatch.dispatcher.connect', new=mock_connect): yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, 'device_config': { - 'zwave.mock_node_14': { + 'zwave.mock_node': { 'ignored': True, }}}}) @@ -247,7 +250,7 @@ def test_node_ignored(hass, mock_openzwave): hass.async_add_job(mock_receivers[0], node) yield from hass.async_block_till_done() - assert hass.states.get('zwave.mock_node_14') is None + assert hass.states.get('zwave.mock_node') is None @asyncio.coroutine @@ -260,7 +263,9 @@ def test_value_discovery(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) assert len(mock_receivers) == 1 @@ -272,7 +277,7 @@ def test_value_discovery(hass, mock_openzwave): yield from hass.async_block_till_done() assert hass.states.get( - 'binary_sensor.mock_node_mock_value_11_12_13').state is 'off' + 'binary_sensor.mock_node_mock_value').state is 'off' @asyncio.coroutine @@ -285,7 +290,9 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) assert len(mock_receivers) == 1 @@ -297,9 +304,9 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): hass.async_add_job(mock_receivers[0], node, setpoint) yield from hass.async_block_till_done() - assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + assert hass.states.get('climate.mock_node_mock_value').attributes[ 'temperature'] == 22.0 - assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + assert hass.states.get('climate.mock_node_mock_value').attributes[ 'current_temperature'] is None def mock_update(self): @@ -314,75 +321,12 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): hass.async_add_job(mock_receivers[0], node, temperature) yield from hass.async_block_till_done() - assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + assert hass.states.get('climate.mock_node_mock_value').attributes[ 'temperature'] == 22.0 - assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + assert hass.states.get('climate.mock_node_mock_value').attributes[ 'current_temperature'] == 23.5 -@asyncio.coroutine -def test_scene_activated(hass, mock_openzwave): - """Test scene activated event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_SCENE_EVENT: - mock_receivers.append(receiver) - - with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_SCENE_ACTIVATED, listener) - - node = MockNode(node_id=11) - scene_id = 123 - hass.async_add_job(mock_receivers[0], node, scene_id) - yield from hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].data[ATTR_ENTITY_ID] == "mock_node_11" - assert events[0].data[const.ATTR_OBJECT_ID] == "mock_node_11" - assert events[0].data[const.ATTR_SCENE_ID] == scene_id - - -@asyncio.coroutine -def test_node_event_activated(hass, mock_openzwave): - """Test Node event activated event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_NODE_EVENT: - mock_receivers.append(receiver) - - with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_NODE_EVENT, listener) - - node = MockNode(node_id=11) - value = 234 - hass.async_add_job(mock_receivers[0], node, value) - yield from hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].data[const.ATTR_OBJECT_ID] == "mock_node_11" - assert events[0].data[const.ATTR_BASIC_LEVEL] == value - - @asyncio.coroutine def test_network_ready(hass, mock_openzwave): """Test Node network ready event.""" @@ -393,7 +337,9 @@ def test_network_ready(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) assert len(mock_receivers) == 1 @@ -420,7 +366,9 @@ def test_network_complete(hass, mock_openzwave): mock_receivers.append(receiver) with patch('pydispatch.dispatcher.connect', new=mock_connect): - yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) assert len(mock_receivers) == 1 @@ -450,7 +398,9 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.hass = get_test_home_assistant() self.hass.start() - setup_component(self.hass, 'zwave', {'zwave': {}}) + setup_component(self.hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) self.hass.block_till_done() self.node = MockNode() @@ -478,9 +428,10 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.no_match_value = MockValue( command_class='mock_bad_class', node=self.node) - self.entity_id = '{}.{}'.format('mock_component', - zwave.object_id(self.primary)) - self.zwave_config = {} + self.entity_id = 'mock_component.mock_node_mock_value' + self.zwave_config = {'zwave': { + 'new_entity_ids': True, + }} self.device_config = {self.entity_id: {}} def tearDown(self): # pylint: disable=invalid-name @@ -491,6 +442,11 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_discovery(self, discovery, get_platform): """Test the creation of a new entity.""" + mock_platform = MagicMock() + get_platform.return_value = mock_platform + mock_device = MagicMock() + mock_device.name = 'test_device' + mock_platform.get_device.return_value = mock_device values = zwave.ZWaveDeviceEntityValues( hass=self.hass, schema=self.mock_schema, @@ -550,6 +506,11 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_existing_values(self, discovery, get_platform): """Test the loading of already discovered values.""" + mock_platform = MagicMock() + get_platform.return_value = mock_platform + mock_device = MagicMock() + mock_device.name = 'test_device' + mock_platform.get_device.return_value = mock_device self.node.values = { self.primary.value_id: self.primary, self.secondary.value_id: self.secondary, @@ -613,11 +574,15 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): """Test ignore workaround.""" + mock_platform = MagicMock() + get_platform.return_value = mock_platform + mock_device = MagicMock() + mock_device.name = 'test_device' + mock_platform.get_device.return_value = mock_device self.node.manufacturer_id = '010f' self.node.product_type = '0b00' self.primary.command_class = const.COMMAND_CLASS_SENSOR_ALARM - self.entity_id = '{}.{}'.format('binary_sensor', - zwave.object_id(self.primary)) + self.entity_id = 'binary_sensor.mock_node_mock_value' self.device_config = {self.entity_id: {}} self.mock_schema = { @@ -721,6 +686,11 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_config_polling_intensity(self, discovery, get_platform): """Test polling intensity.""" + mock_platform = MagicMock() + get_platform.return_value = mock_platform + mock_device = MagicMock() + mock_device.name = 'test_device' + mock_platform.get_device.return_value = mock_device self.node.values = { self.primary.value_id: self.primary, self.secondary.value_id: self.secondary, @@ -770,7 +740,9 @@ class TestZWaveServices(unittest.TestCase): self.hass.start() # Initialize zwave - setup_component(self.hass, 'zwave', {'zwave': {}}) + setup_component(self.hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) self.hass.block_till_done() self.zwave_network = self.hass.data[DATA_NETWORK] self.zwave_network.state = MockNetwork.STATE_READY diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 73e8e163096..b7148dd982e 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -4,7 +4,8 @@ import unittest from unittest.mock import patch, MagicMock import tests.mock.zwave as mock_zwave import pytest -from homeassistant.components.zwave import node_entity +from homeassistant.components.zwave import node_entity, const +from homeassistant.const import ATTR_ENTITY_ID @asyncio.coroutine @@ -30,6 +31,92 @@ def test_maybe_schedule_update(hass, mock_openzwave): assert len(mock_call_later.mock_calls) == 2 +@asyncio.coroutine +def test_node_event_activated(hass, mock_openzwave): + """Test Node event activated event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == mock_zwave.MockNetwork.SIGNAL_NODE_EVENT: + mock_receivers.append(receiver) + + node = mock_zwave.MockNode(node_id=11) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_NODE_EVENT, listener) + + # Test event before entity added to hass + value = 234 + hass.async_add_job(mock_receivers[0], node, value) + yield from hass.async_block_till_done() + assert len(events) == 0 + + # Add entity to hass + entity.hass = hass + entity.entity_id = 'zwave.mock_node' + + value = 234 + hass.async_add_job(mock_receivers[0], node, value) + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data[ATTR_ENTITY_ID] == "zwave.mock_node" + assert events[0].data[const.ATTR_NODE_ID] == 11 + assert events[0].data[const.ATTR_BASIC_LEVEL] == value + + +@asyncio.coroutine +def test_scene_activated(hass, mock_openzwave): + """Test scene activated event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == mock_zwave.MockNetwork.SIGNAL_SCENE_EVENT: + mock_receivers.append(receiver) + + node = mock_zwave.MockNode(node_id=11) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave, True) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_SCENE_ACTIVATED, listener) + + # Test event before entity added to hass + scene_id = 123 + hass.async_add_job(mock_receivers[0], node, scene_id) + yield from hass.async_block_till_done() + assert len(events) == 0 + + # Add entity to hass + entity.hass = hass + entity.entity_id = 'zwave.mock_node' + + scene_id = 123 + hass.async_add_job(mock_receivers[0], node, scene_id) + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data[ATTR_ENTITY_ID] == "zwave.mock_node" + assert events[0].data[const.ATTR_NODE_ID] == 11 + assert events[0].data[const.ATTR_SCENE_ID] == scene_id + + @pytest.mark.usefixtures('mock_openzwave') class TestZWaveNodeEntity(unittest.TestCase): """Class to test ZWaveNodeEntity.""" @@ -44,7 +131,7 @@ class TestZWaveNodeEntity(unittest.TestCase): self.node.manufacturer_name = 'Test Manufacturer' self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, - self.zwave_network) + self.zwave_network, True) def test_network_node_changed_from_value(self): """Test for network_node_changed.""" @@ -85,6 +172,8 @@ class TestZWaveNodeEntity(unittest.TestCase): {'node_id': self.node.node_id, 'node_name': 'Mock Node', 'manufacturer_name': 'Test Manufacturer', + 'old_entity_id': 'zwave.mock_node_567', + 'new_entity_id': 'zwave.mock_node', 'product_name': 'Test Product'}, self.entity.device_state_attributes) @@ -143,6 +232,8 @@ class TestZWaveNodeEntity(unittest.TestCase): {'node_id': self.node.node_id, 'node_name': 'Mock Node', 'manufacturer_name': 'Test Manufacturer', + 'old_entity_id': 'zwave.mock_node_567', + 'new_entity_id': 'zwave.mock_node', 'product_name': 'Test Product', 'query_stage': 'Dynamic', 'is_awake': True, From d24b45054a34cb159d3cc460f81728ce3ed3def7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 16 Jun 2017 11:47:48 +0200 Subject: [PATCH 03/12] Update numpy 1.13.0 (#8059) --- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index f19d50300b8..0de209639a8 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -17,7 +17,7 @@ from homeassistant.components.image_processing import ( ImageProcessingEntity) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.12.0'] +REQUIREMENTS = ['numpy==1.13.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c4032e65c77..89283b0f5b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -398,7 +398,7 @@ netdisco==1.0.1 neurio==0.3.1 # homeassistant.components.image_processing.opencv -numpy==1.12.0 +numpy==1.13.0 # homeassistant.components.google oauth2client==4.0.0 From d796e8db5c3fec13ef286a70bf8b5a0cb9597947 Mon Sep 17 00:00:00 2001 From: pezinek Date: Fri, 16 Jun 2017 14:55:59 +0200 Subject: [PATCH 04/12] No update in MQTT Binary Sensor #7478 (#8057) --- homeassistant/components/mqtt/__init__.py | 2 +- tests/components/mqtt/test_init.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 16ac00e3b7b..7c5d1a4faab 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -646,7 +646,7 @@ def _match_topic(subscription, topic): if sub_part == "+": reg_ex_parts.append(r"([^\/]+)") else: - reg_ex_parts.append(sub_part) + reg_ex_parts.append(re.escape(sub_part)) reg_ex = "^" + (r'\/'.join(reg_ex_parts)) + suffix + "$" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 0ef512edcd6..3be3d5d5ef6 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -249,6 +249,19 @@ class TestMQTT(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(self.calls)) + def test_subscribe_special_characters(self): + """Test the subscription to topics with special characters.""" + topic = '/test-topic/$(.)[^]{-}' + payload = 'p4y.l[]a|> ?' + + mqtt.subscribe(self.hass, topic, self.record_calls) + + fire_mqtt_message(self.hass, topic, payload) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual(topic, self.calls[0][0]) + self.assertEqual(payload, self.calls[0][1]) + def test_subscribe_binary_topic(self): """Test the subscription to a binary topic.""" mqtt.subscribe(self.hass, 'test-topic', self.record_calls, From 3ea7dee83d5fe996d4d000834ccb446c36a8e8ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Jun 2017 17:17:18 -0700 Subject: [PATCH 05/12] Always enable monkey patch (#8054) --- homeassistant/__main__.py | 51 ++------------------------------------- 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index c02d8c8bfc6..75aaeaa1fd1 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -31,48 +31,6 @@ def attempt_use_uvloop(): pass -def monkey_patch_asyncio(): - """Replace weakref.WeakSet to address Python 3 bug. - - Under heavy threading operations that schedule calls into - the asyncio event loop, Task objects are created. Due to - a bug in Python, GC may have an issue when switching between - the threads and objects with __del__ (which various components - in HASS have). - - This monkey-patch removes the weakref.Weakset, and replaces it - with an object that ignores the only call utilizing it (the - Task.__init__ which calls _all_tasks.add(self)). It also removes - the __del__ which could trigger the future objects __del__ at - unpredictable times. - - The side-effect of this manipulation of the Task is that - Task.all_tasks() is no longer accurate, and there will be no - warning emitted if a Task is GC'd while in use. - - On Python 3.6, after the bug is fixed, this monkey-patch can be - disabled. - - See https://bugs.python.org/issue26617 for details of the Python - bug. - """ - # pylint: disable=no-self-use, protected-access, bare-except - import asyncio.tasks - - class IgnoreCalls: - """Ignore add calls.""" - - def add(self, other): - """No-op add.""" - return - - asyncio.tasks.Task._all_tasks = IgnoreCalls() - try: - del asyncio.tasks.Task.__del__ - except: - pass - - def validate_python() -> None: """Validate that the right Python version is running.""" if sys.platform == "win32" and \ @@ -374,18 +332,13 @@ def main() -> int: """Start Home Assistant.""" validate_python() - if os.environ.get('HASS_MONKEYPATCH_ASYNCIO') == '1': - if sys.version_info[:3] >= (3, 6): + if os.environ.get('HASS_NO_MONKEY') != '1': + if sys.version_info[:2] >= (3, 6): monkey_patch.disable_c_asyncio() monkey_patch.patch_weakref_tasks() - elif sys.version_info[:3] < (3, 5, 3): - monkey_patch.patch_weakref_tasks() attempt_use_uvloop() - if sys.version_info[:3] < (3, 5, 3): - monkey_patch_asyncio() - args = get_arguments() if args.script is not None: From bf495edbb5091b4b88c706b669f383dbe9c524c7 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 17 Jun 2017 20:02:37 +0300 Subject: [PATCH 06/12] Add to zwave services descriptions (#8072) --- homeassistant/components/zwave/services.yaml | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 4a7379fbf56..3ca72c7fdda 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -3,41 +3,46 @@ change_association: fields: association: description: Specify add or remove assosication + example: add node_id: description: Node id of the node to set association for. + example: 10 target_node_id: description: Node id of the node to associate to. + example: 42 group: description: Group number to set association for. instance: - description: (Optional) Instance of association. Defaults to 0. + description: (Optional) Instance of multichannel association. Defaults to 0. add_node: - description: Add a new node to the Z-Wave network. Refer to OZW.log for details. + description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW.log for progress. add_node_secure: - description: Add a new node to the Z-Wave network with secure communications. Node must support this, and network key must be set. Refer to OZW.log for details. + description: Add a new node to the Z-Wave network with secure communications. Node must support this, and network key must be set. Note that unsecure devices can't directly talk to secure devices. Refer to OZW.log for progress. cancel_command: description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you wasn't going to use it but activated it. heal_network: - description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for details. + description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress. remove_node: - description: Remove a node from the Z-Wave network. Refer to OZW.log for details. + description: Remove a node from the Z-Wave network. Refer to OZW.log for progress. remove_failed_node: - descsription: This command will remove a failed node from the network. The node should be on the controllers failed nodes list, otherwise this command will fail. Refer to OZW.log for details. + descsription: This command will remove a failed node from the network. The node should be on the controllers failed nodes list, otherwise this command will fail. Refer to OZW.log for progress. fields: node_id: description: Node id of the device to remove (integer). + example: 10 replace_failed_node: - descsription: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW.log for details. + descsription: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW.log for progress. fields: node_id: description: Node id of the device to replace (integer). + example: 10 set_config_parameter: description: Set a config parameter to a node on the Z-Wave network. @@ -97,7 +102,7 @@ soft_reset: description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to controllers manual. test_network: - description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW.log for details. + description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW.log for progress. rename_node: description: Set the name(s) of a node. From a250f583ebcd6de658654e89a12482adbc85366c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 17 Jun 2017 19:03:49 +0200 Subject: [PATCH 07/12] Fix attribute entity (#8066) * Bugfix entity attribute setter * Fix tests * Fix tests part 2 * Change filter only None * Fix tests part 3 * Update entity.py * Fix tests --- homeassistant/helpers/entity.py | 4 ++-- tests/components/light/test_mqtt.py | 6 +++--- tests/components/light/test_mqtt_json.py | 4 ++-- tests/components/light/test_mqtt_template.py | 6 +++--- tests/components/light/test_rflink.py | 2 +- tests/components/lock/test_mqtt.py | 2 +- tests/components/sensor/test_dsmr.py | 2 +- tests/components/sensor/test_template.py | 2 +- tests/components/switch/test_mqtt.py | 2 +- tests/components/switch/test_rflink.py | 2 +- tests/components/switch/test_template.py | 2 +- tests/components/test_group.py | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 767b3412caf..dc6c29ce735 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -146,7 +146,7 @@ class Entity(object): @property def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" - return False + return None @property def force_update(self) -> bool: @@ -321,7 +321,7 @@ class Entity(object): value = getattr(self, name) - if not value: + if value is None: return try: diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index d3e56794712..97375aa6b13 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -228,7 +228,7 @@ class TestLightMQTT(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'test_light_rgb/status', '1') self.hass.block_till_done() @@ -320,7 +320,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) self.assertIsNone(state.attributes.get('brightness')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'test_scale/status', 'on') self.hass.block_till_done() @@ -367,7 +367,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'test_scale/status', 'on') self.hass.block_till_done() diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index d3cbf2bda00..10bb3f030e9 100755 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -175,7 +175,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # Turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', @@ -424,7 +424,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # Turn on the light fire_mqtt_message(self.hass, 'test_light_rgb', diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 99a91f8f6cc..a28d862bf53 100755 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -88,7 +88,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'test_light_rgb', 'on') self.hass.block_till_done() @@ -141,7 +141,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('color_temp')) self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', @@ -401,7 +401,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertIsNone(state.attributes.get('color_temp')) self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index 0d34bb6a90f..03180c47a4a 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -70,7 +70,7 @@ def test_default_setup(hass, monkeypatch): light_after_first_command = hass.states.get(DOMAIN + '.test') assert light_after_first_command.state == 'on' # also after receiving first command state not longer has to be assumed - assert 'assumed_state' not in light_after_first_command.attributes + assert not light_after_first_command.attributes.get('assumed_state') # mock incoming command event for this device event_callback({ diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index 5815329717c..c66ed5f2b26 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -36,7 +36,7 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNLOCKED, state.state) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'state-topic', 'LOCK') self.hass.block_till_done() diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 59e66ca82b6..86e637ab1ae 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -88,7 +88,7 @@ def test_default_setup(hass, mock_connection_factory): # tariff should be translated in human readable and have no unit power_tariff = hass.states.get('sensor.power_tariff') assert power_tariff.state == 'low' - assert power_tariff.attributes.get('unit_of_measurement') is None + assert power_tariff.attributes.get('unit_of_measurement') == '' @asyncio.coroutine diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 62a38abd317..efff5186854 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -72,7 +72,7 @@ class TestTemplateSensor: self.hass.block_till_done() state = self.hass.states.get('sensor.test_template_sensor') - assert 'icon' not in state.attributes + assert state.attributes.get('icon') == '' self.hass.states.set('sensor.test_state', 'Works') self.hass.block_till_done() diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index e5e68fe021e..8215eae26cc 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -35,7 +35,7 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'state-topic', '1') self.hass.block_till_done() diff --git a/tests/components/switch/test_rflink.py b/tests/components/switch/test_rflink.py index 3952b6f32bc..d48c9aca7a4 100644 --- a/tests/components/switch/test_rflink.py +++ b/tests/components/switch/test_rflink.py @@ -59,7 +59,7 @@ def test_default_setup(hass, monkeypatch): switch_after_first_command = hass.states.get('switch.test') assert switch_after_first_command.state == 'on' # also after receiving first command state not longer has to be assumed - assert 'assumed_state' not in switch_after_first_command.attributes + assert not switch_after_first_command.attributes.get('assumed_state') # mock incoming command event for this device event_callback({ diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 0ef3d505e5a..f7e9b7d730c 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -161,7 +161,7 @@ class TestTemplateSwitch: self.hass.block_till_done() state = self.hass.states.get('switch.test_template_switch') - assert 'icon' not in state.attributes + assert state.attributes.get('icon') == '' state = self.hass.states.set('switch.test_state', STATE_ON) self.hass.block_till_done() diff --git a/tests/components/test_group.py b/tests/components/test_group.py index c9c413c30c1..d94ccaa385c 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -292,7 +292,7 @@ class TestComponentsGroup(unittest.TestCase): ['light.Bowl', 'light.Ceiling', 'sensor.no_exist']) state = self.hass.states.get(test_group.entity_id) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) self.hass.states.set('light.Bowl', STATE_ON, { ATTR_ASSUMED_STATE: True @@ -306,7 +306,7 @@ class TestComponentsGroup(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(test_group.entity_id) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) def test_group_updated_after_device_tracker_zone_change(self): """Test group state when device tracker in group changes zone.""" From 9fc22ee47afd56754b431b551b57dcf04dfbe9ae Mon Sep 17 00:00:00 2001 From: Lev Aronsky Date: Sat, 17 Jun 2017 20:22:23 +0300 Subject: [PATCH 08/12] Added 'all_plants' group and support for plant groups state. (#8063) * Added 'all_plants' group and support for plant groups state. * Reversed the group states. --- homeassistant/components/group.py | 6 ++++-- homeassistant/components/plant.py | 16 +++++++++++----- homeassistant/const.py | 2 ++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 7c992a277f8..c628d04679f 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -14,7 +14,8 @@ from homeassistant import config as conf_util, core as ha from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, - STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE, SERVICE_RELOAD) + STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, + ATTR_ASSUMED_STATE, SERVICE_RELOAD) from homeassistant.core import callback from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent @@ -94,7 +95,8 @@ CONFIG_SCHEMA = vol.Schema({ # List of ON/OFF state tuples for groupable states _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), - (STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)] + (STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED), + (STATE_PROBLEM, STATE_OK)] def _get_group_on_off(state): diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 2070c22fb97..cd43fbf715c 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -10,8 +10,9 @@ import asyncio import voluptuous as vol from homeassistant.const import ( - STATE_UNKNOWN, TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_SENSORS, - ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON) + STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, TEMP_CELSIUS, ATTR_TEMPERATURE, + CONF_SENSORS, ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON) +from homeassistant.components import group import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -69,6 +70,10 @@ PLANT_SCHEMA = vol.Schema({ }) DOMAIN = 'plant' +DEPENDENCIES = ['zone', 'group'] + +GROUP_NAME_ALL_PLANTS = 'all plants' +ENTITY_ID_ALL_PLANTS = group.ENTITY_ID_FORMAT.format('all_plants') CONFIG_SCHEMA = vol.Schema({ DOMAIN: { @@ -80,7 +85,8 @@ CONFIG_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): """Set up the Plant component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent(_LOGGER, DOMAIN, hass, + group_name=GROUP_NAME_ALL_PLANTS) entities = [] for plant_name, plant_config in config[DOMAIN].items(): @@ -199,11 +205,11 @@ class Plant(Entity): self._icon = params['icon'] if len(result) == 0: - self._state = 'ok' + self._state = STATE_OK self._icon = 'mdi:thumb-up' self._problems = PROBLEM_NONE else: - self._state = 'problem' + self._state = STATE_PROBLEM self._problems = ','.join(result) _LOGGER.debug("New data processed") self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/const.py b/homeassistant/const.py index 85c82a60728..ed7584c83ef 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -199,6 +199,8 @@ STATE_ALARM_TRIGGERED = 'triggered' STATE_LOCKED = 'locked' STATE_UNLOCKED = 'unlocked' STATE_UNAVAILABLE = 'unavailable' +STATE_OK = 'ok' +STATE_PROBLEM = 'problem' # #### STATE AND EVENT ATTRIBUTES #### # Attribution From 363a429c41a5318c3950bbbe70d7a9d947c653c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 17 Jun 2017 10:50:59 -0700 Subject: [PATCH 09/12] Fix EntityComponent handle entities without a name (#8065) * Fix EntityComponent handle entities without a name * Implement solution by Anders --- homeassistant/helpers/entity_component.py | 3 ++- tests/helpers/test_entity_component.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f7cf23b21fd..8cfc9984e2e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -238,7 +238,8 @@ class EntityComponent(object): This method must be run in the event loop. """ if self.group_name is not None: - ids = sorted(self.entities, key=lambda x: self.entities[x].name) + ids = sorted(self.entities, + key=lambda x: self.entities[x].name or x) group = get_component('group') group.async_set_group( self.hass, slugify(self.group_name), name=self.group_name, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 530e2662083..f68090358c7 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -84,7 +84,7 @@ class TestHelpersEntityComponent(unittest.TestCase): # No group after setup assert len(self.hass.states.entity_ids()) == 0 - component.add_entities([EntityTest(name='hello')]) + component.add_entities([EntityTest()]) # group exists assert len(self.hass.states.entity_ids()) == 2 @@ -92,7 +92,8 @@ class TestHelpersEntityComponent(unittest.TestCase): group = self.hass.states.get('group.everyone') - assert group.attributes.get('entity_id') == ('test_domain.hello',) + assert group.attributes.get('entity_id') == \ + ('test_domain.unnamed_device',) # group extended component.add_entities([EntityTest(name='goodbye')]) @@ -100,9 +101,9 @@ class TestHelpersEntityComponent(unittest.TestCase): assert len(self.hass.states.entity_ids()) == 3 group = self.hass.states.get('group.everyone') - # Sorted order + # Ordered in order of added to the group assert group.attributes.get('entity_id') == \ - ('test_domain.goodbye', 'test_domain.hello') + ('test_domain.goodbye', 'test_domain.unnamed_device') def test_polling_only_updates_entities_it_should_poll(self): """Test the polling of only updated entities.""" From a2fbc0d2ef3f0ab69efb00a3e379f1ea3626633e Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 17 Jun 2017 13:09:27 -0500 Subject: [PATCH 10/12] Update pyunifi component to use APIError passed from pyunifi 2.13. Better accommodate login failures with wrapper in pyunifi 2.13. (#7899) * Pyunifi update * Update pyunifi_test * Import API Error * Adjust test_unifi.py to import APIError * Remove urllib import * Remove urllib import from test * Try fix mock * Remove automations.yaml * Lint --- homeassistant/components/device_tracker/unifi.py | 10 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 3 +++ script/gen_requirements_all.py | 1 + tests/components/device_tracker/test_unifi.py | 13 +++++-------- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index b0409e99883..29c997b4dac 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.unifi/ """ import logging -import urllib import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -15,7 +14,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_VERIFY_SSL -REQUIREMENTS = ['pyunifi==2.12'] +REQUIREMENTS = ['pyunifi==2.13'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' @@ -40,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_scanner(hass, config): """Set up the Unifi device_tracker.""" - from pyunifi.controller import Controller + from pyunifi.controller import Controller, APIError host = config[DOMAIN].get(CONF_HOST) username = config[DOMAIN].get(CONF_USERNAME) @@ -53,7 +52,7 @@ def get_scanner(hass, config): try: ctrl = Controller(host, username, password, port, version='v4', site_id=site_id, ssl_verify=verify_ssl) - except urllib.error.HTTPError as ex: + except APIError as ex: _LOGGER.error("Failed to connect to Unifi: %s", ex) persistent_notification.create( hass, 'Failed to connect to Unifi. ' @@ -77,9 +76,10 @@ class UnifiScanner(DeviceScanner): def _update(self): """Get the clients from the device.""" + from pyunifi.controller import APIError try: clients = self._controller.get_clients() - except urllib.error.HTTPError as ex: + except APIError as ex: _LOGGER.error("Failed to scan clients: %s", ex) clients = [] diff --git a/requirements_all.txt b/requirements_all.txt index 89283b0f5b9..248ab48404c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -742,7 +742,7 @@ pytrackr==0.0.5 pytradfri==1.1 # homeassistant.components.device_tracker.unifi -pyunifi==2.12 +pyunifi==2.13 # homeassistant.components.keyboard # pyuserinput==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49b6f2ae2f5..d64f2642ec4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,6 +100,9 @@ pynx584==0.4 # homeassistant.components.sensor.darksky python-forecastio==1.3.5 +# homeassistant.components.device_tracker.unifi +pyunifi==2.13 + # homeassistant.components.notify.html5 pywebpush==1.0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 833c351b750..92617a4ad60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -67,6 +67,7 @@ TEST_REQUIREMENTS = ( 'pywebpush', 'PyJWT', 'restrictedpython', + 'pyunifi', ) IGNORE_PACKAGES = ( diff --git a/tests/components/device_tracker/test_unifi.py b/tests/components/device_tracker/test_unifi.py index eea52637241..d62897a86c4 100644 --- a/tests/components/device_tracker/test_unifi.py +++ b/tests/components/device_tracker/test_unifi.py @@ -1,6 +1,6 @@ """The tests for the Unifi WAP device tracker platform.""" from unittest import mock -import urllib +from pyunifi.controller import APIError import pytest import voluptuous as vol @@ -13,11 +13,8 @@ from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, @pytest.fixture def mock_ctrl(): """Mock pyunifi.""" - module = mock.MagicMock() - with mock.patch.dict('sys.modules', { - 'pyunifi.controller': module.controller, - }): - yield module.controller.Controller + with mock.patch('pyunifi.controller.Controller') as mock_control: + yield mock_control @pytest.fixture @@ -100,7 +97,7 @@ def test_config_controller_failed(hass, mock_ctrl, mock_scanner): CONF_PASSWORD: 'password', } } - mock_ctrl.side_effect = urllib.error.HTTPError( + mock_ctrl.side_effect = APIError( '/', 500, 'foo', {}, None) result = unifi.get_scanner(hass, config) assert result is False @@ -122,7 +119,7 @@ def test_scanner_update(): def test_scanner_update_error(): """Test the scanner update for error.""" ctrl = mock.MagicMock() - ctrl.get_clients.side_effect = urllib.error.HTTPError( + ctrl.get_clients.side_effect = APIError( '/', 500, 'foo', {}, None) unifi.UnifiScanner(ctrl) From 84aab1c9738dea951bc3f55561ee3eb92673845f Mon Sep 17 00:00:00 2001 From: happyleavesaoc Date: Sat, 17 Jun 2017 12:42:56 -0400 Subject: [PATCH 11/12] bump usps version (#8074) --- homeassistant/components/sensor/usps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/usps.py b/homeassistant/components/sensor/usps.py index 4157364eb4b..1e818587a72 100644 --- a/homeassistant/components/sensor/usps.py +++ b/homeassistant/components/sensor/usps.py @@ -18,7 +18,7 @@ from homeassistant.util import slugify from homeassistant.util.dt import now, parse_datetime import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['myusps==1.1.1'] +REQUIREMENTS = ['myusps==1.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 248ab48404c..17578fe7833 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -385,7 +385,7 @@ miflora==0.1.16 mutagen==1.37.0 # homeassistant.components.sensor.usps -myusps==1.1.1 +myusps==1.1.2 # homeassistant.components.media_player.nad # homeassistant.components.media_player.nadtcp From 8fffaebe50683d485440ebd1ba38bde17e25bd63 Mon Sep 17 00:00:00 2001 From: happyleavesaoc Date: Sat, 17 Jun 2017 12:42:12 -0400 Subject: [PATCH 12/12] bump ups (#8075) --- homeassistant/components/sensor/ups.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py index cfb4dd7c9ce..2b2ae6172d0 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle from homeassistant.util.dt import now, parse_date import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['upsmychoice==1.0.4'] +REQUIREMENTS = ['upsmychoice==1.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 17578fe7833..d46518c82d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -875,7 +875,7 @@ twilio==5.7.0 uber_rides==0.4.1 # homeassistant.components.sensor.ups -upsmychoice==1.0.4 +upsmychoice==1.0.6 # homeassistant.components.camera.uvc uvcclient==0.10.0