Reorg: Merged observers, actors and HTTPInterface into components
This commit is contained in:
parent
c5f5dcbce2
commit
dd271febae
19 changed files with 795 additions and 659 deletions
|
@ -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
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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
|
|
@ -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"):
|
||||
|
|
6
homeassistant/components/__init__.py
Normal file
6
homeassistant/components/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
homeassistant.components
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This package contains components that can be plugged into Home Assistant.
|
||||
"""
|
22
homeassistant/components/browser.py
Normal file
22
homeassistant/components/browser.py
Normal 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
|
103
homeassistant/components/chromecast.py
Normal file
103
homeassistant/components/chromecast.py
Normal 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
|
|
@ -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:
|
142
homeassistant/components/device_sun_light_trigger.py
Normal file
142
homeassistant/components/device_sun_light_trigger.py
Normal 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
|
86
homeassistant/components/downloader.py
Normal file
86
homeassistant/components/downloader.py
Normal 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
|
26
homeassistant/components/general.py
Normal file
26
homeassistant/components/general.py
Normal 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
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
homeassistant.httpinterface
|
||||
homeassistant.components.httpinterface
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides an API and a HTTP interface for debug purposes.
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 318 B |
56
homeassistant/components/keyboard.py
Normal file
56
homeassistant/components/keyboard.py
Normal 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
|
181
homeassistant/components/light.py
Normal file
181
homeassistant/components/light.py
Normal 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
|
89
homeassistant/components/sun.py
Normal file
89
homeassistant/components/sun.py
Normal 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
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue