From dd271febaedf24ce0d1016403b056a8a563ef376 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Dec 2013 00:07:30 -0800 Subject: [PATCH] Reorg: Merged observers, actors and HTTPInterface into components --- README.md | 2 +- homeassistant/__init__.py | 25 +- homeassistant/actors.py | 354 ------------------ homeassistant/bootstrap.py | 53 +-- homeassistant/components/__init__.py | 6 + homeassistant/components/browser.py | 22 ++ homeassistant/components/chromecast.py | 103 +++++ .../{observers.py => components/device.py} | 303 ++------------- .../components/device_sun_light_trigger.py | 142 +++++++ homeassistant/components/downloader.py | 86 +++++ homeassistant/components/general.py | 26 ++ .../httpinterface/__init__.py} | 2 +- .../httpinterface}/www_static/favicon.ico | Bin .../httpinterface}/www_static/style.css | 0 homeassistant/components/keyboard.py | 56 +++ homeassistant/components/light.py | 181 +++++++++ homeassistant/components/sun.py | 89 +++++ homeassistant/remote.py | 2 +- homeassistant/test.py | 2 +- 19 files changed, 795 insertions(+), 659 deletions(-) delete mode 100644 homeassistant/actors.py create mode 100644 homeassistant/components/__init__.py create mode 100644 homeassistant/components/browser.py create mode 100644 homeassistant/components/chromecast.py rename homeassistant/{observers.py => components/device.py} (57%) create mode 100644 homeassistant/components/device_sun_light_trigger.py create mode 100644 homeassistant/components/downloader.py create mode 100644 homeassistant/components/general.py rename homeassistant/{httpinterface.py => components/httpinterface/__init__.py} (99%) rename homeassistant/{ => components/httpinterface}/www_static/favicon.ico (100%) rename homeassistant/{ => components/httpinterface}/www_static/style.css (100%) create mode 100644 homeassistant/components/keyboard.py create mode 100644 homeassistant/components/light.py create mode 100644 homeassistant/components/sun.py diff --git a/README.md b/README.md index 6398d4cc6ec..10170b7b5a4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It is currently able to do the following things: * Track what your Chromecasts are up to * Turn on the lights when people get home when it is dark * Slowly turn on the lights to compensate for light loss when the sun sets - * Turn off the lights when everybody leaves the house + * Turn off lights and connected devices when everybody leaves the house * Start YouTube video's on the Chromecast * Download files to the host * Open a url in the default browser on the host diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index f810ab77f2c..223db1afe8d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -16,8 +16,10 @@ logging.basicConfig(level=logging.INFO) ALL_EVENTS = '*' - DOMAIN_HOMEASSISTANT = "homeassistant" + +SERVICE_TURN_ON = "turn_on" +SERVICE_TURN_OFF = "turn_off" SERVICE_HOMEASSISTANT_STOP = "stop" EVENT_HOMEASSISTANT_START = "homeassistant.start" @@ -89,6 +91,27 @@ def _matcher(subject, pattern): return '*' in pattern or subject in pattern +def get_grouped_state_cats(statemachine, cat_format_string, strip_prefix): + """ Get states that are part of a group of states. + + Example category_format_string can be "devices.{}" + + If input states are devices, devices.paulus and devices.paulus.charging + then the output will be paulus if strip_prefix is True, else devices.paulus + """ + group_prefix = cat_format_string.format("") + + if strip_prefix: + id_part = slice(len(group_prefix), None) + + return [cat[id_part] for cat in statemachine.categories + if cat.startswith(group_prefix) and cat.count(".") == 1] + + else: + return [cat for cat in statemachine.categories + if cat.startswith(group_prefix) and cat.count(".") == 1] + + def create_state(state, attributes=None, last_changed=None): """ Creates a new state and initializes defaults where necessary. """ attributes = attributes or {} diff --git a/homeassistant/actors.py b/homeassistant/actors.py deleted file mode 100644 index 756f65367ec..00000000000 --- a/homeassistant/actors.py +++ /dev/null @@ -1,354 +0,0 @@ -""" -homeassistant.actors -~~~~~~~~~~~~~~~~~~~~ - -This module provides actors that will react to events happening within -homeassistant or provide services. - -""" - -import os -import logging -from datetime import datetime, timedelta -import re - -import requests - -import homeassistant as ha -import homeassistant.util as util -from homeassistant.observers import ( - STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON, - is_sun_up, next_sun_setting, - - STATE_CATEGORY_ALL_DEVICES, DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME, - STATE_CATEGORY_DEVICE_FORMAT, get_device_ids, is_device_home, - - is_light_on, turn_light_on, turn_light_off, get_light_ids) - -LIGHT_TRANSITION_TIME = timedelta(minutes=15) - -DOMAIN_DOWNLOADER = "downloader" -DOMAIN_BROWSER = "browser" -DOMAIN_KEYBOARD = "keyboard" - -SERVICE_DOWNLOAD_FILE = "download_file" -SERVICE_BROWSE_URL = "browse_url" -SERVICE_KEYBOARD_VOLUME_UP = "volume_up" -SERVICE_KEYBOARD_VOLUME_DOWN = "volume_down" -SERVICE_KEYBOARD_VOLUME_MUTE = "volume_mute" -SERVICE_KEYBOARD_MEDIA_PLAY_PAUSE = "media_play_pause" -SERVICE_KEYBOARD_MEDIA_NEXT_TRACK = "media_next_track" -SERVICE_KEYBOARD_MEDIA_PREV_TRACK = "media_prev_track" - - -# pylint: disable=too-many-branches -def setup_device_light_triggers(bus, statemachine): - """ Triggers to turn lights on or off based on device precense. """ - - logger = logging.getLogger(__name__) - - device_state_categories = [STATE_CATEGORY_DEVICE_FORMAT.format(device_id) - for device_id in get_device_ids(statemachine)] - - if len(device_state_categories) == 0: - logger.error("LightTrigger:No devices given to track") - - return False - - light_ids = get_light_ids(statemachine) - - if len(light_ids) == 0: - logger.error("LightTrigger:No lights found to turn on") - - return False - - # Calculates the time when to start fading lights in when sun sets - time_for_light_before_sun_set = lambda: \ - (next_sun_setting(statemachine) - LIGHT_TRANSITION_TIME * - len(light_ids)) - - # pylint: disable=unused-argument - def handle_sun_rising(category, old_state, new_state): - """The moment sun sets we want to have all the lights on. - We will schedule to have each light start after one another - and slowly transition in.""" - - def turn_light_on_before_sunset(light_id): - """ Helper function to turn on lights slowly if there - are devices home and the light is not on yet. """ - if (is_device_home(statemachine) and - not is_light_on(statemachine, light_id)): - - turn_light_on(bus, light_id, LIGHT_TRANSITION_TIME.seconds) - - def turn_on(light_id): - """ Lambda can keep track of function parameters but not local - parameters. If we put the lambda directly in the below statement - only the last light will be turned on.. """ - return lambda now: turn_light_on_before_sunset(light_id) - - start_point = time_for_light_before_sun_set() - - for index, light_id in enumerate(light_ids): - ha.track_time_change(bus, turn_on(light_id), - point_in_time=(start_point + - index * LIGHT_TRANSITION_TIME)) - - # Track every time sun rises so we can schedule a time-based - # pre-sun set event - ha.track_state_change(bus, STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, - SUN_STATE_ABOVE_HORIZON, handle_sun_rising) - - # If the sun is already above horizon - # schedule the time-based pre-sun set event - if is_sun_up(statemachine): - handle_sun_rising(None, None, None) - - def handle_device_state_change(category, old_state, new_state): - """ Function to handle tracked device state changes. """ - lights_are_on = is_light_on(statemachine) - - light_needed = not (lights_are_on or is_sun_up(statemachine)) - - # Specific device came home ? - if (category != STATE_CATEGORY_ALL_DEVICES and - new_state['state'] == DEVICE_STATE_HOME): - - # These variables are needed for the elif check - now = datetime.now() - start_point = time_for_light_before_sun_set() - - # Do we need lights? - if light_needed: - - logger.info( - "Home coming event for {}. Turning lights on". - format(category)) - - turn_light_on(bus) - - # Are we in the time span were we would turn on the lights - # if someone would be home? - # Check this by seeing if current time is later then the point - # in time when we would start putting the lights on. - elif start_point < now < next_sun_setting(statemachine): - - # Check for every light if it would be on if someone was home - # when the fading in started and turn it on if so - for index, light_id in enumerate(light_ids): - - if now > start_point + index * LIGHT_TRANSITION_TIME: - turn_light_on(bus, light_id) - - else: - # If this light didn't happen to be turned on yet so - # will all the following then, break. - break - - # Did all devices leave the house? - elif (category == STATE_CATEGORY_ALL_DEVICES and - new_state['state'] == DEVICE_STATE_NOT_HOME and lights_are_on): - - logger.info( - "Everyone has left but lights are on. Turning lights off") - - turn_light_off(bus) - - # Track home coming of each seperate device - for category in device_state_categories: - ha.track_state_change(bus, category, - DEVICE_STATE_NOT_HOME, DEVICE_STATE_HOME, - handle_device_state_change) - - # Track when all devices are gone to shut down lights - ha.track_state_change(bus, STATE_CATEGORY_ALL_DEVICES, - DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME, - handle_device_state_change) - - return True - - -class HueLightControl(object): - """ Class to interface with the Hue light system. """ - - def __init__(self, host=None): - try: - import phue - except ImportError: - logging.getLogger(__name__).exception( - "HueLightControl: Error while importing dependency phue.") - - self.success_init = False - - return - - self._bridge = phue.Bridge(host) - - self._light_map = {util.slugify(light.name): light for light - in self._bridge.get_light_objects()} - - self.success_init = True - - @property - def light_ids(self): - """ Return a list of light ids. """ - return self._light_map.keys() - - def is_light_on(self, light_id=None): - """ Returns if specified or all light are on. """ - if not light_id: - return sum( - [1 for light in self._light_map.values() if light.on]) > 0 - - else: - return self._bridge.get_light(self._convert_id(light_id), 'on') - - def turn_light_on(self, light_id=None, transition_seconds=None): - """ Turn the specified or all lights on. """ - self._turn_light(True, light_id, transition_seconds) - - def turn_light_off(self, light_id=None, transition_seconds=None): - """ Turn the specified or all lights off. """ - self._turn_light(False, light_id, transition_seconds) - - def _turn_light(self, turn_on, light_id=None, transition_seconds=None): - """ Helper method to turn lights on or off. """ - if light_id: - light_id = self._convert_id(light_id) - else: - light_id = [light.light_id for light in self._light_map.values()] - - command = {'on': True, 'xy': [0.5119, 0.4147], 'bri': 164} if turn_on \ - else {'on': False} - - if transition_seconds: - # Transition time is in 1/10th seconds and cannot exceed - # MAX_TRANSITION_TIME which is 900 seconds for Hue. - command['transitiontime'] = min(9000, transition_seconds * 10) - - self._bridge.set_light(light_id, command) - - def _convert_id(self, light_id): - """ Returns internal light id to be used with phue. """ - return self._light_map[light_id].light_id - - -def setup_file_downloader(bus, download_path): - """ Listens for download events to download files. """ - - logger = logging.getLogger(__name__) - - if not os.path.isdir(download_path): - - logger.error( - ("FileDownloader:" - "Download path {} does not exist. File Downloader not active."). - format(download_path)) - - return False - - def download_file(service): - """ Downloads file specified in the url. """ - - try: - req = requests.get(service.data['url'], stream=True) - if req.status_code == 200: - filename = None - - if 'content-disposition' in req.headers: - match = re.findall(r"filename=(\S+)", - req.headers['content-disposition']) - - if len(match) > 0: - filename = match[0].strip("'\" ") - - if not filename: - filename = os.path.basename(service.data['url']).strip() - - if not filename: - filename = "ha_download" - - # Remove stuff to ruin paths - filename = util.sanitize_filename(filename) - - path, ext = os.path.splitext(os.path.join(download_path, - filename)) - - # If file exist append a number. We test filename, filename_2.. - tries = 0 - while True: - tries += 1 - - name_suffix = "" if tries == 1 else "_{}".format(tries) - final_path = path + name_suffix + ext - - if not os.path.isfile(final_path): - break - - logger.info("FileDownloader:{} -> {}".format( - service.data['url'], final_path)) - - with open(final_path, 'wb') as fil: - for chunk in req.iter_content(1024): - fil.write(chunk) - - except requests.exceptions.ConnectionError: - logger.exception("FileDownloader:ConnectionError occured for {}". - format(service.data['url'])) - - bus.register_service(DOMAIN_DOWNLOADER, SERVICE_DOWNLOAD_FILE, - download_file) - - return True - - -def setup_webbrowser(bus): - """ Listen for browse_url events and open - the url in the default webbrowser. """ - - import webbrowser - - bus.register_service(DOMAIN_BROWSER, SERVICE_BROWSE_URL, - lambda event: webbrowser.open(event.data['url'])) - - return True - - -def setup_media_buttons(bus): - """ Listen for keyboard events. """ - try: - import pykeyboard - except ImportError: - logging.getLogger(__name__).exception( - "MediaButtons: Error while importing dependency PyUserInput.") - - return False - - keyboard = pykeyboard.PyKeyboard() - keyboard.special_key_assignment() - - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_UP, - lambda event: - keyboard.tap_key(keyboard.volume_up_key)) - - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_DOWN, - lambda event: - keyboard.tap_key(keyboard.volume_down_key)) - - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_MUTE, - lambda event: - keyboard.tap_key(keyboard.volume_mute_key)) - - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_PLAY_PAUSE, - lambda event: - keyboard.tap_key(keyboard.media_play_pause_key)) - - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_NEXT_TRACK, - lambda event: - keyboard.tap_key(keyboard.media_next_track_key)) - - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_PREV_TRACK, - lambda event: - keyboard.tap_key(keyboard.media_prev_track_key)) - - return True diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 61572563a5a..57953da45a1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -6,9 +6,10 @@ import ConfigParser import logging import homeassistant as ha -import homeassistant.observers as observers -import homeassistant.actors as actors -import homeassistant.httpinterface as httpinterface +from homeassistant.components import (general, chromecast, + device_sun_light_trigger, device, + downloader, keyboard, light, sun, + browser, httpinterface) # pylint: disable=too-many-branches @@ -26,25 +27,22 @@ def from_config_file(config_path): bus = ha.Bus() statemachine = ha.StateMachine(bus) - # Init observers # Device scanner if config.has_option('tomato', 'host') and \ config.has_option('tomato', 'username') and \ config.has_option('tomato', 'password') and \ config.has_option('tomato', 'http_id'): - device_scanner = observers.TomatoDeviceScanner( + device_scanner = device.TomatoDeviceScanner( config.get('tomato', 'host'), config.get('tomato', 'username'), config.get('tomato', 'password'), config.get('tomato', 'http_id')) - if device_scanner.success_init: - statusses.append(("Device Scanner - Tomato", True)) - - else: - statusses.append(("Device Scanner - Tomato", False)) + statusses.append(("Device Scanner - Tomato", + device_scanner.success_init)) + if not device_scanner.success_init: device_scanner = None else: @@ -52,7 +50,7 @@ def from_config_file(config_path): # Device Tracker if device_scanner: - observers.DeviceTracker(bus, statemachine, device_scanner) + device.DeviceTracker(bus, statemachine, device_scanner) statusses.append(("Device Tracker", True)) @@ -61,25 +59,26 @@ def from_config_file(config_path): config.has_option("common", "longitude"): statusses.append(("Weather - Ephem", - observers.track_sun( + sun.setup( bus, statemachine, config.get("common", "latitude"), config.get("common", "longitude")))) + # Chromecast if config.has_option("chromecast", "host"): - statusses.append(("Chromecast", - observers.setup_chromecast( - bus, statemachine, - config.get("chromecast", "host")))) + chromecast_started = chromecast.setup(bus, statemachine, + config.get("chromecast", "host")) + + statusses.append(("Chromecast", chromecast_started)) + else: + chromecast_started = False - # -------------------------- - # Init actors # Light control if config.has_section("hue"): if config.has_option("hue", "host"): - light_control = actors.HueLightControl(config.get("hue", "host")) + light_control = light.HueLightControl(config.get("hue", "host")) else: - light_control = actors.HueLightControl() + light_control = light.HueLightControl() statusses.append(("Light Control - Hue", light_control.success_init)) @@ -88,18 +87,22 @@ def from_config_file(config_path): # Light trigger if light_control: - observers.setup_light_control(bus, statemachine, light_control) + light.setup(bus, statemachine, light_control) - statusses.append(("Light Trigger", actors.setup_device_light_triggers( + statusses.append(("Light Trigger", device_sun_light_trigger.setup( bus, statemachine))) if config.has_option("downloader", "download_dir"): - statusses.append(("Downloader", actors.setup_file_downloader( + statusses.append(("Downloader", downloader.setup( bus, config.get("downloader", "download_dir")))) - statusses.append(("Webbrowser", actors.setup_webbrowser(bus))) + # Currently only works with Chromecast or Light_Control + if chromecast_started or light_control: + statusses.append(("General", general.setup(bus, statemachine))) - statusses.append(("Media Buttons", actors.setup_media_buttons(bus))) + statusses.append(("Browser", browser.setup(bus))) + + statusses.append(("Media Buttons", keyboard.setup(bus))) # Init HTTP interface if config.has_option("httpinterface", "api_password"): diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py new file mode 100644 index 00000000000..9133801033c --- /dev/null +++ b/homeassistant/components/__init__.py @@ -0,0 +1,6 @@ +""" +homeassistant.components +~~~~~~~~~~~~~~~~~~~~~~~~ + +This package contains components that can be plugged into Home Assistant. +""" diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py new file mode 100644 index 00000000000..bc5741caa20 --- /dev/null +++ b/homeassistant/components/browser.py @@ -0,0 +1,22 @@ +""" +homeassistant.components.browser +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to launch a webbrowser on the host machine. +""" + +DOMAIN_BROWSER = "browser" + +SERVICE_BROWSE_URL = "browse_url" + + +def setup(bus): + """ Listen for browse_url events and open + the url in the default webbrowser. """ + + import webbrowser + + bus.register_service(DOMAIN_BROWSER, SERVICE_BROWSE_URL, + lambda service: webbrowser.open(service.data['url'])) + + return True diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py new file mode 100644 index 00000000000..e7459aa9d5a --- /dev/null +++ b/homeassistant/components/chromecast.py @@ -0,0 +1,103 @@ +""" +homeassistant.components.chromecast +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to interact with Chromecasts. +""" + +from homeassistant.packages import pychromecast + +import homeassistant as ha +import homeassistant.util as util + + +DOMAIN_CHROMECAST = "chromecast" + +SERVICE_YOUTUBE_VIDEO = "play_youtube_video" + +STATE_CATEGORY_FORMAT = 'chromecasts.{}' +STATE_NO_APP = "none" + +ATTR_FRIENDLY_NAME = "friendly_name" +ATTR_HOST = "host" +ATTR_STATE = "state" +ATTR_OPTIONS = "options" + + +def get_ids(statemachine): + """ Gets the IDs of the different Chromecasts that are being tracked. """ + return ha.get_grouped_state_cats(statemachine, STATE_CATEGORY_FORMAT, True) + + +def get_categories(statemachine): + """ Gets the categories of the different Chromecasts that are being + tracked. """ + return ha.get_grouped_state_cats(statemachine, STATE_CATEGORY_FORMAT, + False) + + +def turn_off(statemachine, cc_id=None): + """ Exits any running app on the specified ChromeCast and shows + idle screen. Will quit all ChromeCasts if nothing specified. """ + + cats = [STATE_CATEGORY_FORMAT.format(cc_id)] if cc_id \ + else get_categories(statemachine) + + for cat in cats: + state = statemachine.get_state(cat) + + if state and \ + state['state'] != STATE_NO_APP or \ + state['state'] != pychromecast.APP_ID_HOME: + + pychromecast.quit_app(state['attributes'][ATTR_HOST]) + + +def setup(bus, statemachine, host): + """ Listen for chromecast events. """ + device = pychromecast.get_device_status(host) + + if not device: + return False + + category = STATE_CATEGORY_FORMAT.format(util.slugify( + device.friendly_name)) + + bus.register_service(DOMAIN_CHROMECAST, ha.SERVICE_TURN_OFF, + lambda service: + turn_off(statemachine, + service.data.get("cc_id", None))) + + bus.register_service(DOMAIN_CHROMECAST, "start_fireplace", + lambda service: + pychromecast.play_youtube_video(host, "eyU3bRy2x44")) + + bus.register_service(DOMAIN_CHROMECAST, "start_epic_sax", + lambda service: + pychromecast.play_youtube_video(host, "kxopViU98Xo")) + + bus.register_service(DOMAIN_CHROMECAST, SERVICE_YOUTUBE_VIDEO, + lambda service: + pychromecast.play_youtube_video( + host, service.data['video'])) + + def update_chromecast_state(time): # pylint: disable=unused-argument + """ Retrieve state of Chromecast and update statemachine. """ + status = pychromecast.get_app_status(host) + + if status: + statemachine.set_state(category, status.name, + {ATTR_FRIENDLY_NAME: + pychromecast.get_friendly_name( + status.name), + ATTR_HOST: host, + ATTR_STATE: status.state, + ATTR_OPTIONS: status.options}) + else: + statemachine.set_state(category, STATE_NO_APP) + + ha.track_time_change(bus, update_chromecast_state) + + update_chromecast_state(None) + + return True diff --git a/homeassistant/observers.py b/homeassistant/components/device.py similarity index 57% rename from homeassistant/observers.py rename to homeassistant/components/device.py index 38529f3ef02..7c1dd1f3917 100644 --- a/homeassistant/observers.py +++ b/homeassistant/components/device.py @@ -1,19 +1,16 @@ """ -homeassistant.observers -~~~~~~~~~~~~~~~~~~~~~~~ - -This module provides observers that can change the state or fire -events based on observations. +homeassistant.components.sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides functionality to keep track of devices. """ - import logging -import csv -import os -from datetime import datetime, timedelta import threading +import os +import csv import re import json +from datetime import datetime, timedelta import requests @@ -21,34 +18,15 @@ import homeassistant as ha import homeassistant.util as util DOMAIN_DEVICE_TRACKER = "device_tracker" -DOMAIN_CHROMECAST = "chromecast" -DOMAIN_LIGHT_CONTROL = "light_control" SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" -SERVICE_CHROMECAST_YOUTUBE_VIDEO = "play_youtube_video" -SERVICE_TURN_LIGHT_ON = "turn_light_on" -SERVICE_TURN_LIGHT_OFF = "turn_light_off" - -STATE_CATEGORY_SUN = "weather.sun" -STATE_ATTRIBUTE_NEXT_SUN_RISING = "next_rising" -STATE_ATTRIBUTE_NEXT_SUN_SETTING = "next_setting" STATE_CATEGORY_ALL_DEVICES = 'devices' -STATE_CATEGORY_DEVICE_FORMAT = 'devices.{}' +STATE_CATEGORY_FORMAT = 'devices.{}' -STATE_CATEGORY_CHROMECAST_FORMAT = 'chromecasts.{}' +STATE_NOT_HOME = 'device_not_home' +STATE_HOME = 'device_home' -STATE_CATEGORY_ALL_LIGHTS = 'lights' -STATE_CATEGORY_LIGHT_FORMAT = "lights.{}" - -SUN_STATE_ABOVE_HORIZON = "above_horizon" -SUN_STATE_BELOW_HORIZON = "below_horizon" - -LIGHT_STATE_ON = "on" -LIGHT_STATE_OFF = "off" - -DEVICE_STATE_NOT_HOME = 'device_not_home' -DEVICE_STATE_HOME = 'device_home' # After how much time do we consider a device not home if # it does not show up on scans @@ -57,255 +35,28 @@ TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=1) # Return cached results if last scan was less then this time ago TOMATO_MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) -LIGHTS_MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - # Filename to save known devices to KNOWN_DEVICES_FILE = "known_devices.csv" -def _get_grouped_states(statemachine, category_format_string): - """ Get states that are part of a group of states. +def get_categories(statemachine): + """ Returns the categories of devices that are being tracked in the + statemachine. """ + return ha.get_grouped_state_cats(statemachine, STATE_CATEGORY_FORMAT, + False) - Example category_format_string can be devices.{} - If input states are devices, devices.paulus and devices.paulus.charging - then the output will be paulus. - """ - group_prefix = category_format_string.format("") - - id_part = slice(len(group_prefix), None) - - return [cat[id_part] for cat in statemachine.categories - if cat.startswith(group_prefix) and cat.count(".") == 1] - - -def is_sun_up(statemachine): - """ Returns if the sun is currently up based on the statemachine. """ - return statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_ABOVE_HORIZON) - - -def next_sun_setting(statemachine): - """ Returns the datetime object representing the next sun setting. """ - state = statemachine.get_state(STATE_CATEGORY_SUN) - - return None if not state else ha.str_to_datetime( - state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_SETTING]) - - -def next_sun_rising(statemachine): - """ Returns the datetime object representing the next sun setting. """ - state = statemachine.get_state(STATE_CATEGORY_SUN) - - return None if not state else ha.str_to_datetime( - state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_RISING]) - - -def track_sun(bus, statemachine, latitude, longitude): - """ Tracks the state of the sun. """ - logger = logging.getLogger(__name__) - - try: - import ephem - except ImportError: - logger.exception("TrackSun:Error while importing dependency ephem.") - return False - - sun = ephem.Sun() # pylint: disable=no-member - - def update_sun_state(now): # pylint: disable=unused-argument - """ Method to update the current state of the sun and - set time of next setting and rising. """ - observer = ephem.Observer() - observer.lat = latitude - observer.long = longitude - - next_rising = ephem.localtime(observer.next_rising(sun)) - next_setting = ephem.localtime(observer.next_setting(sun)) - - if next_rising > next_setting: - new_state = SUN_STATE_ABOVE_HORIZON - next_change = next_setting - - else: - new_state = SUN_STATE_BELOW_HORIZON - next_change = next_rising - - logger.info( - "Sun:{}. Next change: {}".format(new_state, - next_change.strftime("%H:%M"))) - - state_attributes = { - STATE_ATTRIBUTE_NEXT_SUN_RISING: ha.datetime_to_str(next_rising), - STATE_ATTRIBUTE_NEXT_SUN_SETTING: ha.datetime_to_str(next_setting) - } - - statemachine.set_state(STATE_CATEGORY_SUN, new_state, state_attributes) - - # +10 seconds to be sure that the change has occured - ha.track_time_change(bus, update_sun_state, - point_in_time=next_change + timedelta(seconds=10)) - - update_sun_state(None) - - return True - - -def get_chromecast_ids(statemachine): - """ Gets the IDs of the different Chromecasts that are being tracked. """ - return _get_grouped_states(statemachine, STATE_CATEGORY_CHROMECAST_FORMAT) - - -def setup_chromecast(bus, statemachine, host): - """ Listen for chromecast events. """ - from homeassistant.packages import pychromecast - - device = pychromecast.get_device_status(host) - - if not device: - return False - - category = STATE_CATEGORY_CHROMECAST_FORMAT.format(util.slugify( - device.friendly_name)) - - bus.register_service(DOMAIN_CHROMECAST, "start_fireplace", - lambda event: - pychromecast.play_youtube_video(host, "eyU3bRy2x44")) - - bus.register_service(DOMAIN_CHROMECAST, "start_epic_sax", - lambda event: - pychromecast.play_youtube_video(host, "kxopViU98Xo")) - - bus.register_service(DOMAIN_CHROMECAST, SERVICE_CHROMECAST_YOUTUBE_VIDEO, - lambda event: - pychromecast.play_youtube_video(host, - event.data['video'])) - - def update_chromecast_state(time): # pylint: disable=unused-argument - """ Retrieve state of Chromecast and update statemachine. """ - status = pychromecast.get_app_status(host) - - if status: - statemachine.set_state(category, status.name, - {"friendly_name": - pychromecast.get_friendly_name( - status.name), - "state": status.state, - "options": status.options}) - else: - statemachine.set_state(category, "none") - - ha.track_time_change(bus, update_chromecast_state) - - update_chromecast_state(None) - - return True - - -def is_light_on(statemachine, light_id=None): - """ Returns if the lights are on based on the statemachine. """ - category = STATE_CATEGORY_LIGHT_FORMAT.format(light_id) if light_id \ - else STATE_CATEGORY_ALL_LIGHTS - - return statemachine.is_state(category, LIGHT_STATE_ON) - - -def turn_light_on(bus, light_id=None, transition_seconds=None): - """ Turns all or specified light on. """ - data = {} - - if light_id: - data["light_id"] = light_id - - if transition_seconds: - data["transition_seconds"] = transition_seconds - - bus.call_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_ON, data) - - -def turn_light_off(bus, light_id=None, transition_seconds=None): - """ Turns all or specified light off. """ - data = {} - - if light_id: - data["light_id"] = light_id - - if transition_seconds: - data["transition_seconds"] = transition_seconds - - bus.call_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_OFF, data) - - -def get_light_ids(statemachine): - """ Get the light IDs that are being tracked in the statemachine. """ - return _get_grouped_states(statemachine, STATE_CATEGORY_LIGHT_FORMAT) - - -def setup_light_control(bus, statemachine, light_control): - """ Exposes light control via statemachine and services. """ - - def update_light_state(time): # pylint: disable=unused-argument - """ Track the state of the lights. """ - try: - should_update = datetime.now() - update_light_state.last_updated \ - > LIGHTS_MIN_TIME_BETWEEN_SCANS - - except AttributeError: # if last_updated does not exist - should_update = True - - if should_update: - update_light_state.last_updated = datetime.now() - - status = {light_id: light_control.is_light_on(light_id) - for light_id in light_control.light_ids} - - for light_id, state in status.items(): - state_category = STATE_CATEGORY_LIGHT_FORMAT.format(light_id) - - statemachine.set_state(state_category, - LIGHT_STATE_ON if state - else LIGHT_STATE_OFF) - - statemachine.set_state(STATE_CATEGORY_ALL_LIGHTS, - LIGHT_STATE_ON if True in status.values() - else LIGHT_STATE_OFF) - - ha.track_time_change(bus, update_light_state, second=[0, 30]) - - def handle_light_event(service): - """ Hande a turn light on or off service call. """ - light_id = service.data.get("light_id", None) - transition_seconds = service.data.get("transition_seconds", None) - - if service.service == SERVICE_TURN_LIGHT_ON: - light_control.turn_light_on(light_id, transition_seconds) - else: - light_control.turn_light_off(light_id, transition_seconds) - - update_light_state(None) - - # Listen for light on and light off events - bus.register_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_ON, - handle_light_event) - - bus.register_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_OFF, - handle_light_event) - - update_light_state(None) - - return True - - -def get_device_ids(statemachine): +def get_ids(statemachine): """ Returns the devices that are being tracked in the statemachine. """ - return _get_grouped_states(statemachine, STATE_CATEGORY_DEVICE_FORMAT) + return ha.get_grouped_state_cats(statemachine, STATE_CATEGORY_FORMAT, True) -def is_device_home(statemachine, device_id=None): +def is_home(statemachine, device_id=None): """ Returns if any or specified device is home. """ - category = STATE_CATEGORY_DEVICE_FORMAT.format(device_id) if device_id \ + category = STATE_CATEGORY_FORMAT.format(device_id) if device_id \ else STATE_CATEGORY_ALL_DEVICES - return statemachine.is_state(category, DEVICE_STATE_HOME) + return statemachine.is_state(category, STATE_HOME) class DeviceTracker(object): @@ -350,6 +101,8 @@ class DeviceTracker(object): """ Update device states based on the found devices. """ self.lock.acquire() + now = datetime.now() + temp_tracking_devices = [device for device in self.known_devices if self.known_devices[device]['track']] @@ -358,30 +111,30 @@ class DeviceTracker(object): if device in temp_tracking_devices: temp_tracking_devices.remove(device) - self.known_devices[device]['last_seen'] = datetime.now() + self.known_devices[device]['last_seen'] = now self.statemachine.set_state( - self.known_devices[device]['category'], DEVICE_STATE_HOME) + self.known_devices[device]['category'], STATE_HOME) # For all devices we did not find, set state to NH # But only if they have been gone for longer then the error time span # Because we do not want to have stuff happening when the device does # not show up for 1 scan beacuse of reboot etc for device in temp_tracking_devices: - if (datetime.now() - self.known_devices[device]['last_seen'] > + if (now - self.known_devices[device]['last_seen'] > TIME_SPAN_FOR_ERROR_IN_SCANNING): self.statemachine.set_state( self.known_devices[device]['category'], - DEVICE_STATE_NOT_HOME) + STATE_NOT_HOME) # Get the currently used statuses states_of_devices = [self.statemachine.get_state(category)['state'] for category in self.device_state_categories] # Update the all devices category - all_devices_state = (DEVICE_STATE_HOME if DEVICE_STATE_HOME - in states_of_devices else DEVICE_STATE_NOT_HOME) + all_devices_state = (STATE_HOME if STATE_HOME + in states_of_devices else STATE_NOT_HOME) self.statemachine.set_state(STATE_CATEGORY_ALL_DEVICES, all_devices_state) @@ -468,7 +221,7 @@ class DeviceTracker(object): if tries > 1: suffix = "_{}".format(tries) - category = STATE_CATEGORY_DEVICE_FORMAT.format( + category = STATE_CATEGORY_FORMAT.format( name + suffix) if category not in used_categories: diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py new file mode 100644 index 00000000000..1afc33f5110 --- /dev/null +++ b/homeassistant/components/device_sun_light_trigger.py @@ -0,0 +1,142 @@ +""" +homeassistant.components.device_sun_light_trigger +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to turn on lights based on +the state of the sun and devices. +""" +import logging +from datetime import datetime, timedelta + +import homeassistant as ha + +from . import light, sun, device, general + + +LIGHT_TRANSITION_TIME = timedelta(minutes=15) + + +# pylint: disable=too-many-branches +def setup(bus, statemachine): + """ Triggers to turn lights on or off based on device precense. """ + + logger = logging.getLogger(__name__) + + device_state_categories = device.get_categories(statemachine) + + if len(device_state_categories) == 0: + logger.error("LightTrigger:No devices given to track") + + return False + + light_ids = light.get_ids(statemachine) + + if len(light_ids) == 0: + logger.error("LightTrigger:No lights found to turn on") + + return False + + # Calculates the time when to start fading lights in when sun sets + time_for_light_before_sun_set = lambda: \ + (sun.next_setting(statemachine) - LIGHT_TRANSITION_TIME * + len(light_ids)) + + # pylint: disable=unused-argument + def handle_sun_rising(category, old_state, new_state): + """The moment sun sets we want to have all the lights on. + We will schedule to have each light start after one another + and slowly transition in.""" + + def turn_light_on_before_sunset(light_id): + """ Helper function to turn on lights slowly if there + are devices home and the light is not on yet. """ + if (device.is_home(statemachine) and + not light.is_on(statemachine, light_id)): + + light.turn_on(bus, light_id, LIGHT_TRANSITION_TIME.seconds) + + def turn_on(light_id): + """ Lambda can keep track of function parameters but not local + parameters. If we put the lambda directly in the below statement + only the last light will be turned on.. """ + return lambda now: turn_light_on_before_sunset(light_id) + + start_point = time_for_light_before_sun_set() + + for index, light_id in enumerate(light_ids): + ha.track_time_change(bus, turn_on(light_id), + point_in_time=(start_point + + index * LIGHT_TRANSITION_TIME)) + + # Track every time sun rises so we can schedule a time-based + # pre-sun set event + ha.track_state_change(bus, sun.STATE_CATEGORY, sun.STATE_BELOW_HORIZON, + sun.STATE_ABOVE_HORIZON, handle_sun_rising) + + # If the sun is already above horizon + # schedule the time-based pre-sun set event + if sun.is_up(statemachine): + handle_sun_rising(None, None, None) + + def handle_device_state_change(category, old_state, new_state): + """ Function to handle tracked device state changes. """ + lights_are_on = light.is_on(statemachine) + + light_needed = not (lights_are_on or sun.is_up(statemachine)) + + # Specific device came home ? + if (category != device.STATE_CATEGORY_ALL_DEVICES and + new_state['state'] == device.STATE_HOME): + + # These variables are needed for the elif check + now = datetime.now() + start_point = time_for_light_before_sun_set() + + # Do we need lights? + if light_needed: + + logger.info( + "Home coming event for {}. Turning lights on". + format(category)) + + light.turn_on(bus) + + # Are we in the time span were we would turn on the lights + # if someone would be home? + # Check this by seeing if current time is later then the point + # in time when we would start putting the lights on. + elif start_point < now < sun.next_setting(statemachine): + + # Check for every light if it would be on if someone was home + # when the fading in started and turn it on if so + for index, light_id in enumerate(light_ids): + + if now > start_point + index * LIGHT_TRANSITION_TIME: + light.turn_on(bus, light_id) + + else: + # If this light didn't happen to be turned on yet so + # will all the following then, break. + break + + # Did all devices leave the house? + elif (category == device.STATE_CATEGORY_ALL_DEVICES and + new_state['state'] == device.STATE_NOT_HOME and lights_are_on): + + logger.info( + "Everyone has left but there are devices on. Turning them off") + + shutdown_devices(bus, statemachine) + + # Track home coming of each seperate device + for category in device_state_categories: + ha.track_state_change(bus, category, + device.STATE_NOT_HOME, device.STATE_HOME, + handle_device_state_change) + + # Track when all devices are gone to shut down lights + ha.track_state_change(bus, device.STATE_CATEGORY_ALL_DEVICES, + device.STATE_HOME, device.STATE_NOT_HOME, + handle_device_state_change) + + return True diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py new file mode 100644 index 00000000000..2aec066408c --- /dev/null +++ b/homeassistant/components/downloader.py @@ -0,0 +1,86 @@ +""" +homeassistant.components.downloader +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to download files. +""" +import os +import logging +import re + +import requests + +import homeassistant.util as util + +DOMAIN_DOWNLOADER = "downloader" + +SERVICE_DOWNLOAD_FILE = "download_file" + + +def setup(bus, download_path): + """ Listens for download events to download files. """ + + logger = logging.getLogger(__name__) + + if not os.path.isdir(download_path): + + logger.error( + ("FileDownloader:" + "Download path {} does not exist. File Downloader not active."). + format(download_path)) + + return False + + def download_file(service): + """ Downloads file specified in the url. """ + + try: + req = requests.get(service.data['url'], stream=True) + if req.status_code == 200: + filename = None + + if 'content-disposition' in req.headers: + match = re.findall(r"filename=(\S+)", + req.headers['content-disposition']) + + if len(match) > 0: + filename = match[0].strip("'\" ") + + if not filename: + filename = os.path.basename(service.data['url']).strip() + + if not filename: + filename = "ha_download" + + # Remove stuff to ruin paths + filename = util.sanitize_filename(filename) + + path, ext = os.path.splitext(os.path.join(download_path, + filename)) + + # If file exist append a number. We test filename, filename_2.. + tries = 0 + while True: + tries += 1 + + name_suffix = "" if tries == 1 else "_{}".format(tries) + final_path = path + name_suffix + ext + + if not os.path.isfile(final_path): + break + + logger.info("FileDownloader:{} -> {}".format( + service.data['url'], final_path)) + + with open(final_path, 'wb') as fil: + for chunk in req.iter_content(1024): + fil.write(chunk) + + except requests.exceptions.ConnectionError: + logger.exception("FileDownloader:ConnectionError occured for {}". + format(service.data['url'])) + + bus.register_service(DOMAIN_DOWNLOADER, SERVICE_DOWNLOAD_FILE, + download_file) + + return True diff --git a/homeassistant/components/general.py b/homeassistant/components/general.py new file mode 100644 index 00000000000..711f3459f7c --- /dev/null +++ b/homeassistant/components/general.py @@ -0,0 +1,26 @@ +""" +homeassistant.components.general +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This component contains a service to shut down all devices. +""" + +import homeassistant as ha +from . import chromecast, light + +SERVICE_SHUTDOWN_DEVICES = "shutdown_devices" + + +def shutdown_devices(bus, statemachine): + """ Tries to shutdown all devices that are currently on. """ + chromecast.turn_off(statemachine) + light.turn_off(bus) + + +def setup(bus, statemachine): + """ Setup services related to homeassistant. """ + + bus.register_service(ha.DOMAIN_HOMEASSISTANT, SERVICE_SHUTDOWN_DEVICES, + lambda service: shutdown_devices(bus, statemachine)) + + return True diff --git a/homeassistant/httpinterface.py b/homeassistant/components/httpinterface/__init__.py similarity index 99% rename from homeassistant/httpinterface.py rename to homeassistant/components/httpinterface/__init__.py index 28a0110140a..74f624bc33c 100644 --- a/homeassistant/httpinterface.py +++ b/homeassistant/components/httpinterface/__init__.py @@ -1,5 +1,5 @@ """ -homeassistant.httpinterface +homeassistant.components.httpinterface ~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module provides an API and a HTTP interface for debug purposes. diff --git a/homeassistant/www_static/favicon.ico b/homeassistant/components/httpinterface/www_static/favicon.ico similarity index 100% rename from homeassistant/www_static/favicon.ico rename to homeassistant/components/httpinterface/www_static/favicon.ico diff --git a/homeassistant/www_static/style.css b/homeassistant/components/httpinterface/www_static/style.css similarity index 100% rename from homeassistant/www_static/style.css rename to homeassistant/components/httpinterface/www_static/style.css diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py new file mode 100644 index 00000000000..374d88b804c --- /dev/null +++ b/homeassistant/components/keyboard.py @@ -0,0 +1,56 @@ +""" +homeassistant.components.keyboard +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to emulate keyboard presses on host machine. +""" +import logging + +DOMAIN_KEYBOARD = "keyboard" + +SERVICE_KEYBOARD_VOLUME_UP = "volume_up" +SERVICE_KEYBOARD_VOLUME_DOWN = "volume_down" +SERVICE_KEYBOARD_VOLUME_MUTE = "volume_mute" +SERVICE_KEYBOARD_MEDIA_PLAY_PAUSE = "media_play_pause" +SERVICE_KEYBOARD_MEDIA_NEXT_TRACK = "media_next_track" +SERVICE_KEYBOARD_MEDIA_PREV_TRACK = "media_prev_track" + + +def setup(bus): + """ Listen for keyboard events. """ + try: + import pykeyboard + except ImportError: + logging.getLogger(__name__).exception( + "MediaButtons: Error while importing dependency PyUserInput.") + + return False + + keyboard = pykeyboard.PyKeyboard() + keyboard.special_key_assignment() + + bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_UP, + lambda service: + keyboard.tap_key(keyboard.volume_up_key)) + + bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_DOWN, + lambda service: + keyboard.tap_key(keyboard.volume_down_key)) + + bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_MUTE, + lambda service: + keyboard.tap_key(keyboard.volume_mute_key)) + + bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_PLAY_PAUSE, + lambda service: + keyboard.tap_key(keyboard.media_play_pause_key)) + + bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_NEXT_TRACK, + lambda service: + keyboard.tap_key(keyboard.media_next_track_key)) + + bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_PREV_TRACK, + lambda service: + keyboard.tap_key(keyboard.media_prev_track_key)) + + return True diff --git a/homeassistant/components/light.py b/homeassistant/components/light.py new file mode 100644 index 00000000000..11008090ff3 --- /dev/null +++ b/homeassistant/components/light.py @@ -0,0 +1,181 @@ +""" +homeassistant.components.sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to interact with lights. +""" + +import logging +from datetime import datetime, timedelta + +import homeassistant as ha +import homeassistant.util as util + +DOMAIN = "light" + +STATE_CATEGORY_ALL_LIGHTS = 'lights' +STATE_CATEGORY_FORMAT = "lights.{}" + +STATE_ON = "on" +STATE_OFF = "off" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + + +def is_on(statemachine, light_id=None): + """ Returns if the lights are on based on the statemachine. """ + category = STATE_CATEGORY_FORMAT.format(light_id) if light_id \ + else STATE_CATEGORY_ALL_LIGHTS + + return statemachine.is_state(category, STATE_ON) + + +def turn_on(bus, light_id=None, transition_seconds=None): + """ Turns all or specified light on. """ + data = {} + + if light_id: + data["light_id"] = light_id + + if transition_seconds: + data["transition_seconds"] = transition_seconds + + bus.call_service(DOMAIN, ha.SERVICE_TURN_ON, data) + + +def turn_off(bus, light_id=None, transition_seconds=None): + """ Turns all or specified light off. """ + data = {} + + if light_id: + data["light_id"] = light_id + + if transition_seconds: + data["transition_seconds"] = transition_seconds + + bus.call_service(DOMAIN, ha.SERVICE_TURN_OFF, data) + + +def get_ids(statemachine): + """ Get the light IDs that are being tracked in the statemachine. """ + return ha.get_grouped_state_cats(statemachine, STATE_CATEGORY_FORMAT, True) + + +def setup(bus, statemachine, light_control): + """ Exposes light control via statemachine and services. """ + + def update_light_state(time): # pylint: disable=unused-argument + """ Track the state of the lights. """ + try: + should_update = datetime.now() - update_light_state.last_updated \ + > MIN_TIME_BETWEEN_SCANS + + except AttributeError: # if last_updated does not exist + should_update = True + + if should_update: + update_light_state.last_updated = datetime.now() + + status = {light_id: light_control.is_light_on(light_id) + for light_id in light_control.light_ids} + + for light_id, state in status.items(): + state_category = STATE_CATEGORY_FORMAT.format(light_id) + + statemachine.set_state(state_category, + STATE_ON if state + else STATE_OFF) + + statemachine.set_state(STATE_CATEGORY_ALL_LIGHTS, + STATE_ON if True in status.values() + else STATE_OFF) + + ha.track_time_change(bus, update_light_state, second=[0, 30]) + + def handle_light_event(service): + """ Hande a turn light on or off service call. """ + light_id = service.data.get("light_id", None) + transition_seconds = service.data.get("transition_seconds", None) + + if service.service == ha.SERVICE_TURN_ON: + light_control.turn_light_on(light_id, transition_seconds) + else: + light_control.turn_light_off(light_id, transition_seconds) + + update_light_state(None) + + # Listen for light on and light off events + bus.register_service(DOMAIN, ha.SERVICE_TURN_ON, + handle_light_event) + + bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF, + handle_light_event) + + update_light_state(None) + + return True + + +class HueLightControl(object): + """ Class to interface with the Hue light system. """ + + def __init__(self, host=None): + try: + import phue + except ImportError: + logging.getLogger(__name__).exception( + "HueLightControl: Error while importing dependency phue.") + + self.success_init = False + + return + + self._bridge = phue.Bridge(host) + + self._light_map = {util.slugify(light.name): light for light + in self._bridge.get_light_objects()} + + self.success_init = True + + @property + def light_ids(self): + """ Return a list of light ids. """ + return self._light_map.keys() + + def is_light_on(self, light_id=None): + """ Returns if specified or all light are on. """ + if not light_id: + return sum( + [1 for light in self._light_map.values() if light.on]) > 0 + + else: + return self._bridge.get_light(self._convert_id(light_id), 'on') + + def turn_light_on(self, light_id=None, transition_seconds=None): + """ Turn the specified or all lights on. """ + self._turn_light(True, light_id, transition_seconds) + + def turn_light_off(self, light_id=None, transition_seconds=None): + """ Turn the specified or all lights off. """ + self._turn_light(False, light_id, transition_seconds) + + def _turn_light(self, turn, light_id=None, transition_seconds=None): + """ Helper method to turn lights on or off. """ + if light_id: + light_id = self._convert_id(light_id) + else: + light_id = [light.light_id for light in self._light_map.values()] + + command = {'on': True, 'xy': [0.5119, 0.4147], 'bri': 164} if turn \ + else {'on': False} + + if transition_seconds: + # Transition time is in 1/10th seconds and cannot exceed + # MAX_TRANSITION_TIME which is 900 seconds for Hue. + command['transitiontime'] = min(9000, transition_seconds * 10) + + self._bridge.set_light(light_id, command) + + def _convert_id(self, light_id): + """ Returns internal light id to be used with phue. """ + return self._light_map[light_id].light_id diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py new file mode 100644 index 00000000000..728dc632644 --- /dev/null +++ b/homeassistant/components/sun.py @@ -0,0 +1,89 @@ +""" +homeassistant.components.sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to keep track of the sun. +""" +import logging +from datetime import timedelta + +import homeassistant as ha + +STATE_CATEGORY = "weather.sun" + +STATE_ABOVE_HORIZON = "above_horizon" +STATE_BELOW_HORIZON = "below_horizon" + +STATE_ATTR_NEXT_RISING = "next_rising" +STATE_ATTR_NEXT_SETTING = "next_setting" + + +def is_up(statemachine): + """ Returns if the sun is currently up based on the statemachine. """ + return statemachine.is_state(STATE_CATEGORY, STATE_ABOVE_HORIZON) + + +def next_setting(statemachine): + """ Returns the datetime object representing the next sun setting. """ + state = statemachine.get_state(STATE_CATEGORY) + + return None if not state else ha.str_to_datetime( + state['attributes'][STATE_ATTR_NEXT_SETTING]) + + +def next_rising(statemachine): + """ Returns the datetime object representing the next sun setting. """ + state = statemachine.get_state(STATE_CATEGORY) + + return None if not state else ha.str_to_datetime( + state['attributes'][STATE_ATTR_NEXT_RISING]) + + +def setup(bus, statemachine, latitude, longitude): + """ Tracks the state of the sun. """ + logger = logging.getLogger(__name__) + + try: + import ephem + except ImportError: + logger.exception("TrackSun:Error while importing dependency ephem.") + return False + + sun = ephem.Sun() # pylint: disable=no-member + + def update_sun_state(now): # pylint: disable=unused-argument + """ Method to update the current state of the sun and + set time of next setting and rising. """ + observer = ephem.Observer() + observer.lat = latitude + observer.long = longitude + + next_rising_dt = ephem.localtime(observer.next_rising(sun)) + next_setting_dt = ephem.localtime(observer.next_setting(sun)) + + if next_rising_dt > next_setting_dt: + new_state = STATE_ABOVE_HORIZON + next_change = next_setting_dt + + else: + new_state = STATE_BELOW_HORIZON + next_change = next_rising_dt + + logger.info( + "Sun:{}. Next change: {}".format(new_state, + next_change.strftime("%H:%M"))) + + state_attributes = { + STATE_ATTR_NEXT_RISING: ha.datetime_to_str(next_rising_dt), + STATE_ATTR_NEXT_SETTING: ha.datetime_to_str(next_setting_dt) + } + + statemachine.set_state(STATE_CATEGORY, new_state, state_attributes) + + # +10 seconds to be sure that the change has occured + ha.track_time_change(bus, update_sun_state, + point_in_time=next_change + timedelta(seconds=10)) + + update_sun_state(None) + + return True diff --git a/homeassistant/remote.py b/homeassistant/remote.py index b905509fe9a..a49dbf97785 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -17,7 +17,7 @@ import urlparse import requests import homeassistant as ha -import homeassistant.httpinterface as hah +import homeassistant.components.httpinterface as hah METHOD_GET = "get" METHOD_POST = "post" diff --git a/homeassistant/test.py b/homeassistant/test.py index ec6030e551f..bfb85bf81c8 100644 --- a/homeassistant/test.py +++ b/homeassistant/test.py @@ -13,7 +13,7 @@ import requests import homeassistant as ha import homeassistant.remote as remote -import homeassistant.httpinterface as hah +import homeassistant.components.httpinterface as hah API_PASSWORD = "test1234"