Files
core/homeassistant/components/python_script.py
T

212 lines
6.9 KiB
Python
Raw Normal View History

2017-10-30 08:02:15 +01:00
"""
Component to allow running Python scripts.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/python_script/
"""
2017-09-14 16:52:47 +02:00
import datetime
import glob
import logging
import os
import time
2017-06-09 03:38:40 -07:00
import voluptuous as vol
2017-09-21 17:00:45 +02:00
from homeassistant.const import SERVICE_RELOAD
2017-06-15 22:13:10 -07:00
from homeassistant.exceptions import HomeAssistantError
2017-07-16 10:14:46 -07:00
from homeassistant.loader import bind_hass
2017-06-15 22:13:10 -07:00
from homeassistant.util import sanitize_filename
2018-01-21 07:35:38 +01:00
import homeassistant.util.dt as dt_util
2017-10-30 08:02:15 +01:00
2018-05-19 10:05:02 +02:00
REQUIREMENTS = ['restrictedpython==4.0b4']
2017-10-30 08:02:15 +01:00
_LOGGER = logging.getLogger(__name__)
2017-06-15 22:13:10 -07:00
2017-06-09 03:38:40 -07:00
DOMAIN = 'python_script'
2017-10-30 08:02:15 +01:00
2017-06-09 03:38:40 -07:00
FOLDER = 'python_scripts'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(dict)
}, extra=vol.ALLOW_EXTRA)
2017-06-15 22:13:10 -07:00
ALLOWED_HASS = set(['bus', 'services', 'states'])
ALLOWED_EVENTBUS = set(['fire'])
ALLOWED_STATEMACHINE = set(['entity_ids', 'all', 'get', 'is_state',
'is_state_attr', 'remove', 'set'])
ALLOWED_SERVICEREGISTRY = set(['services', 'has_service', 'call'])
ALLOWED_TIME = set(['sleep', 'strftime', 'strptime', 'gmtime', 'localtime',
'ctime', 'time', 'mktime'])
ALLOWED_DATETIME = set(['date', 'time', 'datetime', 'timedelta', 'tzinfo'])
ALLOWED_DT_UTIL = set([
'utcnow', 'now', 'as_utc', 'as_timestamp', 'as_local',
'utc_from_timestamp', 'start_of_local_day', 'parse_datetime', 'parse_date',
'get_age'])
2017-06-15 22:13:10 -07:00
class ScriptError(HomeAssistantError):
"""When a script error occurs."""
pass
2017-06-09 03:38:40 -07:00
def setup(hass, config):
2017-10-30 08:02:15 +01:00
"""Initialize the Python script component."""
2017-06-09 03:38:40 -07:00
path = hass.config.path(FOLDER)
2017-09-21 17:00:45 +02:00
if not os.path.isdir(path):
2017-10-30 08:02:15 +01:00
_LOGGER.warning("Folder %s not found in configuration folder", FOLDER)
2017-09-21 17:00:45 +02:00
return False
discover_scripts(hass)
def reload_scripts_handler(call):
"""Handle reload service calls."""
discover_scripts(hass)
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_scripts_handler)
return True
def discover_scripts(hass):
"""Discover python scripts in folder."""
path = hass.config.path(FOLDER)
2017-06-09 03:38:40 -07:00
if not os.path.isdir(path):
2017-10-30 08:02:15 +01:00
_LOGGER.warning("Folder %s not found in configuration folder", FOLDER)
2017-06-09 03:38:40 -07:00
return False
2017-06-15 22:13:10 -07:00
def python_script_service_handler(call):
2017-06-09 03:38:40 -07:00
"""Handle python script service calls."""
2017-06-15 22:13:10 -07:00
execute_script(hass, call.service, call.data)
2017-06-09 03:38:40 -07:00
2017-09-21 17:00:45 +02:00
existing = hass.services.services.get(DOMAIN, {}).keys()
for existing_service in existing:
if existing_service == SERVICE_RELOAD:
continue
hass.services.remove(DOMAIN, existing_service)
2017-06-09 03:38:40 -07:00
for fil in glob.iglob(os.path.join(path, '*.py')):
name = os.path.splitext(os.path.basename(fil))[0]
2017-06-15 22:13:10 -07:00
hass.services.register(DOMAIN, name, python_script_service_handler)
2017-06-09 03:38:40 -07:00
2017-07-16 10:14:46 -07:00
@bind_hass
2017-06-15 22:13:10 -07:00
def execute_script(hass, name, data=None):
2017-06-09 03:38:40 -07:00
"""Execute a script."""
2017-06-15 22:13:10 -07:00
filename = '{}.py'.format(name)
with open(hass.config.path(FOLDER, sanitize_filename(filename))) as fil:
source = fil.read()
execute(hass, filename, source, data)
2017-07-16 10:14:46 -07:00
@bind_hass
2017-06-15 22:13:10 -07:00
def execute(hass, filename, source, data=None):
"""Execute Python source."""
2017-06-09 03:38:40 -07:00
from RestrictedPython import compile_restricted_exec
2017-09-14 16:52:47 +02:00
from RestrictedPython.Guards import safe_builtins, full_write_guard, \
guarded_iter_unpack_sequence, guarded_unpack_sequence
from RestrictedPython.Utilities import utility_builtins
from RestrictedPython.Eval import default_guarded_getitem
2017-06-09 03:38:40 -07:00
compiled = compile_restricted_exec(source, filename=filename)
if compiled.errors:
2017-10-30 08:02:15 +01:00
_LOGGER.error("Error loading script %s: %s", filename,
", ".join(compiled.errors))
2017-06-09 03:38:40 -07:00
return
if compiled.warnings:
2017-10-30 08:02:15 +01:00
_LOGGER.warning("Warning loading script %s: %s", filename,
", ".join(compiled.warnings))
2017-06-09 03:38:40 -07:00
2017-06-15 22:13:10 -07:00
def protected_getattr(obj, name, default=None):
"""Restricted method to get attributes."""
# pylint: disable=too-many-boolean-expressions
if name.startswith('async_'):
2017-10-30 08:02:15 +01:00
raise ScriptError("Not allowed to access async methods")
2017-06-15 22:13:10 -07:00
elif (obj is hass and name not in ALLOWED_HASS or
obj is hass.bus and name not in ALLOWED_EVENTBUS or
obj is hass.states and name not in ALLOWED_STATEMACHINE or
obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or
obj is dt_util and name not in ALLOWED_DT_UTIL or
obj is datetime and name not in ALLOWED_DATETIME or
isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME):
2017-10-30 08:02:15 +01:00
raise ScriptError("Not allowed to access {}.{}".format(
2017-06-15 22:13:10 -07:00
obj.__class__.__name__, name))
return getattr(obj, name, default)
builtins = safe_builtins.copy()
builtins.update(utility_builtins)
2017-09-14 16:52:47 +02:00
builtins['datetime'] = datetime
2017-11-16 23:06:02 -07:00
builtins['sorted'] = sorted
builtins['time'] = TimeWrapper()
builtins['dt_util'] = dt_util
2017-06-09 03:38:40 -07:00
restricted_globals = {
'__builtins__': builtins,
2017-06-09 03:38:40 -07:00
'_print_': StubPrinter,
2017-06-15 22:13:10 -07:00
'_getattr_': protected_getattr,
2017-06-09 03:38:40 -07:00
'_write_': full_write_guard,
2017-06-21 04:32:50 -07:00
'_getiter_': iter,
2017-09-14 16:52:47 +02:00
'_getitem_': default_guarded_getitem,
'_iter_unpack_sequence_': guarded_iter_unpack_sequence,
'_unpack_sequence_': guarded_unpack_sequence,
2017-06-09 03:38:40 -07:00
}
2017-06-15 22:13:10 -07:00
logger = logging.getLogger('{}.{}'.format(__name__, filename))
2017-06-09 03:38:40 -07:00
local = {
'hass': hass,
2017-06-15 22:13:10 -07:00
'data': data or {},
'logger': logger
2017-06-09 03:38:40 -07:00
}
try:
2017-10-30 08:02:15 +01:00
_LOGGER.info("Executing %s: %s", filename, data)
2017-06-09 03:38:40 -07:00
# pylint: disable=exec-used
exec(compiled.code, restricted_globals, local)
2017-06-15 22:13:10 -07:00
except ScriptError as err:
2017-10-30 08:02:15 +01:00
logger.error("Error executing script: %s", err)
2017-06-09 03:38:40 -07:00
except Exception as err: # pylint: disable=broad-except
2017-10-30 08:02:15 +01:00
logger.exception("Error executing script: %s", err)
2017-06-09 03:38:40 -07:00
class StubPrinter:
"""Class to handle printing inside scripts."""
def __init__(self, _getattr_):
"""Initialize our printer."""
pass
def _call_print(self, *objects, **kwargs):
"""Print text."""
# pylint: disable=no-self-use
_LOGGER.warning(
2017-10-30 08:02:15 +01:00
"Don't use print() inside scripts. Use logger.info() instead")
class TimeWrapper:
2018-01-21 07:35:38 +01:00
"""Wrap the time module."""
# Class variable, only going to warn once per Home Assistant run
warned = False
# pylint: disable=no-self-use
def sleep(self, *args, **kwargs):
"""Sleep method that warns once."""
if not TimeWrapper.warned:
TimeWrapper.warned = True
2017-10-30 08:02:15 +01:00
_LOGGER.warning("Using time.sleep can reduce the performance of "
"Home Assistant")
time.sleep(*args, **kwargs)
def __getattr__(self, attr):
"""Fetch an attribute from Time module."""
attribute = getattr(time, attr)
if callable(attribute):
def wrapper(*args, **kw):
2018-01-21 07:35:38 +01:00
"""Wrap to return callable method if callable."""
return attribute(*args, **kw)
return wrapper
2018-02-11 18:20:28 +01:00
return attribute