From 730514cea8dab3bf4edbe0062ee8d7f139e680d5 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 00:27:11 -0400 Subject: [PATCH 01/19] Service validation for the thermostat component. --- .../components/thermostat/__init__.py | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index eb1b926a2e3..7e2ff9f2234 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -7,14 +7,16 @@ https://home-assistant.io/components/thermostat/ import logging import os +import voluptuous as vol + from homeassistant.helpers.entity_component import EntityComponent from homeassistant.config import load_yaml_config_file -import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import convert from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components import (ecobee, zwave) +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELCIUS) @@ -48,6 +50,19 @@ DISCOVERY_PLATFORMS = { zwave.DISCOVER_THERMOSTATS: 'zwave' } +SET_AWAY_MODE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_AWAY_MODE): cv.boolean, +}) +SET_TEMPERATURE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), +}) +SET_FAN_MODE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_FAN): cv.boolean, +}) + def set_away_mode(hass, away_mode, entity_id=None): """Turn all or specified thermostat away mode on.""" @@ -97,13 +112,7 @@ def setup(hass, config): """Set away mode on target thermostats.""" target_thermostats = component.extract_from_service(service) - away_mode = service.data.get(ATTR_AWAY_MODE) - - if away_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) - return + away_mode = service.data[ATTR_AWAY_MODE] for thermostat in target_thermostats: if away_mode: @@ -115,20 +124,14 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, - descriptions.get(SERVICE_SET_AWAY_MODE)) + descriptions.get(SERVICE_SET_AWAY_MODE), + schema=SET_AWAY_MODE_SCHEMA) def temperature_set_service(service): """Set temperature on the target thermostats.""" target_thermostats = component.extract_from_service(service) - temperature = util.convert( - service.data.get(ATTR_TEMPERATURE), float) - - if temperature is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE) - return + temperature = service.data[ATTR_TEMPERATURE] for thermostat in target_thermostats: thermostat.set_temperature(convert( @@ -139,19 +142,14 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, - descriptions.get(SERVICE_SET_TEMPERATURE)) + descriptions.get(SERVICE_SET_TEMPERATURE), + schema=SET_TEMPERATURE_SCHEMA) def fan_mode_set_service(service): """Set fan mode on target thermostats.""" target_thermostats = component.extract_from_service(service) - fan_mode = service.data.get(ATTR_FAN) - - if fan_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_FAN_MODE, ATTR_FAN) - return + fan_mode = service.data[ATTR_FAN] for thermostat in target_thermostats: if fan_mode: @@ -163,7 +161,8 @@ def setup(hass, config): hass.services.register( DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, - descriptions.get(SERVICE_SET_FAN_MODE)) + descriptions.get(SERVICE_SET_FAN_MODE), + schema=SET_FAN_MODE_SCHEMA) return True From 1deaf2fe8fda33f67229a2337c317a80146e770b Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 00:37:42 -0400 Subject: [PATCH 02/19] Service validation for switch component. --- homeassistant/components/switch/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index d807668fb73..8bd0585ff0c 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -8,10 +8,13 @@ from datetime import timedelta import logging import os +import voluptuous as vol + from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) @@ -50,6 +53,10 @@ PROP_TO_ATTR = { 'today_power_mw': ATTR_TODAY_MWH, } +SWITCH_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + _LOGGER = logging.getLogger(__name__) @@ -102,11 +109,14 @@ def setup(hass, config): descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service, - descriptions.get(SERVICE_TURN_OFF)) + descriptions.get(SERVICE_TURN_OFF), + schema=SWITCH_SERVICE_SCHEMA) hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service, - descriptions.get(SERVICE_TURN_ON)) + descriptions.get(SERVICE_TURN_ON), + schema=SWITCH_SERVICE_SCHEMA) hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_switch_service, - descriptions.get(SERVICE_TOGGLE)) + descriptions.get(SERVICE_TOGGLE), + schema=SWITCH_SERVICE_SCHEMA) return True From d90f31bf6e98a9b3ba8b766c07a1ec9af63fdbbd Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 00:52:39 -0400 Subject: [PATCH 03/19] Config and service validation for shell_command component. --- homeassistant/components/shell_command.py | 28 +++++++++++------------ tests/components/test_shell_command.py | 26 +++++++++++---------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index b2ee7fa4314..88df938d38c 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -7,26 +7,26 @@ https://home-assistant.io/components/shell_command/ import logging import subprocess -from homeassistant.util import slugify +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv DOMAIN = 'shell_command' _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +SHELL_COMMAND_SCHEMA = vol.Schema({}) + def setup(hass, config): """Setup the shell_command component.""" - conf = config.get(DOMAIN) - - if not isinstance(conf, dict): - _LOGGER.error('Expected configuration to be a dictionary') - return False - - for name in conf.keys(): - if name != slugify(name): - _LOGGER.error('Invalid service name: %s. Try %s', - name, slugify(name)) - return False + conf = config.get(DOMAIN, {}) def service_handler(call): """Execute a shell command service.""" @@ -38,6 +38,6 @@ def setup(hass, config): _LOGGER.exception('Error running command') for name in conf.keys(): - hass.services.register(DOMAIN, name, service_handler) - + hass.services.register(DOMAIN, name, service_handler, + schema=SHELL_COMMAND_SCHEMA) return True diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index f899411356d..a313a41b66a 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -5,6 +5,7 @@ import unittest from unittest.mock import patch from subprocess import SubprocessError +from homeassistant.bootstrap import _setup_component from homeassistant.components import shell_command from tests.common import get_test_home_assistant @@ -25,11 +26,11 @@ class TestShellCommand(unittest.TestCase): """Test if able to call a configured service.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'called.txt') - self.assertTrue(shell_command.setup(self.hass, { - 'shell_command': { + assert _setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { 'test_service': "date > {}".format(path) } - })) + }) self.hass.services.call('shell_command', 'test_service', blocking=True) @@ -38,16 +39,17 @@ class TestShellCommand(unittest.TestCase): def test_config_not_dict(self): """Test if config is not a dict.""" - self.assertFalse(shell_command.setup(self.hass, { - 'shell_command': ['some', 'weird', 'list'] - })) + assert not _setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: ['some', 'weird', 'list'] + }) def test_config_not_valid_service_names(self): """Test if config contains invalid service names.""" - self.assertFalse(shell_command.setup(self.hass, { - 'shell_command': { + assert not _setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { 'this is invalid because space': 'touch bla.txt' - }})) + } + }) @patch('homeassistant.components.shell_command.subprocess.call', side_effect=SubprocessError) @@ -56,11 +58,11 @@ class TestShellCommand(unittest.TestCase): """Test subprocess.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'called.txt') - self.assertTrue(shell_command.setup(self.hass, { - 'shell_command': { + assert _setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { 'test_service': "touch {}".format(path) } - })) + }) self.hass.services.call('shell_command', 'test_service', blocking=True) From 567d1065b2602db0ae4385d0359c9ae4033b4e11 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 01:26:21 -0400 Subject: [PATCH 04/19] Service validation for script component. --- homeassistant/components/script.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 3dde6f1690e..c19e614f19d 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -109,6 +109,11 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA} }, extra=vol.ALLOW_EXTRA) +SCRIPT_SERVICE_SCHEMA = vol.Schema({}) +SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + def is_on(hass, entity_id): """Return if the switch is on based on the statemachine.""" @@ -149,7 +154,8 @@ def setup(hass, config): alias = cfg.get(CONF_ALIAS, object_id) script = Script(object_id, alias, cfg[CONF_SEQUENCE]) component.add_entities((script,)) - hass.services.register(DOMAIN, object_id, service_handler) + hass.services.register(DOMAIN, object_id, service_handler, + schema=SCRIPT_SERVICE_SCHEMA) def turn_on_service(service): """Call a service to turn script on.""" @@ -168,10 +174,12 @@ def setup(hass, config): for script in component.extract_from_service(service): script.toggle() - hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service) - hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service) - hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service) - + hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service, + schema=SCRIPT_TURN_ONOFF_SCHEMA) + hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service, + schema=SCRIPT_TURN_ONOFF_SCHEMA) + hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service, + schema=SCRIPT_TURN_ONOFF_SCHEMA) return True From ad6f5d3b1d203aab86de74a87a02249544b5756e Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 02:01:22 -0400 Subject: [PATCH 05/19] Service validation for scene component. --- homeassistant/components/scene/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 03bb7c265c3..5ac7a2d9c86 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -7,9 +7,12 @@ https://home-assistant.io/components/scene/ import logging from collections import namedtuple +import voluptuous as vol + from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, CONF_PLATFORM) from homeassistant.helpers import extract_domain_configs +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -19,6 +22,10 @@ STATE = 'scening' CONF_ENTITIES = "entities" +SCENE_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + SceneConfig = namedtuple('SceneConfig', ['name', 'states']) @@ -61,7 +68,8 @@ def setup(hass, config): for scene in target_scenes: scene.activate() - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_scene_service) + hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_scene_service, + schema=SCENE_SERVICE_SCHEMA) return True From 40e36126bcbcfe859ac70540a462163f5b32cb78 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 02:25:21 -0400 Subject: [PATCH 06/19] Service validation for rollershutter component. --- .../components/rollershutter/__init__.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py index 5c90aa99fe2..98bee419802 100644 --- a/homeassistant/components/rollershutter/__init__.py +++ b/homeassistant/components/rollershutter/__init__.py @@ -7,10 +7,13 @@ https://home-assistant.io/components/rollershutter/ import os import logging +import voluptuous as vol + from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +import homeassistant.helpers.config_validation as cv from homeassistant.components import group from homeassistant.const import ( SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP, @@ -33,6 +36,10 @@ _LOGGER = logging.getLogger(__name__) ATTR_CURRENT_POSITION = 'current_position' +ROLLERSHUTTER_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + def is_open(hass, entity_id=None): """Return if the roller shutter is open based on the statemachine.""" @@ -85,14 +92,16 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_MOVE_UP, handle_rollershutter_service, - descriptions.get(SERVICE_MOVE_UP)) + descriptions.get(SERVICE_MOVE_UP), + schema=ROLLERSHUTTER_SERVICE_SCHEMA) hass.services.register(DOMAIN, SERVICE_MOVE_DOWN, handle_rollershutter_service, - descriptions.get(SERVICE_MOVE_DOWN)) + descriptions.get(SERVICE_MOVE_DOWN), + schema=ROLLERSHUTTER_SERVICE_SCHEMA) hass.services.register(DOMAIN, SERVICE_STOP, handle_rollershutter_service, - descriptions.get(SERVICE_STOP)) - + descriptions.get(SERVICE_STOP), + schema=ROLLERSHUTTER_SERVICE_SCHEMA) return True From 7ffc254a8776ad7b396a51f2888e76d22ac9e0ca Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 02:51:51 -0400 Subject: [PATCH 07/19] Service validation for notify component. --- homeassistant/components/notify/__init__.py | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 5be2bb1be64..4dacfcb350e 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -8,11 +8,13 @@ from functools import partial import logging import os +import voluptuous as vol + import homeassistant.bootstrap as bootstrap from homeassistant.config import load_yaml_config_file from homeassistant.helpers import config_per_platform, template from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa - +import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME DOMAIN = "notify" @@ -32,6 +34,13 @@ ATTR_DATA = 'data' SERVICE_NOTIFY = "notify" +NOTIFY_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string, + vol.Optional(ATTR_TARGET): cv.string, + vol.Optional(ATTR_DATA): dict, # nobody seems to be using this (yet) +}) + _LOGGER = logging.getLogger(__name__) @@ -71,13 +80,7 @@ def setup(hass, config): def notify_message(notify_service, call): """Handle sending notification message service calls.""" - message = call.data.get(ATTR_MESSAGE) - - if message is None: - _LOGGER.error( - 'Received call to %s without attribute %s', - call.service, ATTR_MESSAGE) - return + message = call.data[ATTR_MESSAGE] title = template.render( hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)) @@ -91,7 +94,8 @@ def setup(hass, config): service_call_handler = partial(notify_message, notify_service) service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY) hass.services.register(DOMAIN, service_notify, service_call_handler, - descriptions.get(SERVICE_NOTIFY)) + descriptions.get(SERVICE_NOTIFY), + schema=NOTIFY_SERVICE_SCHEMA) success = True return success From d6f31239372f608721757ffdb55085fc771bd8e7 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 21:01:35 -0400 Subject: [PATCH 08/19] Service validation for logbook component. --- homeassistant/components/logbook.py | 24 ++++++++++++++++-------- tests/components/test_logbook.py | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index d26b3373cb8..98740388cd5 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -9,6 +9,8 @@ import re from datetime import timedelta from itertools import groupby +import voluptuous as vol + import homeassistant.util.dt as dt_util from homeassistant.components import recorder, sun from homeassistant.const import ( @@ -18,6 +20,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.core import State from homeassistant.helpers.entity import split_entity_id from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv DOMAIN = "logbook" DEPENDENCIES = ['recorder', 'http'] @@ -39,6 +42,13 @@ ATTR_MESSAGE = 'message' ATTR_DOMAIN = 'domain' ATTR_ENTITY_ID = 'entity_id' +LOG_MESSAGE_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_DOMAIN): cv.slug, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, +}) + def log_entry(hass, name, message, domain=None, entity_id=None): """Add an entry to the logbook.""" @@ -58,19 +68,17 @@ def setup(hass, config): """Listen for download events to download files.""" def log_message(service): """Handle sending notification message service calls.""" - message = service.data.get(ATTR_MESSAGE) - name = service.data.get(ATTR_NAME) - domain = service.data.get(ATTR_DOMAIN, None) - entity_id = service.data.get(ATTR_ENTITY_ID, None) - - if not message or not name: - return + message = service.data[ATTR_MESSAGE] + name = service.data[ATTR_NAME] + domain = service.data.get(ATTR_DOMAIN) + entity_id = service.data.get(ATTR_ENTITY_ID) message = template.render(hass, message) log_entry(hass, name, message, domain, entity_id) hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook) - hass.services.register(DOMAIN, 'log', log_message) + hass.services.register(DOMAIN, 'log', log_message, + schema=LOG_MESSAGE_SCHEMA) return True diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index d27865f4402..b7e95ad7eed 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -37,7 +37,7 @@ class TestComponentHistory(unittest.TestCase): logbook.ATTR_NAME: 'Alarm', logbook.ATTR_MESSAGE: 'is triggered', logbook.ATTR_DOMAIN: 'switch', - logbook.ATTR_ENTITY_ID: 'test_switch' + logbook.ATTR_ENTITY_ID: 'switch.test_switch' }, True) self.hass.pool.block_till_done() @@ -48,7 +48,7 @@ class TestComponentHistory(unittest.TestCase): self.assertEqual('is triggered', last_call.data.get( logbook.ATTR_MESSAGE)) self.assertEqual('switch', last_call.data.get(logbook.ATTR_DOMAIN)) - self.assertEqual('test_switch', last_call.data.get( + self.assertEqual('switch.test_switch', last_call.data.get( logbook.ATTR_ENTITY_ID)) def test_service_call_create_log_book_entry_no_message(self): From 5c520b0d35dd8e894fdedaad3e19ae26c7e31348 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 22:01:53 -0400 Subject: [PATCH 09/19] Service validation for lock component. --- homeassistant/components/lock/__init__.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 821e2e3da33..299c5fbb778 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -8,10 +8,13 @@ from datetime import timedelta import logging import os +import voluptuous as vol + from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) @@ -33,6 +36,11 @@ DISCOVERY_PLATFORMS = { verisure.DISCOVER_LOCKS: 'verisure' } +LOCK_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_CODE): cv.string, +}) + _LOGGER = logging.getLogger(__name__) @@ -75,10 +83,7 @@ def setup(hass, config): """Handle calls to the lock services.""" target_locks = component.extract_from_service(service) - if ATTR_CODE not in service.data: - code = None - else: - code = service.data[ATTR_CODE] + code = service.data.get(ATTR_CODE) for item in target_locks: if service.service == SERVICE_LOCK: @@ -92,10 +97,11 @@ def setup(hass, config): descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) hass.services.register(DOMAIN, SERVICE_UNLOCK, handle_lock_service, - descriptions.get(SERVICE_UNLOCK)) + descriptions.get(SERVICE_UNLOCK), + schema=LOCK_SERVICE_SCHEMA) hass.services.register(DOMAIN, SERVICE_LOCK, handle_lock_service, - descriptions.get(SERVICE_LOCK)) - + descriptions.get(SERVICE_LOCK), + schema=LOCK_SERVICE_SCHEMA) return True From 8b40346db33ae76b48d79e4552d5b3036230e03c Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 22:13:40 -0400 Subject: [PATCH 10/19] Service validation for keyboard component. --- homeassistant/components/keyboard.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index eb801fae252..1a33b7dc082 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -4,6 +4,8 @@ Provides functionality to emulate keyboard presses on host machine. For more details about this component, please refer to the documentation at https://home-assistant.io/components/keyboard/ """ +import voluptuous as vol + from homeassistant.const import ( SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, @@ -12,6 +14,8 @@ from homeassistant.const import ( DOMAIN = "keyboard" REQUIREMENTS = ['pyuserinput==0.1.9'] +TAP_KEY_SCHEMA = vol.Schema({}) + def volume_up(hass): """Press the keyboard button for volume up.""" @@ -52,26 +56,31 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_VOLUME_UP, lambda service: - keyboard.tap_key(keyboard.volume_up_key)) + keyboard.tap_key(keyboard.volume_up_key), + schema=TAP_KEY_SCHEMA) hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN, lambda service: - keyboard.tap_key(keyboard.volume_down_key)) + keyboard.tap_key(keyboard.volume_down_key), + schema=TAP_KEY_SCHEMA) hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, lambda service: - keyboard.tap_key(keyboard.volume_mute_key)) + keyboard.tap_key(keyboard.volume_mute_key), + schema=TAP_KEY_SCHEMA) hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, lambda service: - keyboard.tap_key(keyboard.media_play_pause_key)) + keyboard.tap_key(keyboard.media_play_pause_key), + schema=TAP_KEY_SCHEMA) hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, lambda service: - keyboard.tap_key(keyboard.media_next_track_key)) + keyboard.tap_key(keyboard.media_next_track_key), + schema=TAP_KEY_SCHEMA) hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, lambda service: - keyboard.tap_key(keyboard.media_prev_track_key)) - + keyboard.tap_key(keyboard.media_prev_track_key), + schema=TAP_KEY_SCHEMA) return True From 620d7a92f0f9e6d1a42c8dd2b16c74f704782366 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 22:52:14 -0400 Subject: [PATCH 11/19] Service validation for input_slider component. --- homeassistant/components/input_slider.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index bef270f1387..a142fb364c7 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -6,7 +6,10 @@ at https://home-assistant.io/components/input_slider/ """ import logging +import voluptuous as vol + from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util import slugify @@ -29,6 +32,11 @@ ATTR_STEP = 'step' SERVICE_SELECT_VALUE = 'select_value' +SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_VALUE): vol.Coerce(int), +}) + def select_value(hass, entity_id, value): """Set input_slider to value.""" @@ -81,10 +89,11 @@ def setup(hass, config): target_inputs = component.extract_from_service(call) for input_slider in target_inputs: - input_slider.select_value(call.data.get(ATTR_VALUE)) + input_slider.select_value(call.data[ATTR_VALUE]) hass.services.register(DOMAIN, SERVICE_SELECT_VALUE, - select_value_service) + select_value_service, + schema=SERVICE_SELECT_VALUE_SCHEMA) component.add_entities(entities) From bfdbbbddac02219019091ffa6546a21632a5016e Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 22:57:18 -0400 Subject: [PATCH 12/19] Service validation for input_select component. --- homeassistant/components/input_select.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index e7c6e280386..15b4c398287 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -6,7 +6,10 @@ at https://home-assistant.io/components/input_select/ """ import logging +import voluptuous as vol + from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util import slugify @@ -25,6 +28,11 @@ ATTR_OPTIONS = 'options' SERVICE_SELECT_OPTION = 'select_option' +SERVICE_SELECT_OPTION_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_OPTION): cv.string, +}) + def select_option(hass, entity_id, option): """Set input_select to False.""" @@ -79,10 +87,11 @@ def setup(hass, config): target_inputs = component.extract_from_service(call) for input_select in target_inputs: - input_select.select_option(call.data.get(ATTR_OPTION)) + input_select.select_option(call.data[ATTR_OPTION]) hass.services.register(DOMAIN, SERVICE_SELECT_OPTION, - select_option_service) + select_option_service, + schema=SERVICE_SELECT_OPTION_SCHEMA) component.add_entities(entities) From 7e0d9bc7095d9ba47def6f54f2970b35e7bf30ec Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 23:04:06 -0400 Subject: [PATCH 13/19] Service validation for input_boolean component. --- homeassistant/components/input_boolean.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 75d5d940f47..8702fd6eae2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -6,8 +6,11 @@ at https://home-assistant.io/components/input_boolean/ """ import logging +import voluptuous as vol + from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util import slugify @@ -22,6 +25,10 @@ CONF_NAME = "name" CONF_INITIAL = "initial" CONF_ICON = "icon" +TOGGLE_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + def is_on(hass, entity_id): """Test if input_boolean is True.""" @@ -75,8 +82,10 @@ def setup(hass, config): else: input_b.turn_off() - hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service) - hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service) + hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service, + schema=TOGGLE_SERVICE_SCHEMA) + hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service, + schema=TOGGLE_SERVICE_SCHEMA) component.add_entities(entities) From 49b002dc53c5e48b8ee8bf56bff99876838daa17 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 23:08:20 -0400 Subject: [PATCH 14/19] Service validation for ifttt component. --- homeassistant/components/ifttt.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py index 6694bb5d2ee..a30ef184d7e 100644 --- a/homeassistant/components/ifttt.py +++ b/homeassistant/components/ifttt.py @@ -7,8 +7,10 @@ https://home-assistant.io/components/ifttt/ import logging import requests +import voluptuous as vol from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -23,6 +25,13 @@ ATTR_VALUE3 = 'value3' REQUIREMENTS = ['pyfttt==0.3'] +SERVICE_TRIGGER_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT): cv.string, + vol.Optional(ATTR_VALUE1): cv.string, + vol.Optional(ATTR_VALUE2): cv.string, + vol.Optional(ATTR_VALUE3): cv.string, +}) + def trigger(hass, event, value1=None, value2=None, value3=None): """Trigger a Maker IFTTT recipe.""" @@ -44,12 +53,10 @@ def setup(hass, config): def trigger_service(call): """Handle IFTTT trigger service calls.""" - event = call.data.get(ATTR_EVENT) + event = call.data[ATTR_EVENT] value1 = call.data.get(ATTR_VALUE1) value2 = call.data.get(ATTR_VALUE2) value3 = call.data.get(ATTR_VALUE3) - if event is None: - return try: import pyfttt as pyfttt @@ -57,6 +64,7 @@ def setup(hass, config): except requests.exceptions.RequestException: _LOGGER.exception("Error communicating with IFTTT") - hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service) + hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service, + schema=SERVICE_TRIGGER_SCHEMA) return True From 652fe7e0f2991ff5aa8e17bb4a1d2f61ce5a29ca Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 23:13:24 -0400 Subject: [PATCH 15/19] Service validation for garage_door component. --- homeassistant/components/garage_door/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garage_door/__init__.py b/homeassistant/components/garage_door/__init__.py index 736949e474e..30d9a5e2e0b 100644 --- a/homeassistant/components/garage_door/__init__.py +++ b/homeassistant/components/garage_door/__init__.py @@ -7,10 +7,13 @@ at https://home-assistant.io/components/garage_door/ import logging import os +import voluptuous as vol + from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, ATTR_ENTITY_ID) @@ -29,6 +32,10 @@ DISCOVERY_PLATFORMS = { wink.DISCOVER_GARAGE_DOORS: 'wink' } +GARAGE_DOOR_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + _LOGGER = logging.getLogger(__name__) @@ -73,10 +80,11 @@ def setup(hass, config): descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service, - descriptions.get(SERVICE_OPEN)) + descriptions.get(SERVICE_OPEN), + schema=GARAGE_DOOR_SERVICE_SCHEMA) hass.services.register(DOMAIN, SERVICE_CLOSE, handle_garage_door_service, - descriptions.get(SERVICE_CLOSE)) - + descriptions.get(SERVICE_CLOSE), + schema=GARAGE_DOOR_SERVICE_SCHEMA) return True From 003bd24976e9ded0109a2d0914ec82c56d10c56f Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Tue, 12 Apr 2016 23:21:27 -0400 Subject: [PATCH 16/19] Service validation for downloader component. --- homeassistant/components/downloader.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index c425d9cbb23..e05c617bcf0 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -10,8 +10,10 @@ import re import threading import requests +import voluptuous as vol from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv from homeassistant.util import sanitize_filename DOMAIN = "downloader" @@ -21,6 +23,11 @@ SERVICE_DOWNLOAD_FILE = "download_file" ATTR_URL = "url" ATTR_SUBDIR = "subdir" +SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ + vol.Required(ATTR_URL): vol.Url, + vol.Optional(ATTR_SUBDIR): cv.string, +}) + CONF_DOWNLOAD_DIR = 'download_dir' @@ -48,10 +55,6 @@ def setup(hass, config): def download_file(service): """Start thread to download file specified in the URL.""" - if ATTR_URL not in service.data: - logger.error("Service called but 'url' parameter not specified.") - return - def do_download(): """Download the file.""" try: @@ -127,7 +130,7 @@ def setup(hass, config): threading.Thread(target=do_download).start() - hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, - download_file) + hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file, + schema=SERVICE_DOWNLOAD_FILE_SCHEMA) return True From 7c9729b9c1d088ee504ee83cda7ee68ed6794ef1 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 13 Apr 2016 12:48:39 -0400 Subject: [PATCH 17/19] Service validation for conversation component. --- homeassistant/components/conversation.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 11e018846c2..dc8446412c4 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -8,9 +8,12 @@ import logging import re import warnings +import voluptuous as vol + from homeassistant import core from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) +import homeassistant.helpers.config_validation as cv DOMAIN = "conversation" @@ -18,6 +21,10 @@ SERVICE_PROCESS = "process" ATTR_TEXT = "text" +SERVICE_PROCESS_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower), +}) + REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') REQUIREMENTS = ['fuzzywuzzy==0.8.0'] @@ -32,11 +39,7 @@ def setup(hass, config): def process(service): """Parse text into commands.""" - if ATTR_TEXT not in service.data: - logger.error("Received process service call without a text") - return - - text = service.data[ATTR_TEXT].lower() + text = service.data[ATTR_TEXT] match = REGEX_TURN_COMMAND.match(text) if not match: @@ -67,6 +70,6 @@ def setup(hass, config): logger.error( 'Got unsupported command %s from text %s', command, text) - hass.services.register(DOMAIN, SERVICE_PROCESS, process) - + hass.services.register(DOMAIN, SERVICE_PROCESS, process, + schema=SERVICE_PROCESS_SCHEMA) return True From 5cdaee7ebbffd0b41c68effb8ff03aed77ca6854 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 13 Apr 2016 12:57:47 -0400 Subject: [PATCH 18/19] Service validation for browser component. --- homeassistant/components/browser.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index edfe1008c6e..fc78b83bd60 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -4,10 +4,18 @@ Provides functionality to launch a web browser on the host machine. For more details about this component, please refer to the documentation at https://home-assistant.io/components/browser/ """ +import voluptuous as vol DOMAIN = "browser" SERVICE_BROWSE_URL = "browse_url" +ATTR_URL = 'url' +ATTR_URL_DEFAULT = 'https://www.google.com' + +SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ + vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url, +}) + def setup(hass, config): """Listen for browse_url events.""" @@ -15,8 +23,7 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_BROWSE_URL, lambda service: - webbrowser.open( - service.data.get( - 'url', 'https://www.google.com'))) + webbrowser.open(service.data[ATTR_URL]), + schema=SERVICE_BROWSE_URL_SCHEMA) return True From 298a1c2af7fefd4e0b624da46a55de66437118e4 Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Wed, 13 Apr 2016 13:45:11 -0400 Subject: [PATCH 19/19] Service validation for alarm_control_panel component. --- .../components/alarm_control_panel/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index e3bde441211..cf042abbe10 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -7,12 +7,15 @@ https://home-assistant.io/components/alarm_control_panel/ import logging import os +import voluptuous as vol + from homeassistant.components import verisure from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) from homeassistant.config import load_yaml_config_file from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -38,6 +41,11 @@ ATTR_TO_PROPERTY = [ ATTR_CODE_FORMAT ] +ALARM_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_CODE): cv.string, +}) + def setup(hass, config): """Track states and offer events for sensors.""" @@ -51,10 +59,7 @@ def setup(hass, config): """Map services to methods on Alarm.""" target_alarms = component.extract_from_service(service) - if ATTR_CODE not in service.data: - code = None - else: - code = service.data[ATTR_CODE] + code = service.data.get(ATTR_CODE) method = SERVICE_TO_METHOD[service.service] @@ -68,8 +73,8 @@ def setup(hass, config): for service in SERVICE_TO_METHOD: hass.services.register(DOMAIN, service, alarm_service_handler, - descriptions.get(service)) - + descriptions.get(service), + schema=ALARM_SERVICE_SCHEMA) return True