diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e97ed0c6386..d7cfd0a2f00 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,64 @@ 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 SIGTERM. Are you running in a thread?') + + hass_proc.start() + try: + hass_proc.join() + except KeyboardInterrupt: + request_stop() + try: + hass_proc.join() + except KeyboardInterrupt: + return False + return not requested_stop.isSet() and hass_proc.exitcode == 100 + + def main(): """ Starts Home Assistant. """ validate_python() @@ -233,33 +294,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 as 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/components/light/lifx.py b/homeassistant/components/light/lifx.py index 4979c1dc2d6..63c5e923ea6 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/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 c08a61826ef..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,51 +25,48 @@ 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 = [] -# 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,11 +77,9 @@ class CastDevice(MediaPlayerDevice): # pylint: disable=abstract-method # pylint: disable=too-many-public-methods - def __init__(self, host): - import pychromecast.controllers.youtube as youtube - self.cast = cast.Chromecast(host) - self.youtube = youtube.YouTubeController() - self.cast.register_handler(self.youtube) + def __init__(self, host, port): + import pychromecast + self.cast = pychromecast.Chromecast(host, port) self.cast.socket_client.receiver_controller.register_status_listener( self) @@ -224,11 +219,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. """ @@ -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/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 1266f36485c..c1cadb71e19 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 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() diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 4af3cae9260..b87e26aa415 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,14 +55,16 @@ 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) ) - if sensors is None: + if not sensors: _LOGGER.error("No sensors added") return False add_devices(sensors) @@ -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 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/homeassistant/const.py b/homeassistant/const.py index e50d8ddb96d..cd711df44cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -126,6 +126,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 6f95cedb9a9..936a068480f 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, ATTR_SERVICE_DATA) @@ -47,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.""" @@ -70,28 +74,33 @@ 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: - 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 RESTART_EXIT_CODE if request_restart.isSet() else 0 def stop(self): """Stop Home Assistant and shuts down all threads.""" 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) 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 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 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)) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 513117a8a9e..cc416e28f4e 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,54 @@ 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_sensor_does_not_create(self): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + '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': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'not_value_template': + "{{ states.sensor.test_state.state }}" + } + } + } + }) + assert self.hass.states.all() == [] 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 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') 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))