diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 82927246ec6..38128489ba0 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -26,7 +26,7 @@ DEFAULT_NAME = 'SimpliSafe' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE): cv.positive_int, + vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 0ba85105c18..6c08bf391d8 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -70,8 +70,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]] value = node.values[discovery_info[ATTR_VALUE_ID]] value.set_change_verified(False) - if value.index != 1: # Only add 1 device - return add_devices([ZWaveClimate(value, temp_unit)]) _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", discovery_info, zwave.NETWORK) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 4247213087b..236fde6fb3f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -338,7 +338,7 @@ class Device(Entity): attr[ATTR_BATTERY] = self.battery if self.attributes: - for key, value in self.attributes: + for key, value in self.attributes.items(): attr[key] = value return attr diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 927c515b3a5..7855323ba06 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -142,6 +142,7 @@ class AutomaticDeviceScanner(object): for vehicle in self.last_results: dev_id = vehicle.get('id') + host_name = vehicle.get('display_name') attrs = { 'fuel_level': vehicle.get('fuel_level_percent') @@ -149,6 +150,7 @@ class AutomaticDeviceScanner(object): kwargs = { 'dev_id': dev_id, + 'host_name': host_name, 'mac': dev_id, ATTR_ATTRIBUTES: attrs } diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index c4cd177925d..41901d87e86 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -46,12 +46,12 @@ def _conf_preprocess(value): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: {cv.match_all: vol.Schema(vol.All(_conf_preprocess, { + DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, { vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), CONF_VIEW: cv.boolean, CONF_NAME: cv.string, CONF_ICON: cv.icon, - }))} + }, cv.match_all)) }, extra=vol.ALLOW_EXTRA) # List of ON/OFF state tuples for groupable states diff --git a/homeassistant/const.py b/homeassistant/const.py index eb8b65df998..797fd3108b9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 28 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 1be157c789d..009736024a1 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,4 +1,5 @@ """Helpers for config validation using voluptuous.""" +from collections import OrderedDict from datetime import timedelta import os from urllib.parse import urlparse @@ -290,6 +291,27 @@ def url(value: Any) -> str: raise vol.Invalid('invalid url') +def ordered_dict(value_validator, key_validator=match_all): + """Validate an ordered dict validator that maintains ordering. + + value_validator will be applied to each value of the dictionary. + key_validator (optional) will be applied to each key of the dictionary. + """ + item_validator = vol.Schema({key_validator: value_validator}) + + def validator(value): + """Validate ordered dict.""" + config = OrderedDict() + + for key, val in value.items(): + v_res = item_validator({key: val}) + config.update(v_res) + + return config + + return validator + + # Validator helpers def key_dependency(key, dependency): diff --git a/tests/components/test_group.py b/tests/components/test_group.py index e82190a3f29..6c601a411fb 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -1,5 +1,6 @@ """The tests for the Group components.""" # pylint: disable=protected-access,too-many-public-methods +from collections import OrderedDict import unittest from unittest.mock import patch @@ -220,16 +221,16 @@ class TestComponentsGroup(unittest.TestCase): test_group = group.Group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) - _setup_component(self.hass, 'group', {'group': { - 'second_group': { + group_conf = OrderedDict() + group_conf['second_group'] = { 'entities': 'light.Bowl, ' + test_group.entity_id, 'icon': 'mdi:work', 'view': True, - }, - 'test_group': 'hello.world,sensor.happy', - 'empty_group': {'name': 'Empty Group', 'entities': None}, - } - }) + } + group_conf['test_group'] = 'hello.world,sensor.happy' + group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None} + + _setup_component(self.hass, 'group', {'group': group_conf}) group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('second_group')) @@ -241,6 +242,7 @@ class TestComponentsGroup(unittest.TestCase): group_state.attributes.get(ATTR_ICON)) self.assertTrue(group_state.attributes.get(group.ATTR_VIEW)) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) + self.assertEqual(1, group_state.attributes.get(group.ATTR_ORDER)) group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('test_group')) @@ -251,6 +253,7 @@ class TestComponentsGroup(unittest.TestCase): self.assertIsNone(group_state.attributes.get(ATTR_ICON)) self.assertIsNone(group_state.attributes.get(group.ATTR_VIEW)) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) + self.assertEqual(2, group_state.attributes.get(group.ATTR_ORDER)) def test_groups_get_unique_names(self): """Two groups with same name should both have a unique entity id.""" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index d9da2c51da7..60b14757378 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1,3 +1,5 @@ +"""Test config validators.""" +from collections import OrderedDict from datetime import timedelta import os import tempfile @@ -367,3 +369,51 @@ def test_has_at_least_one_key(): for value in ({'beer': None}, {'soda': None}): schema(value) + + +def test_ordered_dict_order(): + """Test ordered_dict validator.""" + schema = vol.Schema(cv.ordered_dict(int, cv.string)) + + val = OrderedDict() + val['first'] = 1 + val['second'] = 2 + + validated = schema(val) + + assert isinstance(validated, OrderedDict) + assert ['first', 'second'] == list(validated.keys()) + + +def test_ordered_dict_key_validator(): + """Test ordered_dict key validator.""" + schema = vol.Schema(cv.ordered_dict(cv.match_all, cv.string)) + + with pytest.raises(vol.Invalid): + schema({None: 1}) + + schema({'hello': 'world'}) + + schema = vol.Schema(cv.ordered_dict(cv.match_all, int)) + + with pytest.raises(vol.Invalid): + schema({'hello': 1}) + + schema({1: 'works'}) + + +def test_ordered_dict_value_validator(): + """Test ordered_dict validator.""" + schema = vol.Schema(cv.ordered_dict(cv.string)) + + with pytest.raises(vol.Invalid): + schema({'hello': None}) + + schema({'hello': 'world'}) + + schema = vol.Schema(cv.ordered_dict(int)) + + with pytest.raises(vol.Invalid): + schema({'hello': 'world'}) + + schema({'hello': 5})