Merge pull request #1821 from home-assistant/service-validations

Service validations
This commit is contained in:
Paulus Schoutsen 2016-04-13 22:38:14 -07:00
commit f5ee3e6e13
21 changed files with 257 additions and 133 deletions

View File

@ -7,12 +7,15 @@ https://home-assistant.io/components/alarm_control_panel/
import logging import logging
import os import os
import voluptuous as vol
from homeassistant.components import verisure from homeassistant.components import verisure
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa 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 import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -38,6 +41,11 @@ ATTR_TO_PROPERTY = [
ATTR_CODE_FORMAT 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): def setup(hass, config):
"""Track states and offer events for sensors.""" """Track states and offer events for sensors."""
@ -51,10 +59,7 @@ def setup(hass, config):
"""Map services to methods on Alarm.""" """Map services to methods on Alarm."""
target_alarms = component.extract_from_service(service) target_alarms = component.extract_from_service(service)
if ATTR_CODE not in service.data: code = service.data.get(ATTR_CODE)
code = None
else:
code = service.data[ATTR_CODE]
method = SERVICE_TO_METHOD[service.service] method = SERVICE_TO_METHOD[service.service]
@ -68,8 +73,8 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, alarm_service_handler, hass.services.register(DOMAIN, service, alarm_service_handler,
descriptions.get(service)) descriptions.get(service),
schema=ALARM_SERVICE_SCHEMA)
return True return True

View File

@ -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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/browser/ https://home-assistant.io/components/browser/
""" """
import voluptuous as vol
DOMAIN = "browser" DOMAIN = "browser"
SERVICE_BROWSE_URL = "browse_url" 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): def setup(hass, config):
"""Listen for browse_url events.""" """Listen for browse_url events."""
@ -15,8 +23,7 @@ def setup(hass, config):
hass.services.register(DOMAIN, SERVICE_BROWSE_URL, hass.services.register(DOMAIN, SERVICE_BROWSE_URL,
lambda service: lambda service:
webbrowser.open( webbrowser.open(service.data[ATTR_URL]),
service.data.get( schema=SERVICE_BROWSE_URL_SCHEMA)
'url', 'https://www.google.com')))
return True return True

View File

@ -8,9 +8,12 @@ import logging
import re import re
import warnings import warnings
import voluptuous as vol
from homeassistant import core from homeassistant import core
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
import homeassistant.helpers.config_validation as cv
DOMAIN = "conversation" DOMAIN = "conversation"
@ -18,6 +21,10 @@ SERVICE_PROCESS = "process"
ATTR_TEXT = "text" 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<name>(?: |\w)+) (?P<command>\w+)') REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.8.0'] REQUIREMENTS = ['fuzzywuzzy==0.8.0']
@ -32,11 +39,7 @@ def setup(hass, config):
def process(service): def process(service):
"""Parse text into commands.""" """Parse text into commands."""
if ATTR_TEXT not in service.data: text = service.data[ATTR_TEXT]
logger.error("Received process service call without a text")
return
text = service.data[ATTR_TEXT].lower()
match = REGEX_TURN_COMMAND.match(text) match = REGEX_TURN_COMMAND.match(text)
if not match: if not match:
@ -67,6 +70,6 @@ def setup(hass, config):
logger.error( logger.error(
'Got unsupported command %s from text %s', command, text) '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 return True

View File

@ -10,8 +10,10 @@ import re
import threading import threading
import requests import requests
import voluptuous as vol
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
import homeassistant.helpers.config_validation as cv
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
DOMAIN = "downloader" DOMAIN = "downloader"
@ -21,6 +23,11 @@ SERVICE_DOWNLOAD_FILE = "download_file"
ATTR_URL = "url" ATTR_URL = "url"
ATTR_SUBDIR = "subdir" 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' CONF_DOWNLOAD_DIR = 'download_dir'
@ -48,10 +55,6 @@ def setup(hass, config):
def download_file(service): def download_file(service):
"""Start thread to download file specified in the URL.""" """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(): def do_download():
"""Download the file.""" """Download the file."""
try: try:
@ -127,7 +130,7 @@ def setup(hass, config):
threading.Thread(target=do_download).start() threading.Thread(target=do_download).start()
hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file,
download_file) schema=SERVICE_DOWNLOAD_FILE_SCHEMA)
return True return True

View File

@ -7,10 +7,13 @@ at https://home-assistant.io/components/garage_door/
import logging import logging
import os import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
@ -29,6 +32,10 @@ DISCOVERY_PLATFORMS = {
wink.DISCOVER_GARAGE_DOORS: 'wink' wink.DISCOVER_GARAGE_DOORS: 'wink'
} }
GARAGE_DOOR_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -73,10 +80,11 @@ def setup(hass, config):
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service, 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, 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 return True

View File

@ -7,8 +7,10 @@ https://home-assistant.io/components/ifttt/
import logging import logging
import requests import requests
import voluptuous as vol
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,6 +25,13 @@ ATTR_VALUE3 = 'value3'
REQUIREMENTS = ['pyfttt==0.3'] 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): def trigger(hass, event, value1=None, value2=None, value3=None):
"""Trigger a Maker IFTTT recipe.""" """Trigger a Maker IFTTT recipe."""
@ -44,12 +53,10 @@ def setup(hass, config):
def trigger_service(call): def trigger_service(call):
"""Handle IFTTT trigger service calls.""" """Handle IFTTT trigger service calls."""
event = call.data.get(ATTR_EVENT) event = call.data[ATTR_EVENT]
value1 = call.data.get(ATTR_VALUE1) value1 = call.data.get(ATTR_VALUE1)
value2 = call.data.get(ATTR_VALUE2) value2 = call.data.get(ATTR_VALUE2)
value3 = call.data.get(ATTR_VALUE3) value3 = call.data.get(ATTR_VALUE3)
if event is None:
return
try: try:
import pyfttt as pyfttt import pyfttt as pyfttt
@ -57,6 +64,7 @@ def setup(hass, config):
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
_LOGGER.exception("Error communicating with IFTTT") _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 return True

View File

@ -6,8 +6,11 @@ at https://home-assistant.io/components/input_boolean/
""" """
import logging import logging
import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) 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 import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util import slugify from homeassistant.util import slugify
@ -22,6 +25,10 @@ CONF_NAME = "name"
CONF_INITIAL = "initial" CONF_INITIAL = "initial"
CONF_ICON = "icon" CONF_ICON = "icon"
TOGGLE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def is_on(hass, entity_id): def is_on(hass, entity_id):
"""Test if input_boolean is True.""" """Test if input_boolean is True."""
@ -75,8 +82,10 @@ def setup(hass, config):
else: else:
input_b.turn_off() input_b.turn_off()
hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service) hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service,
hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service) schema=TOGGLE_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service,
schema=TOGGLE_SERVICE_SCHEMA)
component.add_entities(entities) component.add_entities(entities)

View File

@ -6,7 +6,10 @@ at https://home-assistant.io/components/input_select/
""" """
import logging import logging
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util import slugify from homeassistant.util import slugify
@ -25,6 +28,11 @@ ATTR_OPTIONS = 'options'
SERVICE_SELECT_OPTION = 'select_option' 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): def select_option(hass, entity_id, option):
"""Set input_select to False.""" """Set input_select to False."""
@ -79,10 +87,11 @@ def setup(hass, config):
target_inputs = component.extract_from_service(call) target_inputs = component.extract_from_service(call)
for input_select in target_inputs: 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, hass.services.register(DOMAIN, SERVICE_SELECT_OPTION,
select_option_service) select_option_service,
schema=SERVICE_SELECT_OPTION_SCHEMA)
component.add_entities(entities) component.add_entities(entities)

View File

@ -6,7 +6,10 @@ at https://home-assistant.io/components/input_slider/
""" """
import logging import logging
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util import slugify from homeassistant.util import slugify
@ -29,6 +32,11 @@ ATTR_STEP = 'step'
SERVICE_SELECT_VALUE = 'select_value' 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): def select_value(hass, entity_id, value):
"""Set input_slider to value.""" """Set input_slider to value."""
@ -81,10 +89,11 @@ def setup(hass, config):
target_inputs = component.extract_from_service(call) target_inputs = component.extract_from_service(call)
for input_slider in target_inputs: 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, hass.services.register(DOMAIN, SERVICE_SELECT_VALUE,
select_value_service) select_value_service,
schema=SERVICE_SELECT_VALUE_SCHEMA)
component.add_entities(entities) component.add_entities(entities)

View File

@ -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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/keyboard/ https://home-assistant.io/components/keyboard/
""" """
import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
@ -12,6 +14,8 @@ from homeassistant.const import (
DOMAIN = "keyboard" DOMAIN = "keyboard"
REQUIREMENTS = ['pyuserinput==0.1.9'] REQUIREMENTS = ['pyuserinput==0.1.9']
TAP_KEY_SCHEMA = vol.Schema({})
def volume_up(hass): def volume_up(hass):
"""Press the keyboard button for volume up.""" """Press the keyboard button for volume up."""
@ -52,26 +56,31 @@ def setup(hass, config):
hass.services.register(DOMAIN, SERVICE_VOLUME_UP, hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
lambda service: 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, hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
lambda service: 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, hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service: 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, hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
lambda service: 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, hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
lambda service: 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, hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_prev_track_key)) keyboard.tap_key(keyboard.media_prev_track_key),
schema=TAP_KEY_SCHEMA)
return True return True

View File

@ -8,10 +8,13 @@ from datetime import timedelta
import logging import logging
import os import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
@ -33,6 +36,11 @@ DISCOVERY_PLATFORMS = {
verisure.DISCOVER_LOCKS: 'verisure' 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__) _LOGGER = logging.getLogger(__name__)
@ -75,10 +83,7 @@ def setup(hass, config):
"""Handle calls to the lock services.""" """Handle calls to the lock services."""
target_locks = component.extract_from_service(service) target_locks = component.extract_from_service(service)
if ATTR_CODE not in service.data: code = service.data.get(ATTR_CODE)
code = None
else:
code = service.data[ATTR_CODE]
for item in target_locks: for item in target_locks:
if service.service == SERVICE_LOCK: if service.service == SERVICE_LOCK:
@ -92,10 +97,11 @@ def setup(hass, config):
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_UNLOCK, handle_lock_service, 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, hass.services.register(DOMAIN, SERVICE_LOCK, handle_lock_service,
descriptions.get(SERVICE_LOCK)) descriptions.get(SERVICE_LOCK),
schema=LOCK_SERVICE_SCHEMA)
return True return True

View File

@ -9,6 +9,8 @@ import re
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
import voluptuous as vol
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun from homeassistant.components import recorder, sun
from homeassistant.const import ( from homeassistant.const import (
@ -18,6 +20,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.core import State from homeassistant.core import State
from homeassistant.helpers.entity import split_entity_id from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'http']
@ -39,6 +42,13 @@ ATTR_MESSAGE = 'message'
ATTR_DOMAIN = 'domain' ATTR_DOMAIN = 'domain'
ATTR_ENTITY_ID = 'entity_id' 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): def log_entry(hass, name, message, domain=None, entity_id=None):
"""Add an entry to the logbook.""" """Add an entry to the logbook."""
@ -58,19 +68,17 @@ def setup(hass, config):
"""Listen for download events to download files.""" """Listen for download events to download files."""
def log_message(service): def log_message(service):
"""Handle sending notification message service calls.""" """Handle sending notification message service calls."""
message = service.data.get(ATTR_MESSAGE) message = service.data[ATTR_MESSAGE]
name = service.data.get(ATTR_NAME) name = service.data[ATTR_NAME]
domain = service.data.get(ATTR_DOMAIN, None) domain = service.data.get(ATTR_DOMAIN)
entity_id = service.data.get(ATTR_ENTITY_ID, None) entity_id = service.data.get(ATTR_ENTITY_ID)
if not message or not name:
return
message = template.render(hass, message) message = template.render(hass, message)
log_entry(hass, name, message, domain, entity_id) log_entry(hass, name, message, domain, entity_id)
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook) 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 return True

View File

@ -8,11 +8,13 @@ from functools import partial
import logging import logging
import os import os
import voluptuous as vol
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform, template from homeassistant.helpers import config_per_platform, template
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
DOMAIN = "notify" DOMAIN = "notify"
@ -32,6 +34,13 @@ ATTR_DATA = 'data'
SERVICE_NOTIFY = "notify" 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__) _LOGGER = logging.getLogger(__name__)
@ -71,13 +80,7 @@ def setup(hass, config):
def notify_message(notify_service, call): def notify_message(notify_service, call):
"""Handle sending notification message service calls.""" """Handle sending notification message service calls."""
message = call.data.get(ATTR_MESSAGE) message = call.data[ATTR_MESSAGE]
if message is None:
_LOGGER.error(
'Received call to %s without attribute %s',
call.service, ATTR_MESSAGE)
return
title = template.render( title = template.render(
hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)) 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_call_handler = partial(notify_message, notify_service)
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY) service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)
hass.services.register(DOMAIN, service_notify, service_call_handler, hass.services.register(DOMAIN, service_notify, service_call_handler,
descriptions.get(SERVICE_NOTIFY)) descriptions.get(SERVICE_NOTIFY),
schema=NOTIFY_SERVICE_SCHEMA)
success = True success = True
return success return success

View File

@ -7,10 +7,13 @@ https://home-assistant.io/components/rollershutter/
import os import os
import logging import logging
import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group from homeassistant.components import group
from homeassistant.const import ( from homeassistant.const import (
SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP, SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP,
@ -33,6 +36,10 @@ _LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POSITION = 'current_position' 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): def is_open(hass, entity_id=None):
"""Return if the roller shutter is open based on the statemachine.""" """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, hass.services.register(DOMAIN, SERVICE_MOVE_UP,
handle_rollershutter_service, handle_rollershutter_service,
descriptions.get(SERVICE_MOVE_UP)) descriptions.get(SERVICE_MOVE_UP),
schema=ROLLERSHUTTER_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_MOVE_DOWN, hass.services.register(DOMAIN, SERVICE_MOVE_DOWN,
handle_rollershutter_service, handle_rollershutter_service,
descriptions.get(SERVICE_MOVE_DOWN)) descriptions.get(SERVICE_MOVE_DOWN),
schema=ROLLERSHUTTER_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_STOP, hass.services.register(DOMAIN, SERVICE_STOP,
handle_rollershutter_service, handle_rollershutter_service,
descriptions.get(SERVICE_STOP)) descriptions.get(SERVICE_STOP),
schema=ROLLERSHUTTER_SERVICE_SCHEMA)
return True return True

View File

@ -7,9 +7,12 @@ https://home-assistant.io/components/scene/
import logging import logging
from collections import namedtuple from collections import namedtuple
import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, CONF_PLATFORM) ATTR_ENTITY_ID, SERVICE_TURN_ON, CONF_PLATFORM)
from homeassistant.helpers import extract_domain_configs from homeassistant.helpers import extract_domain_configs
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -19,6 +22,10 @@ STATE = 'scening'
CONF_ENTITIES = "entities" CONF_ENTITIES = "entities"
SCENE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SceneConfig = namedtuple('SceneConfig', ['name', 'states']) SceneConfig = namedtuple('SceneConfig', ['name', 'states'])
@ -61,7 +68,8 @@ def setup(hass, config):
for scene in target_scenes: for scene in target_scenes:
scene.activate() 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 return True

View File

@ -109,6 +109,11 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA} vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
}, extra=vol.ALLOW_EXTRA) }, 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): def is_on(hass, entity_id):
"""Return if the switch is on based on the statemachine.""" """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) alias = cfg.get(CONF_ALIAS, object_id)
script = Script(object_id, alias, cfg[CONF_SEQUENCE]) script = Script(object_id, alias, cfg[CONF_SEQUENCE])
component.add_entities((script,)) 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): def turn_on_service(service):
"""Call a service to turn script on.""" """Call a service to turn script on."""
@ -168,10 +174,12 @@ def setup(hass, config):
for script in component.extract_from_service(service): for script in component.extract_from_service(service):
script.toggle() script.toggle()
hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service) hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service) schema=SCRIPT_TURN_ONOFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service) 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 return True

View File

@ -7,26 +7,26 @@ https://home-assistant.io/components/shell_command/
import logging import logging
import subprocess import subprocess
from homeassistant.util import slugify import voluptuous as vol
import homeassistant.helpers.config_validation as cv
DOMAIN = 'shell_command' DOMAIN = 'shell_command'
_LOGGER = logging.getLogger(__name__) _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): def setup(hass, config):
"""Setup the shell_command component.""" """Setup the shell_command component."""
conf = config.get(DOMAIN) 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
def service_handler(call): def service_handler(call):
"""Execute a shell command service.""" """Execute a shell command service."""
@ -38,6 +38,6 @@ def setup(hass, config):
_LOGGER.exception('Error running command') _LOGGER.exception('Error running command')
for name in conf.keys(): 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 return True

View File

@ -8,10 +8,13 @@ from datetime import timedelta
import logging import logging
import os import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
@ -50,6 +53,10 @@ PROP_TO_ATTR = {
'today_power_mw': ATTR_TODAY_MWH, 'today_power_mw': ATTR_TODAY_MWH,
} }
SWITCH_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -102,11 +109,14 @@ def setup(hass, config):
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service, 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, 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, hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_switch_service,
descriptions.get(SERVICE_TOGGLE)) descriptions.get(SERVICE_TOGGLE),
schema=SWITCH_SERVICE_SCHEMA)
return True return True

View File

@ -7,14 +7,16 @@ https://home-assistant.io/components/thermostat/
import logging import logging
import os import os
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import (ecobee, zwave) from homeassistant.components import (ecobee, zwave)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELCIUS) TEMP_CELCIUS)
@ -48,6 +50,19 @@ DISCOVERY_PLATFORMS = {
zwave.DISCOVER_THERMOSTATS: 'zwave' 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): def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified thermostat away mode on.""" """Turn all or specified thermostat away mode on."""
@ -97,13 +112,7 @@ def setup(hass, config):
"""Set away mode on target thermostats.""" """Set away mode on target thermostats."""
target_thermostats = component.extract_from_service(service) target_thermostats = component.extract_from_service(service)
away_mode = service.data.get(ATTR_AWAY_MODE) away_mode = service.data[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
for thermostat in target_thermostats: for thermostat in target_thermostats:
if away_mode: if away_mode:
@ -115,20 +124,14 @@ def setup(hass, config):
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, 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): def temperature_set_service(service):
"""Set temperature on the target thermostats.""" """Set temperature on the target thermostats."""
target_thermostats = component.extract_from_service(service) target_thermostats = component.extract_from_service(service)
temperature = util.convert( temperature = service.data[ATTR_TEMPERATURE]
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
for thermostat in target_thermostats: for thermostat in target_thermostats:
thermostat.set_temperature(convert( thermostat.set_temperature(convert(
@ -139,19 +142,14 @@ def setup(hass, config):
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, 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): def fan_mode_set_service(service):
"""Set fan mode on target thermostats.""" """Set fan mode on target thermostats."""
target_thermostats = component.extract_from_service(service) target_thermostats = component.extract_from_service(service)
fan_mode = service.data.get(ATTR_FAN) fan_mode = service.data[ATTR_FAN]
if fan_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN)
return
for thermostat in target_thermostats: for thermostat in target_thermostats:
if fan_mode: if fan_mode:
@ -163,7 +161,8 @@ def setup(hass, config):
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, 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 return True

View File

@ -37,7 +37,7 @@ class TestComponentHistory(unittest.TestCase):
logbook.ATTR_NAME: 'Alarm', logbook.ATTR_NAME: 'Alarm',
logbook.ATTR_MESSAGE: 'is triggered', logbook.ATTR_MESSAGE: 'is triggered',
logbook.ATTR_DOMAIN: 'switch', logbook.ATTR_DOMAIN: 'switch',
logbook.ATTR_ENTITY_ID: 'test_switch' logbook.ATTR_ENTITY_ID: 'switch.test_switch'
}, True) }, True)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -48,7 +48,7 @@ class TestComponentHistory(unittest.TestCase):
self.assertEqual('is triggered', last_call.data.get( self.assertEqual('is triggered', last_call.data.get(
logbook.ATTR_MESSAGE)) logbook.ATTR_MESSAGE))
self.assertEqual('switch', last_call.data.get(logbook.ATTR_DOMAIN)) 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)) logbook.ATTR_ENTITY_ID))
def test_service_call_create_log_book_entry_no_message(self): def test_service_call_create_log_book_entry_no_message(self):

View File

@ -5,6 +5,7 @@ import unittest
from unittest.mock import patch from unittest.mock import patch
from subprocess import SubprocessError from subprocess import SubprocessError
from homeassistant.bootstrap import _setup_component
from homeassistant.components import shell_command from homeassistant.components import shell_command
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant
@ -25,11 +26,11 @@ class TestShellCommand(unittest.TestCase):
"""Test if able to call a configured service.""" """Test if able to call a configured service."""
with tempfile.TemporaryDirectory() as tempdirname: with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, 'called.txt') path = os.path.join(tempdirname, 'called.txt')
self.assertTrue(shell_command.setup(self.hass, { assert _setup_component(self.hass, shell_command.DOMAIN, {
'shell_command': { shell_command.DOMAIN: {
'test_service': "date > {}".format(path) 'test_service': "date > {}".format(path)
} }
})) })
self.hass.services.call('shell_command', 'test_service', self.hass.services.call('shell_command', 'test_service',
blocking=True) blocking=True)
@ -38,16 +39,17 @@ class TestShellCommand(unittest.TestCase):
def test_config_not_dict(self): def test_config_not_dict(self):
"""Test if config is not a dict.""" """Test if config is not a dict."""
self.assertFalse(shell_command.setup(self.hass, { assert not _setup_component(self.hass, shell_command.DOMAIN, {
'shell_command': ['some', 'weird', 'list'] shell_command.DOMAIN: ['some', 'weird', 'list']
})) })
def test_config_not_valid_service_names(self): def test_config_not_valid_service_names(self):
"""Test if config contains invalid service names.""" """Test if config contains invalid service names."""
self.assertFalse(shell_command.setup(self.hass, { assert not _setup_component(self.hass, shell_command.DOMAIN, {
'shell_command': { shell_command.DOMAIN: {
'this is invalid because space': 'touch bla.txt' 'this is invalid because space': 'touch bla.txt'
}})) }
})
@patch('homeassistant.components.shell_command.subprocess.call', @patch('homeassistant.components.shell_command.subprocess.call',
side_effect=SubprocessError) side_effect=SubprocessError)
@ -56,11 +58,11 @@ class TestShellCommand(unittest.TestCase):
"""Test subprocess.""" """Test subprocess."""
with tempfile.TemporaryDirectory() as tempdirname: with tempfile.TemporaryDirectory() as tempdirname:
path = os.path.join(tempdirname, 'called.txt') path = os.path.join(tempdirname, 'called.txt')
self.assertTrue(shell_command.setup(self.hass, { assert _setup_component(self.hass, shell_command.DOMAIN, {
'shell_command': { shell_command.DOMAIN: {
'test_service': "touch {}".format(path) 'test_service': "touch {}".format(path)
} }
})) })
self.hass.services.call('shell_command', 'test_service', self.hass.services.call('shell_command', 'test_service',
blocking=True) blocking=True)