From b596fa33d6b03bb858cf6599c1d37cb4a182af33 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:39:59 -0500 Subject: [PATCH 01/21] Implemented restart service Implemented an OS and environment safe restart service. This works by running Home Assistant in a child process. If the child process terminates with an exit code > 0, HASS is restarted. SIGTERM and KeyboardInterrupts to the parent process are forwarded to the child process. KeyboardInterrupts will only be forwarded once. The second KeyboardInterrupt will be handled by the parent. --- homeassistant/__main__.py | 89 +++++++++++++++++++++++++++------------ homeassistant/const.py | 2 +- homeassistant/core.py | 12 +++++- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e97ed0c6386..7da1e6658e1 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,7 +1,10 @@ """ Starts home assistant. """ from __future__ import print_function +from multiprocessing import Process +import signal import sys +import threading import os import argparse @@ -204,6 +207,61 @@ def uninstall_osx(): print("Home Assistant has been uninstalled.") +def setup_and_run_hass(config_dir, args): + """ Setup HASS and run. Block until stopped. """ + if args.demo_mode: + config = { + 'frontend': {}, + 'demo': {} + } + hass = bootstrap.from_config_dict( + config, config_dir=config_dir, daemon=args.daemon, + verbose=args.verbose, skip_pip=args.skip_pip, + log_rotate_days=args.log_rotate_days) + else: + config_file = ensure_config_file(config_dir) + print('Config directory:', config_dir) + hass = bootstrap.from_config_file( + config_file, daemon=args.daemon, verbose=args.verbose, + skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) + + if args.open_ui: + def open_browser(event): + """ Open the webinterface in a browser. """ + if hass.config.api is not None: + import webbrowser + webbrowser.open(hass.config.api.base_url) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) + + hass.start() + sys.exit(int(hass.block_till_stopped())) + + +def run_hass_process(hass_proc): + """ Runs a child hass process. Returns True if it should be restarted. """ + requested_stop = threading.Event() + hass_proc.daemon = True + + def request_stop(): + """ request hass stop """ + requested_stop.set() + hass_proc.terminate() + + try: + signal.signal(signal.SIGTERM, request_stop) + except ValueError: + print('Could not bind to SIGQUIT. Are you running in a thread?') + + hass_proc.start() + try: + hass_proc.join() + except KeyboardInterrupt: + request_stop() + hass_proc.join() + return not requested_stop.isSet() and hass_proc.exitcode > 0 + + def main(): """ Starts Home Assistant. """ validate_python() @@ -233,33 +291,12 @@ def main(): if args.pid_file: write_pid(args.pid_file) - if args.demo_mode: - config = { - 'frontend': {}, - 'demo': {} - } - hass = bootstrap.from_config_dict( - config, config_dir=config_dir, daemon=args.daemon, - verbose=args.verbose, skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days) - else: - config_file = ensure_config_file(config_dir) - print('Config directory:', config_dir) - hass = bootstrap.from_config_file( - config_file, daemon=args.daemon, verbose=args.verbose, - skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) + # Run hass is child process. Restart if necessary. + keep_running = True + while keep_running: + hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args)) + keep_running = run_hass_process(hass_proc) - if args.open_ui: - def open_browser(event): - """ Open the webinterface in a browser. """ - if hass.config.api is not None: - import webbrowser - webbrowser.open(hass.config.api.base_url) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) - - hass.start() - hass.block_till_stopped() if __name__ == "__main__": main() diff --git a/homeassistant/const.py b/homeassistant/const.py index 143704e1968..2d605a7ee71 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -10,7 +10,6 @@ MATCH_ALL = '*' DEVICE_DEFAULT_NAME = "Unnamed Device" # #### CONFIG #### -CONF_ICON = "icon" CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" CONF_TEMPERATURE_UNIT = "temperature_unit" @@ -124,6 +123,7 @@ ATTR_GPS_ACCURACY = 'gps_accuracy' # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" +SERVICE_HOMEASSISTANT_RESTART = "restart" SERVICE_TURN_ON = 'turn_on' SERVICE_TURN_OFF = 'turn_off' diff --git a/homeassistant/core.py b/homeassistant/core.py index 853d09020ce..e6b0a6ec722 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -16,7 +16,8 @@ from collections import namedtuple from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, + SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, + EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) @@ -70,13 +71,21 @@ class HomeAssistant(object): def block_till_stopped(self): """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() + request_restart = threading.Event() def stop_homeassistant(*args): """Stop Home Assistant.""" request_shutdown.set() + def restart_homeassistant(*args): + """Reset Home Assistant.""" + request_restart.set() + request_shutdown.set() + self.services.register( DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) + self.services.register( + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, restart_homeassistant) if os.name != "nt": try: @@ -92,6 +101,7 @@ class HomeAssistant(object): break self.stop() + return request_restart.isSet() def stop(self): """Stop Home Assistant and shuts down all threads.""" From 519abbbfa2b7bffdcb5c075ac7ec7bed535b2682 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:41:57 -0500 Subject: [PATCH 02/21] Better handling of second KeyboardInterrupt Now the second KeyboardInterrupt will be cleanly handled by the parent process. --- homeassistant/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 7da1e6658e1..ac9d5eabd70 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -258,7 +258,10 @@ def run_hass_process(hass_proc): hass_proc.join() except KeyboardInterrupt: request_stop() - hass_proc.join() + try: + hass_proc.join() + except KeyboardInterrupt: + return False return not requested_stop.isSet() and hass_proc.exitcode > 0 From 3534c975f371a91b00cd39d1679dea4890a97276 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:46:01 -0500 Subject: [PATCH 03/21] Added missing CONF_ICON constant --- homeassistant/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2d605a7ee71..e59eb0fa64a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -10,6 +10,7 @@ MATCH_ALL = '*' DEVICE_DEFAULT_NAME = "Unnamed Device" # #### CONFIG #### +CONF_ICON = "icon" CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" CONF_TEMPERATURE_UNIT = "temperature_unit" From a41b66bb94ee8be5e8f02b2910a4221f535d7acb Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 29 Jan 2016 22:02:39 -0500 Subject: [PATCH 04/21] Cleaned up block_till_stop in core.py 1. Removed handling of KeyboardInterrupt. This will no longer happen now that HASS is run in a subprocess. The KeyboardInterrupt will not be sent to the parent process which will send a SIGTERM to the HASS process. 2. Fixed logger warning about not being able to bind to SIGTERM. 3. Removed check for Windows OSs when binding to SIGTERM. This check was originally put in place when HASS was binding to SIGQUIT. SIGTERM exists in NT OSs, so the check is no longer required. 3. Now returning exit code of 100 when requesting a restart. This will allow the parent process to only restart HASS if it is specifically requested and not just on any encountered crash. --- homeassistant/core.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index e6b0a6ec722..eaaecfe87ee 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -48,6 +48,9 @@ _LOGGER = logging.getLogger(__name__) # Temporary to support deprecated methods _MockHA = namedtuple("MockHomeAssistant", ['bus']) +# The exit code to send to request a restart +RESTART_EXIT_CODE = 100 + class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -87,21 +90,17 @@ class HomeAssistant(object): self.services.register( DOMAIN, SERVICE_HOMEASSISTANT_RESTART, restart_homeassistant) - if os.name != "nt": - try: - signal.signal(signal.SIGTERM, stop_homeassistant) - except ValueError: - _LOGGER.warning( - 'Could not bind to SIGQUIT. Are you running in a thread?') + try: + signal.signal(signal.SIGTERM, stop_homeassistant) + except ValueError: + _LOGGER.warning( + 'Could not bind to SIGTERM. Are you running in a thread?') while not request_shutdown.isSet(): - try: - time.sleep(1) - except KeyboardInterrupt: - break + time.sleep(1) self.stop() - return request_restart.isSet() + return RESTART_EXIT_CODE if request_restart.isSet() else 0 def stop(self): """Stop Home Assistant and shuts down all threads.""" From b56369855af490442f81c37ef12e5c7348e4ae88 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 29 Jan 2016 22:11:11 -0500 Subject: [PATCH 05/21] Cleaned up restart handling in __main__.py 1. Fixed logged message about SIGTERM binding failure. 2. Set to only restart HASS with an exit code of 100. 3. Fixed typo in comment. --- homeassistant/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index ac9d5eabd70..d7cfd0a2f00 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -251,7 +251,7 @@ def run_hass_process(hass_proc): try: signal.signal(signal.SIGTERM, request_stop) except ValueError: - print('Could not bind to SIGQUIT. Are you running in a thread?') + print('Could not bind to SIGTERM. Are you running in a thread?') hass_proc.start() try: @@ -262,7 +262,7 @@ def run_hass_process(hass_proc): hass_proc.join() except KeyboardInterrupt: return False - return not requested_stop.isSet() and hass_proc.exitcode > 0 + return not requested_stop.isSet() and hass_proc.exitcode == 100 def main(): @@ -294,7 +294,7 @@ def main(): if args.pid_file: write_pid(args.pid_file) - # Run hass is child process. Restart if necessary. + # Run hass as child process. Restart if necessary. keep_running = True while keep_running: hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args)) From 106c53abf1414a8bc3dc0e570f8afe4ba0243db8 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 29 Jan 2016 22:42:39 -0500 Subject: [PATCH 06/21] Revised HASS Core test Changed the HASS Core test that tested KeyboardInterrupt handling to now test SIGTERM handling. KeyboardInterrupts are no longer handled in the HASS application process as they are handled in the HASS parent process. SIGTERM is the proper way to now stop HASS. --- tests/test_core.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ca935e2d106..4a0096809c8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,6 +7,7 @@ Provides tests to verify that Home Assistant core works. # pylint: disable=protected-access,too-many-public-methods # pylint: disable=too-few-public-methods import os +import signal import unittest from unittest.mock import patch import time @@ -79,15 +80,15 @@ class TestHomeAssistant(unittest.TestCase): self.assertFalse(blocking_thread.is_alive()) - def test_stopping_with_keyboardinterrupt(self): + def test_stopping_with_sigterm(self): calls = [] self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: calls.append(1)) - def raise_keyboardinterrupt(length): - raise KeyboardInterrupt + def send_sigterm(length): + os.kill(os.getpid(), signal.SIGTERM) - with patch('homeassistant.core.time.sleep', raise_keyboardinterrupt): + with patch('homeassistant.core.time.sleep', send_sigterm): self.hass.block_till_stopped() self.assertEqual(1, len(calls)) From 4b253d17badb08bea76432df9376be84a8e5626b Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 31 Jan 2016 00:46:08 +0100 Subject: [PATCH 07/21] use yaml safe loader --- homeassistant/util/yaml.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 26d7c6c316e..50355e43799 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -19,7 +19,7 @@ def load_yaml(fname): with open(fname, encoding='utf-8') as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict - return yaml.load(conf_file) or {} + return yaml.safe_load(conf_file) or {} except yaml.YAMLError: error = 'Error reading YAML configuration file {}'.format(fname) _LOGGER.exception(error) @@ -45,6 +45,6 @@ def _ordered_dict(loader, node): return OrderedDict(loader.construct_pairs(node)) -yaml.add_constructor('!include', _include_yaml) -yaml.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, - _ordered_dict) +yaml.SafeLoader.add_constructor('!include', _include_yaml) +yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + _ordered_dict) From d6b19aae48df00f887b19e9c3a5d7d7d235d809c Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Sun, 31 Jan 2016 22:56:48 +0100 Subject: [PATCH 08/21] - check for reasonable temperature values - round temperature to one digit --- homeassistant/components/sensor/onewire.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 1266f36485c..814289d2ffa 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -96,5 +96,7 @@ class OneWire(Entity): equals_pos = lines[1].find('t=') if equals_pos != -1: temp_string = lines[1][equals_pos+2:] - temp = float(temp_string) / 1000.0 + temp = round(float(temp_string) / 1000.0, 1) + if temp < -55 or temp > 125: + return self._state = temp From 5005b2012240e71361af9ef1d0728a1936c7e8d9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 1 Feb 2016 15:50:17 +0100 Subject: [PATCH 09/21] Added and fixed yr tests --- homeassistant/components/sensor/yr.py | 2 +- tests/components/sensor/test_yr.py | 42 +++++++++++++++++---------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 5b0f55e1730..b284e85e4dd 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -28,7 +28,7 @@ SENSOR_TYPES = { 'temperature': ['Temperature', '°C'], 'windSpeed': ['Wind speed', 'm/s'], 'windGust': ['Wind gust', 'm/s'], - 'pressure': ['Pressure', 'mbar'], + 'pressure': ['Pressure', 'hPa'], 'windDirection': ['Wind direction', '°'], 'humidity': ['Humidity', '%'], 'fog': ['Fog', '%'], diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 780176dd1b8..59f2b6b676b 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -43,37 +43,47 @@ class TestSensorYr: state = self.hass.states.get('sensor.yr_symbol') + assert '46' == state.state assert state.state.isnumeric() assert state.attributes.get('unit_of_measurement') is None def test_custom_setup(self, betamax_session): + now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.sensor.yr.requests.Session', return_value=betamax_session): - assert sensor.setup(self.hass, { - 'sensor': { - 'platform': 'yr', - 'elevation': 0, - 'monitored_conditions': { - 'pressure', - 'windDirection', - 'humidity', - 'fog', - 'windSpeed' + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': { + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed' + } } - } - }) + }) state = self.hass.states.get('sensor.yr_pressure') - assert 'hPa', state.attributes.get('unit_of_measurement') + assert 'hPa' == state.attributes.get('unit_of_measurement') + assert '1025.1' == state.state state = self.hass.states.get('sensor.yr_wind_direction') - assert '°', state.attributes.get('unit_of_measurement') + assert '°'== state.attributes.get('unit_of_measurement') + assert '81.8' == state.state state = self.hass.states.get('sensor.yr_humidity') - assert '%', state.attributes.get('unit_of_measurement') + assert '%' == state.attributes.get('unit_of_measurement') + assert '79.6' == state.state state = self.hass.states.get('sensor.yr_fog') - assert '%', state.attributes.get('unit_of_measurement') + assert '%' == state.attributes.get('unit_of_measurement') + assert '0.0' == state.state state = self.hass.states.get('sensor.yr_wind_speed') assert 'm/s', state.attributes.get('unit_of_measurement') + assert '4.3' == state.state From ac0b6ca50c7388f86a60a303757651a43bc2de22 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 1 Feb 2016 12:08:08 +0100 Subject: [PATCH 10/21] handle situation where no name is set yet for the sensor --- homeassistant/components/sensor/tellduslive.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 364b790ce6f..c9afbf24eae 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -11,7 +11,9 @@ import logging from datetime import datetime -from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL +from homeassistant.const import (TEMP_CELCIUS, + ATTR_BATTERY_LEVEL, + DEVICE_DEFAULT_NAME) from homeassistant.helpers.entity import Entity from homeassistant.components import tellduslive @@ -64,7 +66,8 @@ class TelldusLiveSensor(Entity): self._sensor_id = sensor_id self._sensor_type = sensor_type self._state = None - self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self._name = "{} {}".format(sensor_name or DEVICE_DEFAULT_NAME, + SENSOR_TYPES[sensor_type][0]) self._last_update = None self._battery_level = None self.update() From 95748a688028cfd233b53a7057800f92ef8b41ab Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 1 Feb 2016 17:45:18 +0000 Subject: [PATCH 11/21] Generate entity id correctly, was using friendly_name. --- homeassistant/components/sensor/template.py | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 4af3cae9260..e712d0cf51c 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -9,16 +9,20 @@ https://home-assistant.io/components/sensor.template/ """ import logging -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.core import EVENT_STATE_CHANGED from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, ATTR_UNIT_OF_MEASUREMENT) -from homeassistant.util import template +from homeassistant.util import template, slugify from homeassistant.exceptions import TemplateError +from homeassistant.components.sensor import DOMAIN + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + _LOGGER = logging.getLogger(__name__) CONF_SENSORS = 'sensors' STATE_ERROR = 'error' @@ -34,9 +38,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False for device, device_config in config[CONF_SENSORS].items(): + + if device != slugify(device): + _LOGGER.error("Found invalid key for sensor.template: %s. " + "Use %s instead", device, slugify(device)) + continue + if not isinstance(device_config, dict): _LOGGER.error("Missing configuration data for sensor %s", device) continue + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) state_template = device_config.get(CONF_VALUE_TEMPLATE) @@ -44,9 +55,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error( "Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device) continue + sensors.append( SensorTemplate( hass, + device, friendly_name, unit_of_measurement, state_template) @@ -64,10 +77,15 @@ class SensorTemplate(Entity): # pylint: disable=too-many-arguments def __init__(self, hass, + device_id, friendly_name, unit_of_measurement, state_template): + self.entity_id = generate_entity_id( + ENTITY_ID_FORMAT, device_id, + hass=hass) + self.hass = hass self._name = friendly_name self._unit_of_measurement = unit_of_measurement From d54e10e54a5653d53345c59bddd3b37244b57103 Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 1 Feb 2016 18:18:51 +0000 Subject: [PATCH 12/21] Improve test coverage of error conditions. --- tests/components/sensor/test_template.py | 43 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 513117a8a9e..55657bc0336 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -4,9 +4,6 @@ tests.components.sensor.template Tests template sensor. """ -from unittest.mock import patch - -import pytest import homeassistant.core as ha import homeassistant.components.sensor as sensor @@ -56,8 +53,46 @@ class TestTemplateSensor: } }) - self.hass.states.set('sensor.test_state', 'Works') self.hass.pool.block_till_done() state = self.hass.states.get('sensor.test_template_sensor') assert state.state == 'error' + + def test_invalid_name_does_not_create(self): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test INVALID sensor': { + 'value_template': + "{{ states.sensor.test_state.state }}" + } + } + } + }) + assert self.hass.states.all() == [] + + def test_invalid_config_does_not_create(self): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': {} + } + } + }) + assert self.hass.states.all() == [] + + def test_missing_template_does_not_create(self): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'not_value_template': + "{{ states.sensor.test_state.state }}" + } + } + } + }) + assert self.hass.states.all() == [] From cb2e75befd4e1bcc88383a568ad17da435e0fdd5 Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Mon, 1 Feb 2016 19:24:08 +0100 Subject: [PATCH 13/21] removed trailing whitespace --- homeassistant/components/sensor/onewire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 814289d2ffa..c1cadb71e19 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -98,5 +98,5 @@ class OneWire(Entity): temp_string = lines[1][equals_pos+2:] temp = round(float(temp_string) / 1000.0, 1) if temp < -55 or temp > 125: - return + return self._state = temp From 9caa4752a43283547d4891ee501555b65074fbe8 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 1 Feb 2016 18:29:43 +0000 Subject: [PATCH 14/21] New liffylights release improves device detection Increase device polling to 30 seconds --- homeassistant/components/light/lifx.py | 10 ++++++++-- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index cb7e8cbf647..2e9bb56fa65 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -28,7 +28,7 @@ from homeassistant.components.light import \ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['liffylights==0.9.3'] +REQUIREMENTS = ['liffylights==0.9.4'] DEPENDENCIES = [] CONF_SERVER = "server" # server address configuration item @@ -76,6 +76,12 @@ class LIFX(): power, hue, sat, bri, kel) self._devices.append(bulb) self._add_devices_callback([bulb]) + else: + _LOGGER.debug("update bulb %s %s %d %d %d %d %d", + ipaddr, name, power, hue, sat, bri, kel) + bulb.set_power(power) + bulb.set_color(hue, sat, bri, kel) + bulb.update_ha_state() # pylint: disable=too-many-arguments def on_color(self, ipaddr, hue, sat, bri, kel): @@ -109,7 +115,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr) # register our poll service - track_time_change(hass, lifx_library.poll, second=10) + track_time_change(hass, lifx_library.poll, second=[10,40]) lifx_library.probe() diff --git a/requirements_all.txt b/requirements_all.txt index 8b2607f3e97..e6ef472e8e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,7 +52,7 @@ blinkstick==1.1.7 phue==0.8 # homeassistant.components.light.lifx -liffylights==0.9.3 +liffylights==0.9.4 # homeassistant.components.light.limitlessled limitlessled==1.0.0 From 7c1241c1f84feaeed51cecd23c835bc5165e3454 Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 1 Feb 2016 18:30:39 +0000 Subject: [PATCH 15/21] Add another test, revise another. Improve coverage. --- tests/components/sensor/test_template.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 55657bc0336..cc416e28f4e 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -72,17 +72,25 @@ class TestTemplateSensor: }) assert self.hass.states.all() == [] - def test_invalid_config_does_not_create(self): + def test_invalid_sensor_does_not_create(self): assert sensor.setup(self.hass, { 'sensor': { 'platform': 'template', 'sensors': { - 'test_template_sensor': {} + 'test_template_sensor': 'invalid' } } }) assert self.hass.states.all() == [] + def test_no_sensors_does_not_create(self): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template' + } + }) + assert self.hass.states.all() == [] + def test_missing_template_does_not_create(self): assert sensor.setup(self.hass, { 'sensor': { From 9c33af60f2a30e749dcbe825a8f2db0321d1d26b Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 1 Feb 2016 18:38:11 +0000 Subject: [PATCH 16/21] Fix unreachable code! --- homeassistant/components/sensor/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index e712d0cf51c..b87e26aa415 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): unit_of_measurement, state_template) ) - if sensors is None: + if not sensors: _LOGGER.error("No sensors added") return False add_devices(sensors) From 031e7a4013eef14aea50fdcf0bb9ba7a8ef3af3d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 1 Feb 2016 18:29:43 +0000 Subject: [PATCH 17/21] New liffylights release improves device detection Increase device polling to 30 seconds --- homeassistant/components/light/lifx.py | 10 ++++++++-- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 4979c1dc2d6..81466dd601c 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -16,7 +16,7 @@ from homeassistant.components.light import \ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['liffylights==0.9.3'] +REQUIREMENTS = ['liffylights==0.9.4'] DEPENDENCIES = [] CONF_SERVER = "server" # server address configuration item @@ -64,6 +64,12 @@ class LIFX(): power, hue, sat, bri, kel) self._devices.append(bulb) self._add_devices_callback([bulb]) + else: + _LOGGER.debug("update bulb %s %s %d %d %d %d %d", + ipaddr, name, power, hue, sat, bri, kel) + bulb.set_power(power) + bulb.set_color(hue, sat, bri, kel) + bulb.update_ha_state() # pylint: disable=too-many-arguments def on_color(self, ipaddr, hue, sat, bri, kel): @@ -97,7 +103,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr) # register our poll service - track_time_change(hass, lifx_library.poll, second=10) + track_time_change(hass, lifx_library.poll, second=[10,40]) lifx_library.probe() diff --git a/requirements_all.txt b/requirements_all.txt index 704857206e9..8435ffd01d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ insteon_hub==0.4.5 jsonrpc-requests==0.1 # homeassistant.components.light.lifx -liffylights==0.9.3 +liffylights==0.9.4 # homeassistant.components.light.limitlessled limitlessled==1.0.0 From 08ab7dba2ca2d440a3e54deb92a69cfd31f7ad14 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 2 Feb 2016 00:21:15 +0000 Subject: [PATCH 18/21] Fix whitespace --- homeassistant/components/light/lifx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 81466dd601c..63c5e923ea6 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr) # register our poll service - track_time_change(hass, lifx_library.poll, second=[10,40]) + track_time_change(hass, lifx_library.poll, second=[10, 40]) lifx_library.probe() From c8bfd2718217243e58388780a039b1f71ffc5758 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Feb 2016 21:07:33 -0800 Subject: [PATCH 19/21] No longer ignore ports for Chromecasts --- homeassistant/components/media_player/cast.py | 40 +++++++++---------- tests/components/media_player/test_cast.py | 30 ++++++++++++++ 2 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 tests/components/media_player/test_cast.py diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index c08a61826ef..9b7034220eb 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -28,48 +28,45 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE | SUPPORT_PLAY_MEDIA KNOWN_HOSTS = [] -# pylint: disable=invalid-name -cast = None +DEFAULT_PORT = 8009 # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the cast platform. """ - global cast import pychromecast - cast = pychromecast - logger = logging.getLogger(__name__) # import CEC IGNORE attributes ignore_cec = config.get(CONF_IGNORE_CEC, []) if isinstance(ignore_cec, list): - cast.IGNORE_CEC += ignore_cec + pychromecast.IGNORE_CEC += ignore_cec else: - logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC) + logger.error('CEC config "%s" must be a list.', CONF_IGNORE_CEC) hosts = [] - if discovery_info and discovery_info[0] not in KNOWN_HOSTS: - hosts = [discovery_info[0]] + if discovery_info and discovery_info in KNOWN_HOSTS: + return + + elif discovery_info: + hosts = [discovery_info] elif CONF_HOST in config: - hosts = [config[CONF_HOST]] + hosts = [(config[CONF_HOST], DEFAULT_PORT)] else: - hosts = (host_port[0] for host_port - in cast.discover_chromecasts() - if host_port[0] not in KNOWN_HOSTS) + hosts = [host for host in pychromecast.discover_chromecasts() + if host not in KNOWN_HOSTS] casts = [] for host in hosts: try: - casts.append(CastDevice(host)) - except cast.ChromecastConnectionError: - pass - else: + casts.append(CastDevice(*host)) KNOWN_HOSTS.append(host) + except pychromecast.ChromecastConnectionError: + pass add_devices(casts) @@ -80,9 +77,10 @@ class CastDevice(MediaPlayerDevice): # pylint: disable=abstract-method # pylint: disable=too-many-public-methods - def __init__(self, host): + def __init__(self, host, port): + import pychromecast import pychromecast.controllers.youtube as youtube - self.cast = cast.Chromecast(host) + self.cast = pychromecast.Chromecast(host, port) self.youtube = youtube.YouTubeController() self.cast.register_handler(self.youtube) @@ -224,11 +222,13 @@ class CastDevice(MediaPlayerDevice): """ Turns on the ChromeCast. """ # The only way we can turn the Chromecast is on is by launching an app if not self.cast.status or not self.cast.status.is_active_input: + import pychromecast + if self.cast.app_id: self.cast.quit_app() self.cast.play_media( - CAST_SPLASH, cast.STREAM_TYPE_BUFFERED) + CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) def turn_off(self): """ Turns Chromecast off. """ diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py new file mode 100644 index 00000000000..296c5590593 --- /dev/null +++ b/tests/components/media_player/test_cast.py @@ -0,0 +1,30 @@ +""" +tests.component.media_player.test_cast +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests cast media_player component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest +from unittest.mock import patch + +from homeassistant.components.media_player import cast + + +class TestCastMediaPlayer(unittest.TestCase): + """ Test the media_player module. """ + + @patch('homeassistant.components.media_player.cast.CastDevice') + def test_filter_duplicates(self, mock_device): + cast.setup_platform(None, { + 'host': 'some_host' + }, lambda _: _) + + assert mock_device.called + + mock_device.reset_mock() + assert not mock_device.called + + cast.setup_platform(None, {}, lambda _: _, ('some_host', + cast.DEFAULT_PORT)) + assert not mock_device.called From 00d1cab0917e21c7320643ccf3332fd33bb74904 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Feb 2016 08:41:18 +0100 Subject: [PATCH 20/21] add test for unsafe yaml --- tests/test_config.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 781fc51731f..203ea8a6da5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -94,6 +94,15 @@ class TestConfig(unittest.TestCase): with self.assertRaises(HomeAssistantError): config_util.load_yaml_config_file(YAML_PATH) + def test_load_yaml_config_raises_error_if_unsafe_yaml(self): + """ Test error raised if unsafe YAML. """ + with open(YAML_PATH, 'w') as f: + f.write('hello: !!python/object/apply:os.system') + + with self.assertRaises(HomeAssistantError): + config_util.load_yaml_config_file(YAML_PATH) + + def test_load_yaml_config_preserves_key_order(self): with open(YAML_PATH, 'w') as f: f.write('hello: 0\n') From e7e540d4bb60cb414b3135995c82827a78a6670c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Feb 2016 00:31:36 -0800 Subject: [PATCH 21/21] Clean up and test media player --- .../components/media_player/__init__.py | 51 ++----- homeassistant/components/media_player/cast.py | 11 +- homeassistant/components/media_player/demo.py | 17 ++- .../components/media_player/denon.py | 10 +- .../components/media_player/firetv.py | 14 +- homeassistant/components/media_player/kodi.py | 11 +- homeassistant/components/media_player/plex.py | 2 +- .../components/media_player/squeezebox.py | 7 +- .../components/media_player/universal.py | 7 +- tests/components/media_player/test_demo.py | 141 ++++++++++++++++++ tests/components/media_player/test_init.py | 78 ---------- 11 files changed, 168 insertions(+), 181 deletions(-) create mode 100644 tests/components/media_player/test_demo.py delete mode 100644 tests/components/media_player/test_init.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 58256c9b8fd..7dfb4ede173 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -32,7 +32,6 @@ DISCOVERY_PLATFORMS = { discovery.SERVICE_PLEX: 'plex', } -SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' SERVICE_PLAY_MEDIA = 'play_media' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' @@ -68,14 +67,12 @@ SUPPORT_VOLUME_SET = 4 SUPPORT_VOLUME_MUTE = 8 SUPPORT_PREVIOUS_TRACK = 16 SUPPORT_NEXT_TRACK = 32 -SUPPORT_YOUTUBE = 64 + SUPPORT_TURN_ON = 128 SUPPORT_TURN_OFF = 256 SUPPORT_PLAY_MEDIA = 512 SUPPORT_VOLUME_STEP = 1024 -YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' - SERVICE_TO_METHOD = { SERVICE_TURN_ON: 'turn_on', SERVICE_TURN_OFF: 'turn_off', @@ -200,6 +197,13 @@ def media_previous_track(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) +def media_seek(hass, position, entity_id=None): + """ Send the media player the command to seek in current playing media. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_MEDIA_SEEK_POSITION] = position + hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data) + + def play_media(hass, media_type, media_id, entity_id=None): """ Send the media player the command for playing media. """ data = {"media_type": media_type, "media_id": media_id} @@ -283,7 +287,7 @@ def setup(hass, config): position = service.data[ATTR_MEDIA_SEEK_POSITION] for player in target_players: - player.seek(position) + player.media_seek(position) if player.should_poll: player.update_ha_state(True) @@ -291,20 +295,6 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service, descriptions.get(SERVICE_MEDIA_SEEK)) - def play_youtube_video_service(service, media_id=None): - """ Plays specified media_id on the media player. """ - if media_id is None: - service.data.get('video') - - if media_id is None: - return - - for player in component.extract_from_service(service): - player.play_youtube(media_id) - - if player.should_poll: - player.update_ha_state(True) - def play_media_service(service): """ Plays specified media_id on the media player. """ media_type = service.data.get('media_type') @@ -322,20 +312,6 @@ def setup(hass, config): if player.should_poll: player.update_ha_state(True) - hass.services.register( - DOMAIN, "start_fireplace", - lambda service: play_youtube_video_service(service, "eyU3bRy2x44"), - descriptions.get('start_fireplace')) - - hass.services.register( - DOMAIN, "start_epic_sax", - lambda service: play_youtube_video_service(service, "kxopViU98Xo"), - descriptions.get('start_epic_sax')) - - hass.services.register( - DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service, - descriptions.get(SERVICE_YOUTUBE_VIDEO)) - hass.services.register( DOMAIN, SERVICE_PLAY_MEDIA, play_media_service, descriptions.get(SERVICE_PLAY_MEDIA)) @@ -490,10 +466,6 @@ class MediaPlayerDevice(Entity): """ Send seek command. """ raise NotImplementedError() - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - raise NotImplementedError() - def play_media(self, media_type, media_id): """ Plays a piece of media. """ raise NotImplementedError() @@ -529,11 +501,6 @@ class MediaPlayerDevice(Entity): """ Boolean if next track command supported. """ return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK) - @property - def support_youtube(self): - """ Boolean if YouTube is supported. """ - return bool(self.supported_media_commands & SUPPORT_YOUTUBE) - @property def support_play_media(self): """ Boolean if play media command supported. """ diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 9b7034220eb..6b012fe6fbd 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.components.media_player import ( MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE, SUPPORT_PLAY_MEDIA, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) @@ -25,7 +25,7 @@ CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE | SUPPORT_PLAY_MEDIA + SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA KNOWN_HOSTS = [] DEFAULT_PORT = 8009 @@ -79,10 +79,7 @@ class CastDevice(MediaPlayerDevice): def __init__(self, host, port): import pychromecast - import pychromecast.controllers.youtube as youtube self.cast = pychromecast.Chromecast(host, port) - self.youtube = youtube.YouTubeController() - self.cast.register_handler(self.youtube) self.cast.socket_client.receiver_controller.register_status_listener( self) @@ -266,10 +263,6 @@ class CastDevice(MediaPlayerDevice): """ Plays media from a URL """ self.cast.media_controller.play_media(media_id, media_type) - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - self.youtube.play_video(media_id) - # implementation of chromecast status_listener methods def new_cast_status(self, status): diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 2a7bc5bde1b..98524275a5d 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -7,11 +7,11 @@ from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_OFF) from homeassistant.components.media_player import ( - MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT, + MediaPlayerDevice, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE, + SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK, - SUPPORT_NEXT_TRACK) + SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA) # pylint: disable=unused-argument @@ -26,9 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) +YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' + YOUTUBE_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA MUSIC_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -150,10 +152,9 @@ class DemoYoutubePlayer(AbstractDemoPlayer): """ Flags of media commands that are supported. """ return YOUTUBE_PLAYER_SUPPORT - def play_youtube(self, media_id): - """ Plays a YouTube media. """ + def play_media(self, media_type, media_id): + """ Plays a piece of media. """ self.youtube_id = media_id - self._media_title = 'some YouTube video' self.update_ha_state() @@ -234,7 +235,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): """ Flags of media commands that are supported. """ support = MUSIC_PLAYER_SUPPORT - if self._cur_track > 1: + if self._cur_track > 0: support |= SUPPORT_PREVIOUS_TRACK if self._cur_track < len(self.tracks)-1: diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index e8301bc2509..5d4b326f53e 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -24,7 +24,6 @@ SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Denon platform. """ if not config.get(CONF_HOST): @@ -48,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DenonDevice(MediaPlayerDevice): """ Represents a Denon device. """ - # pylint: disable=too-many-public-methods + # pylint: disable=too-many-public-methods, abstract-method def __init__(self, name, host): self._name = name @@ -145,10 +144,6 @@ class DenonDevice(MediaPlayerDevice): """ mute (true) or unmute (false) media player. """ self.telnet_command("MU" + ("ON" if mute else "OFF")) - def media_play_pause(self): - """ media_play_pause media player. """ - raise NotImplementedError() - def media_play(self): """ media_play media player. """ self.telnet_command("NS9A") @@ -164,9 +159,6 @@ class DenonDevice(MediaPlayerDevice): def media_previous_track(self): self.telnet_command("NS9E") - def media_seek(self, position): - raise NotImplementedError() - def turn_on(self): """ turn the media player on. """ self.telnet_command("PWON") diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index e5f9885f86e..45e63a4534b 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -105,6 +105,8 @@ class FireTV(object): class FireTVDevice(MediaPlayerDevice): """ Represents an Amazon Fire TV device on the network. """ + # pylint: disable=abstract-method + def __init__(self, host, device, name): self._firetv = FireTV(host, device) self._name = name @@ -176,15 +178,3 @@ class FireTVDevice(MediaPlayerDevice): def media_next_track(self): """ Send next track command (results in fast-forward). """ self._firetv.action('media_next') - - def media_seek(self, position): - raise NotImplementedError() - - def mute_volume(self, mute): - raise NotImplementedError() - - def play_youtube(self, media_id): - raise NotImplementedError() - - def set_volume_level(self, volume): - raise NotImplementedError() diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 867255a43c4..19893abe519 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -22,7 +22,6 @@ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the kodi platform. """ @@ -47,7 +46,7 @@ def _get_image_url(kodi_url): class KodiDevice(MediaPlayerDevice): """ Represents a XBMC/Kodi device. """ - # pylint: disable=too-many-public-methods + # pylint: disable=too-many-public-methods, abstract-method def __init__(self, name, url, auth=None): import jsonrpc_requests @@ -263,11 +262,3 @@ class KodiDevice(MediaPlayerDevice): self._server.Player.Seek(players[0]['playerid'], time) self.update_ha_state() - - def turn_on(self): - """ turn the media player on. """ - raise NotImplementedError() - - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - raise NotImplementedError() diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index feb1d282551..94af635496d 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -59,7 +59,7 @@ def config_from_file(filename, config=None): return {} -# pylint: disable=abstract-method, unused-argument +# pylint: disable=abstract-method def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Sets up the plex platform. """ diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 05cbb683a52..5b0adfc7c4e 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -27,7 +27,6 @@ SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the squeezebox platform. """ if not config.get(CONF_HOST): @@ -138,7 +137,7 @@ class LogitechMediaServer(object): class SqueezeBoxDevice(MediaPlayerDevice): """ Represents a SqueezeBox device. """ - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, abstract-method def __init__(self, lms, player_id): super(SqueezeBoxDevice, self).__init__() self._lms = lms @@ -292,7 +291,3 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ turn the media player on. """ self._lms.query(self._id, 'power', '1') self.update_ha_state() - - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - raise NotImplementedError() diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 359249f15e2..bb5edacd79f 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -27,7 +27,7 @@ from homeassistant.components.media_player import ( MediaPlayerDevice, DOMAIN, SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, - SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO, + SERVICE_PLAY_MEDIA, ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME, @@ -397,11 +397,6 @@ class UniversalMediaPlayer(MediaPlayerDevice): data = {ATTR_MEDIA_SEEK_POSITION: position} self._call_service(SERVICE_MEDIA_SEEK, data) - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - data = {'media_id': media_id} - self._call_service(SERVICE_YOUTUBE_VIDEO, data) - def play_media(self, media_type, media_id): """ Plays a piece of media. """ data = {'media_type': media_type, 'media_id': media_id} diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py new file mode 100644 index 00000000000..c19fd59e97f --- /dev/null +++ b/tests/components/media_player/test_demo.py @@ -0,0 +1,141 @@ +""" +tests.component.media_player.test_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo media_player component. +""" +import unittest +from unittest.mock import patch +from pprint import pprint +import homeassistant.core as ha +from homeassistant.const import ( + STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) +import homeassistant.components.media_player as mp + + +entity_id = 'media_player.walkman' + + +class TestDemoMediaPlayer(unittest.TestCase): + """ Test the media_player module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_volume_services(self): + assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) + state = self.hass.states.get(entity_id) + assert 1.0 == state.attributes.get('volume_level') + + mp.set_volume_level(self.hass, 0.5, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 0.5 == state.attributes.get('volume_level') + + mp.volume_down(self.hass, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 0.4 == state.attributes.get('volume_level') + + mp.volume_up(self.hass, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 0.5 == state.attributes.get('volume_level') + + assert False is state.attributes.get('is_volume_muted') + mp.mute_volume(self.hass, True, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert True is state.attributes.get('is_volume_muted') + + def test_turning_off_and_on(self): + assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) + assert self.hass.states.is_state(entity_id, 'playing') + + mp.turn_off(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'off') + assert not mp.is_on(self.hass, entity_id) + + mp.turn_on(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'playing') + + mp.toggle(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'off') + assert not mp.is_on(self.hass, entity_id) + + def test_playing_pausing(self): + assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) + assert self.hass.states.is_state(entity_id, 'playing') + + mp.media_pause(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'paused') + + mp.media_play_pause(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'playing') + + mp.media_play_pause(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'paused') + + mp.media_play(self.hass, entity_id) + self.hass.pool.block_till_done() + assert self.hass.states.is_state(entity_id, 'playing') + + def test_prev_next_track(self): + assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) + state = self.hass.states.get(entity_id) + assert 1 == state.attributes.get('media_track') + assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & + state.attributes.get('supported_media_commands')) + + mp.media_next_track(self.hass, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 2 == state.attributes.get('media_track') + assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & + state.attributes.get('supported_media_commands')) + + mp.media_next_track(self.hass, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 3 == state.attributes.get('media_track') + assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & + state.attributes.get('supported_media_commands')) + + mp.media_previous_track(self.hass, entity_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(entity_id) + assert 2 == state.attributes.get('media_track') + assert 0 < (mp.SUPPORT_PREVIOUS_TRACK & + state.attributes.get('supported_media_commands')) + + @patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.media_seek') + def test_play_media(self, mock_seek): + assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}}) + ent_id = 'media_player.living_room' + state = self.hass.states.get(ent_id) + assert 0 < (mp.SUPPORT_PLAY_MEDIA & + state.attributes.get('supported_media_commands')) + assert state.attributes.get('media_content_id') is not None + + mp.play_media(self.hass, 'youtube', 'some_id', ent_id) + self.hass.pool.block_till_done() + state = self.hass.states.get(ent_id) + assert 0 < (mp.SUPPORT_PLAY_MEDIA & + state.attributes.get('supported_media_commands')) + assert 'some_id' == state.attributes.get('media_content_id') + + assert not mock_seek.called + mp.media_seek(self.hass, 100, ent_id) + self.hass.pool.block_till_done() + assert mock_seek.called + diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py deleted file mode 100644 index a0a7ebc9567..00000000000 --- a/tests/components/media_player/test_init.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -tests.test_component_media_player -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tests media_player component. -""" -# pylint: disable=too-many-public-methods,protected-access -import unittest - -import homeassistant.core as ha -from homeassistant.const import ( - STATE_OFF, - SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, - SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_TOGGLE, - ATTR_ENTITY_ID) -import homeassistant.components.media_player as media_player -from tests.common import mock_service - - -class TestMediaPlayer(unittest.TestCase): - """ Test the media_player module. """ - - def setUp(self): # pylint: disable=invalid-name - self.hass = ha.HomeAssistant() - - self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room') - self.hass.states.set(self.test_entity, STATE_OFF) - - self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom') - self.hass.states.set(self.test_entity2, "YouTube") - - def tearDown(self): # pylint: disable=invalid-name - """ Stop down stuff we started. """ - self.hass.stop() - - def test_is_on(self): - """ Test is_on method. """ - self.assertFalse(media_player.is_on(self.hass, self.test_entity)) - self.assertTrue(media_player.is_on(self.hass, self.test_entity2)) - - def test_services(self): - """ - Test if the call service methods convert to correct service calls. - """ - services = { - SERVICE_TURN_ON: media_player.turn_on, - SERVICE_TURN_OFF: media_player.turn_off, - SERVICE_TOGGLE: media_player.toggle, - SERVICE_VOLUME_UP: media_player.volume_up, - SERVICE_VOLUME_DOWN: media_player.volume_down, - SERVICE_MEDIA_PLAY_PAUSE: media_player.media_play_pause, - SERVICE_MEDIA_PLAY: media_player.media_play, - SERVICE_MEDIA_PAUSE: media_player.media_pause, - SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track, - SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track - } - - for service_name, service_method in services.items(): - calls = mock_service(self.hass, media_player.DOMAIN, service_name) - - service_method(self.hass) - self.hass.pool.block_till_done() - - self.assertEqual(1, len(calls)) - call = calls[-1] - self.assertEqual(media_player.DOMAIN, call.domain) - self.assertEqual(service_name, call.service) - - service_method(self.hass, self.test_entity) - self.hass.pool.block_till_done() - - self.assertEqual(2, len(calls)) - call = calls[-1] - self.assertEqual(media_player.DOMAIN, call.domain) - self.assertEqual(service_name, call.service) - self.assertEqual(self.test_entity, - call.data.get(ATTR_ENTITY_ID))