Reorg: Merged observers, actors and HTTPInterface into components

This commit is contained in:
Paulus Schoutsen 2013-12-11 00:07:30 -08:00
parent c5f5dcbce2
commit dd271febae
19 changed files with 795 additions and 659 deletions

View file

@ -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

View file

@ -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 {}

View file

@ -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

View file

@ -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"):

View file

@ -0,0 +1,6 @@
"""
homeassistant.components
~~~~~~~~~~~~~~~~~~~~~~~~
This package contains components that can be plugged into Home Assistant.
"""

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,5 +1,5 @@
"""
homeassistant.httpinterface
homeassistant.components.httpinterface
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides an API and a HTTP interface for debug purposes.

View file

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 318 B

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"