Added state groups and migrated code base to use them.

This commit is contained in:
Paulus Schoutsen 2014-01-04 17:55:05 -08:00
parent 367433acb2
commit 48026c28c1
12 changed files with 296 additions and 160 deletions

View file

@ -5,16 +5,16 @@ longitude=-117.22743
[httpinterface] [httpinterface]
api_password=mypass api_password=mypass
[hue] [light.hue]
host=192.168.1.2 host=192.168.1.2
[tomato] [device_tracker.tomato]
host=192.168.1.1 host=192.168.1.1
username=admin username=admin
password=PASSWORD password=PASSWORD
http_id=aaaaaaaaaaaaaaa http_id=aaaaaaaaaaaaaaa
[netgear] [device_tracker.netgear]
host=192.168.1.1 host=192.168.1.1
username=admin username=admin
password=PASSWORD password=PASSWORD
@ -24,3 +24,13 @@ host=192.168.1.3
[downloader] [downloader]
download_dir=downloads download_dir=downloads
[device_sun_light_trigger]
# Example how you can specify a specific group that has to be turned on
# light_group=living_room
# A comma seperated list of states that have to be tracked
# As a single group
[groups]
living_room=light.Bowl,light.Ceiling,light.TV_back_light
bedroom=light.Bed_light

View file

@ -16,7 +16,12 @@ logging.basicConfig(level=logging.INFO)
ALL_EVENTS = '*' ALL_EVENTS = '*'
DOMAIN_HOMEASSISTANT = "homeassistant" DOMAIN = "homeassistant"
STATE_ON = "on"
STATE_OFF = "off"
STATE_NOT_HOME = 'device_not_home'
STATE_HOME = 'device_home'
SERVICE_TURN_ON = "turn_on" SERVICE_TURN_ON = "turn_on"
SERVICE_TURN_OFF = "turn_off" SERVICE_TURN_OFF = "turn_off"
@ -40,7 +45,7 @@ def start_home_assistant(bus):
""" Start home assistant. """ """ Start home assistant. """
request_shutdown = threading.Event() request_shutdown = threading.Event()
bus.register_service(DOMAIN_HOMEASSISTANT, SERVICE_HOMEASSISTANT_STOP, bus.register_service(DOMAIN, SERVICE_HOMEASSISTANT_STOP,
lambda service: request_shutdown.set()) lambda service: request_shutdown.set())
Timer(bus) Timer(bus)
@ -88,25 +93,19 @@ def _matcher(subject, pattern):
return '*' in pattern or subject in pattern return '*' in pattern or subject in pattern
def get_grouped_state_cats(statemachine, cat_format_string, strip_prefix): def split_state_category(category):
""" Get states that are part of a group of states. """ Splits a state category into domain, object_id. """
return category.split(".", 1)
Example category_format_string can be "devices.{}"
If input states are devices, devices.paulus and devices.paulus.charging def filter_categories(categories, domain_filter=None, object_id_only=False):
then the output will be paulus if strip_prefix is True, else devices.paulus """ Filter a list of categories based on domain. Setting object_id_only
""" will only return the object_ids. """
group_prefix = cat_format_string.format("") return [
split_state_category(cat)[1] if object_id_only else cat
if strip_prefix: for cat in categories if
id_part = slice(len(group_prefix), None) not domain_filter or cat.startswith(domain_filter)
]
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): def create_state(state, attributes=None, last_changed=None):
@ -119,10 +118,10 @@ def create_state(state, attributes=None, last_changed=None):
'last_changed': datetime_to_str(last_changed)} 'last_changed': datetime_to_str(last_changed)}
def track_state_change(bus, category, from_state, to_state, action): def track_state_change(bus, category, action, from_state=None, to_state=None):
""" Helper method to track specific state changes. """ """ Helper method to track specific state changes. """
from_state = _ensure_list(from_state) from_state = _ensure_list(from_state) if from_state else [ALL_EVENTS]
to_state = _ensure_list(to_state) to_state = _ensure_list(to_state) if to_state else [ALL_EVENTS]
def listener(event): def listener(event):
""" State change listener that listens for specific state changes. """ """ State change listener that listens for specific state changes. """

View file

@ -7,9 +7,9 @@ import logging
import homeassistant as ha import homeassistant as ha
from homeassistant.components import (general, chromecast, from homeassistant.components import (general, chromecast,
device_sun_light_trigger, device, device_sun_light_trigger, device_tracker,
downloader, keyboard, light, sun, downloader, keyboard, light, sun,
browser, httpinterface) browser, httpinterface, group)
# pylint: disable=too-many-branches,too-many-locals,too-many-statements # pylint: disable=too-many-branches,too-many-locals,too-many-statements
@ -34,6 +34,13 @@ def from_config_file(config_path):
has_section = config.has_section has_section = config.has_section
add_status = lambda name, result: statusses.append((name, result)) add_status = lambda name, result: statusses.append((name, result))
def get_opt_safe(section, option, default=None):
""" Failure proof option retriever. """
try:
return config.get(section, option)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return default
# Device scanner # Device scanner
dev_scan = None dev_scan = None
@ -41,23 +48,23 @@ def from_config_file(config_path):
# For the error message if not all option fields exist # For the error message if not all option fields exist
opt_fields = "host, username, password" opt_fields = "host, username, password"
if has_section('tomato'): if has_section('device_tracker.tomato'):
dev_scan_name = "Tomato" dev_scan_name = "Tomato"
opt_fields += ", http_id" opt_fields += ", http_id"
dev_scan = device.TomatoDeviceScanner( dev_scan = device_tracker.TomatoDeviceScanner(
get_opt('tomato', 'host'), get_opt('device_tracker.tomato', 'host'),
get_opt('tomato', 'username'), get_opt('device_tracker.tomato', 'username'),
get_opt('tomato', 'password'), get_opt('device_tracker.tomato', 'password'),
get_opt('tomato', 'http_id')) get_opt('device_tracker.tomato', 'http_id'))
elif has_section('netgear'): elif has_section('device_tracker.netgear'):
dev_scan_name = "Netgear" dev_scan_name = "Netgear"
dev_scan = device.NetgearDeviceScanner( dev_scan = device_tracker.NetgearDeviceScanner(
get_opt('netgear', 'host'), get_opt('device_tracker.netgear', 'host'),
get_opt('netgear', 'username'), get_opt('device_tracker.netgear', 'username'),
get_opt('netgear', 'password')) get_opt('device_tracker.netgear', 'password'))
except ConfigParser.NoOptionError: except ConfigParser.NoOptionError:
# If one of the options didn't exist # If one of the options didn't exist
@ -76,7 +83,7 @@ def from_config_file(config_path):
# Device Tracker # Device Tracker
if dev_scan: if dev_scan:
device.DeviceTracker(bus, statemachine, dev_scan) device_tracker.DeviceTracker(bus, statemachine, dev_scan)
add_status("Device Tracker", True) add_status("Device Tracker", True)
@ -100,11 +107,8 @@ def from_config_file(config_path):
chromecast_started = False chromecast_started = False
# Light control # Light control
if has_section("hue"): if has_section("light.hue"):
if has_opt("hue", "host"): light_control = light.HueLightControl(get_opt_safe("hue", "host"))
light_control = light.HueLightControl(get_opt("hue", "host"))
else:
light_control = light.HueLightControl()
add_status("Light Control - Hue", light_control.success_init) add_status("Light Control - Hue", light_control.success_init)
@ -112,11 +116,6 @@ def from_config_file(config_path):
else: else:
light_control = None light_control = None
# Light trigger
if light_control:
add_status("Light Trigger",
device_sun_light_trigger.setup(bus, statemachine))
if has_opt("downloader", "download_dir"): if has_opt("downloader", "download_dir"):
add_status("Downloader", downloader.setup( add_status("Downloader", downloader.setup(
bus, get_opt("downloader", "download_dir"))) bus, get_opt("downloader", "download_dir")))
@ -137,6 +136,21 @@ def from_config_file(config_path):
add_status("HTTPInterface", True) add_status("HTTPInterface", True)
# Init groups
if has_section("groups"):
for name, categories in config.items("groups"):
add_status("Group - {}".format(name),
group.setup(bus, statemachine, name,
categories.split(",")))
# Light trigger
if light_control:
light_group = get_opt_safe("device_sun_light_trigger", "light_group")
add_status("Light Trigger",
device_sun_light_trigger.setup(bus, statemachine,
light_group))
for component, success_init in statusses: for component, success_init in statusses:
status = "initialized" if success_init else "Failed to initialize" status = "initialized" if success_init else "Failed to initialize"

View file

@ -5,7 +5,7 @@ homeassistant.components.browser
Provides functionality to launch a webbrowser on the host machine. Provides functionality to launch a webbrowser on the host machine.
""" """
DOMAIN_BROWSER = "browser" DOMAIN = "browser"
SERVICE_BROWSE_URL = "browse_url" SERVICE_BROWSE_URL = "browse_url"
@ -16,7 +16,7 @@ def setup(bus):
import webbrowser import webbrowser
bus.register_service(DOMAIN_BROWSER, SERVICE_BROWSE_URL, bus.register_service(DOMAIN, SERVICE_BROWSE_URL,
lambda service: webbrowser.open(service.data['url'])) lambda service: webbrowser.open(service.data['url']))
return True return True

View file

@ -4,6 +4,7 @@ homeassistant.components.chromecast
Provides functionality to interact with Chromecasts. Provides functionality to interact with Chromecasts.
""" """
import logging
from homeassistant.external import pychromecast from homeassistant.external import pychromecast
@ -11,11 +12,11 @@ import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
DOMAIN_CHROMECAST = "chromecast" DOMAIN = "chromecast"
SERVICE_YOUTUBE_VIDEO = "play_youtube_video" SERVICE_YOUTUBE_VIDEO = "play_youtube_video"
STATE_CATEGORY_FORMAT = 'chromecasts.{}' STATE_CATEGORY_FORMAT = DOMAIN + '.{}'
STATE_NO_APP = "none" STATE_NO_APP = "none"
ATTR_FRIENDLY_NAME = "friendly_name" ATTR_FRIENDLY_NAME = "friendly_name"
@ -24,24 +25,12 @@ ATTR_STATE = "state"
ATTR_OPTIONS = "options" 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): def turn_off(statemachine, cc_id=None):
""" Exits any running app on the specified ChromeCast and shows """ Exits any running app on the specified ChromeCast and shows
idle screen. Will quit all ChromeCasts if nothing specified. """ idle screen. Will quit all ChromeCasts if nothing specified. """
cats = [STATE_CATEGORY_FORMAT.format(cc_id)] if cc_id \ cats = [STATE_CATEGORY_FORMAT.format(cc_id)] if cc_id \
else get_categories(statemachine) else ha.filter_categories(statemachine.categories, DOMAIN)
for cat in cats: for cat in cats:
state = statemachine.get_state(cat) state = statemachine.get_state(cat)
@ -55,34 +44,39 @@ def turn_off(statemachine, cc_id=None):
def setup(bus, statemachine, host): def setup(bus, statemachine, host):
""" Listen for chromecast events. """ """ Listen for chromecast events. """
logger = logging.getLogger(__name__)
logger.info("Getting device status")
device = pychromecast.get_device_status(host) device = pychromecast.get_device_status(host)
if not device: if not device:
logger.error("Could not find Chromecast")
return False return False
category = STATE_CATEGORY_FORMAT.format(util.slugify( category = STATE_CATEGORY_FORMAT.format(util.slugify(
device.friendly_name)) device.friendly_name))
bus.register_service(DOMAIN_CHROMECAST, ha.SERVICE_TURN_OFF, bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF,
lambda service: lambda service:
turn_off(statemachine, turn_off(statemachine,
service.data.get("cc_id", None))) service.data.get("cc_id", None)))
bus.register_service(DOMAIN_CHROMECAST, "start_fireplace", bus.register_service(DOMAIN, "start_fireplace",
lambda service: lambda service:
pychromecast.play_youtube_video(host, "eyU3bRy2x44")) pychromecast.play_youtube_video(host, "eyU3bRy2x44"))
bus.register_service(DOMAIN_CHROMECAST, "start_epic_sax", bus.register_service(DOMAIN, "start_epic_sax",
lambda service: lambda service:
pychromecast.play_youtube_video(host, "kxopViU98Xo")) pychromecast.play_youtube_video(host, "kxopViU98Xo"))
bus.register_service(DOMAIN_CHROMECAST, SERVICE_YOUTUBE_VIDEO, bus.register_service(DOMAIN, SERVICE_YOUTUBE_VIDEO,
lambda service: lambda service:
pychromecast.play_youtube_video( pychromecast.play_youtube_video(
host, service.data['video'])) host, service.data['video']))
def update_chromecast_state(time): # pylint: disable=unused-argument def update_chromecast_state(time): # pylint: disable=unused-argument
""" Retrieve state of Chromecast and update statemachine. """ """ Retrieve state of Chromecast and update statemachine. """
logger.info("Updating app status")
status = pychromecast.get_app_status(host) status = pychromecast.get_app_status(host)
if status: if status:

View file

@ -10,28 +10,34 @@ from datetime import datetime, timedelta
import homeassistant as ha import homeassistant as ha
from . import light, sun, device, general from . import light, sun, device_tracker, general, group
LIGHT_TRANSITION_TIME = timedelta(minutes=15) LIGHT_TRANSITION_TIME = timedelta(minutes=15)
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(bus, statemachine): def setup(bus, statemachine, light_group=None):
""" Triggers to turn lights on or off based on device precense. """ """ Triggers to turn lights on or off based on device precense. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
device_state_categories = device.get_categories(statemachine) device_state_categories = ha.filter_categories(statemachine.categories,
device_tracker.DOMAIN)
if len(device_state_categories) == 0: if not device_state_categories:
logger.error("LightTrigger:No devices given to track") logger.error("LightTrigger:No devices found to track")
return False return False
light_ids = light.get_ids(statemachine) if not light_group:
light_group = light.STATE_GROUP_NAME_ALL_LIGHTS
if len(light_ids) == 0: # Get the light IDs from the specified group
light_ids = ha.filter_categories(
group.get_categories(statemachine, light_group), light.DOMAIN, True)
if not light_ids:
logger.error("LightTrigger:No lights found to turn on ") logger.error("LightTrigger:No lights found to turn on ")
return False return False
@ -50,7 +56,7 @@ def setup(bus, statemachine):
def turn_light_on_before_sunset(light_id): def turn_light_on_before_sunset(light_id):
""" Helper function to turn on lights slowly if there """ Helper function to turn on lights slowly if there
are devices home and the light is not on yet. """ are devices home and the light is not on yet. """
if (device.is_home(statemachine) and if (device_tracker.is_home(statemachine) and
not light.is_on(statemachine, light_id)): not light.is_on(statemachine, light_id)):
light.turn_on(bus, light_id, LIGHT_TRANSITION_TIME.seconds) light.turn_on(bus, light_id, LIGHT_TRANSITION_TIME.seconds)
@ -70,8 +76,8 @@ def setup(bus, statemachine):
# Track every time sun rises so we can schedule a time-based # Track every time sun rises so we can schedule a time-based
# pre-sun set event # pre-sun set event
ha.track_state_change(bus, sun.STATE_CATEGORY, sun.STATE_BELOW_HORIZON, ha.track_state_change(bus, sun.STATE_CATEGORY, handle_sun_rising,
sun.STATE_ABOVE_HORIZON, handle_sun_rising) sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
# If the sun is already above horizon # If the sun is already above horizon
# schedule the time-based pre-sun set event # schedule the time-based pre-sun set event
@ -85,8 +91,8 @@ def setup(bus, statemachine):
light_needed = not (lights_are_on or sun.is_up(statemachine)) light_needed = not (lights_are_on or sun.is_up(statemachine))
# Specific device came home ? # Specific device came home ?
if (category != device.STATE_CATEGORY_ALL_DEVICES and if (category != device_tracker.STATE_CATEGORY_ALL_DEVICES and
new_state['state'] == device.STATE_HOME): new_state['state'] == ha.STATE_HOME):
# These variables are needed for the elif check # These variables are needed for the elif check
now = datetime.now() now = datetime.now()
@ -120,8 +126,8 @@ def setup(bus, statemachine):
break break
# Did all devices leave the house? # Did all devices leave the house?
elif (category == device.STATE_CATEGORY_ALL_DEVICES and elif (category == device_tracker.STATE_CATEGORY_ALL_DEVICES and
new_state['state'] == device.STATE_NOT_HOME and lights_are_on): new_state['state'] == ha.STATE_NOT_HOME and lights_are_on):
logger.info( logger.info(
"Everyone has left but there are devices on. Turning them off") "Everyone has left but there are devices on. Turning them off")
@ -130,13 +136,12 @@ def setup(bus, statemachine):
# Track home coming of each seperate device # Track home coming of each seperate device
for category in device_state_categories: for category in device_state_categories:
ha.track_state_change(bus, category, ha.track_state_change(bus, category, handle_device_state_change,
device.STATE_NOT_HOME, device.STATE_HOME, ha.STATE_NOT_HOME, ha.STATE_HOME)
handle_device_state_change)
# Track when all devices are gone to shut down lights # Track when all devices are gone to shut down lights
ha.track_state_change(bus, device.STATE_CATEGORY_ALL_DEVICES, ha.track_state_change(bus, device_tracker.STATE_CATEGORY_ALL_DEVICES,
device.STATE_HOME, device.STATE_NOT_HOME, handle_device_state_change, ha.STATE_HOME,
handle_device_state_change) ha.STATE_NOT_HOME)
return True return True

View file

@ -1,5 +1,5 @@
""" """
homeassistant.components.sun homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices. Provides functionality to keep track of devices.
@ -16,23 +16,23 @@ import requests
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
import homeassistant.components.group as group
import homeassistant.external.pynetgear as pynetgear import homeassistant.external.pynetgear as pynetgear
DOMAIN_DEVICE_TRACKER = "device_tracker" DOMAIN = "device_tracker"
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
STATE_CATEGORY_ALL_DEVICES = 'devices' STATE_GROUP_NAME_ALL_DEVICES = 'all_tracked_devices'
STATE_CATEGORY_FORMAT = 'devices.{}' STATE_CATEGORY_ALL_DEVICES = group.STATE_CATEGORY_FORMAT.format(
STATE_GROUP_NAME_ALL_DEVICES)
STATE_NOT_HOME = 'device_not_home'
STATE_HOME = 'device_home'
STATE_CATEGORY_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if # After how much time do we consider a device not home if
# it does not show up on scans # it does not show up on scans
TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=1) TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
# Return cached results if last scan was less then this time ago # Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
@ -41,26 +41,15 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
KNOWN_DEVICES_FILE = "known_devices.csv" KNOWN_DEVICES_FILE = "known_devices.csv"
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)
def get_ids(statemachine):
""" Returns the devices that are being tracked in the statemachine. """
return ha.get_grouped_state_cats(statemachine, STATE_CATEGORY_FORMAT, True)
def is_home(statemachine, device_id=None): def is_home(statemachine, device_id=None):
""" Returns if any or specified device is home. """ """ Returns if any or specified device is home. """
category = STATE_CATEGORY_FORMAT.format(device_id) if device_id \ category = STATE_CATEGORY_FORMAT.format(device_id) if device_id \
else STATE_CATEGORY_ALL_DEVICES else STATE_CATEGORY_ALL_DEVICES
return statemachine.is_state(category, STATE_HOME) return statemachine.is_state(category, ha.STATE_HOME)
# pylint: disable=too-many-instance-attributes
class DeviceTracker(object): class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """ """ Class that tracks which devices are home and which are not. """
@ -88,12 +77,15 @@ class DeviceTracker(object):
self.update_devices( self.update_devices(
device_scanner.scan_devices())) device_scanner.scan_devices()))
bus.register_service(DOMAIN_DEVICE_TRACKER, bus.register_service(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD, SERVICE_DEVICE_TRACKER_RELOAD,
lambda service: self._read_known_devices_file()) lambda service: self._read_known_devices_file())
self.update_devices(device_scanner.scan_devices()) self.update_devices(device_scanner.scan_devices())
group.setup(bus, statemachine, STATE_GROUP_NAME_ALL_DEVICES,
list(self.device_state_categories))
@property @property
def device_state_categories(self): def device_state_categories(self):
""" Returns a set containing all categories """ Returns a set containing all categories
@ -119,7 +111,7 @@ class DeviceTracker(object):
self.known_devices[device]['last_seen'] = now self.known_devices[device]['last_seen'] = now
self.statemachine.set_state( self.statemachine.set_state(
self.known_devices[device]['category'], STATE_HOME) self.known_devices[device]['category'], ha.STATE_HOME)
# For all devices we did not find, set state to NH # 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 # But only if they have been gone for longer then the error time span
@ -131,18 +123,7 @@ class DeviceTracker(object):
self.statemachine.set_state( self.statemachine.set_state(
self.known_devices[device]['category'], self.known_devices[device]['category'],
STATE_NOT_HOME) ha.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 = (STATE_HOME if STATE_HOME
in states_of_devices else STATE_NOT_HOME)
self.statemachine.set_state(STATE_CATEGORY_ALL_DEVICES,
all_devices_state)
# If we come along any unknown devices we will write them to the # If we come along any unknown devices we will write them to the
# known devices file but only if we did not encounter an invalid # known devices file but only if we did not encounter an invalid
@ -407,8 +388,15 @@ class NetgearDeviceScanner(object):
self.date_updated = None self.date_updated = None
self.last_results = [] self.last_results = []
self.logger.info("Netgear:Logging in")
if self._api.login():
self.success_init = self._update_info() self.success_init = self._update_info()
else:
self.logger.error("Netgear:Failed to Login")
self.success_init = False
def scan_devices(self): def scan_devices(self):
""" Scans for new devices and return a """ Scans for new devices and return a
list containing found device ids. """ list containing found device ids. """
@ -446,6 +434,8 @@ class NetgearDeviceScanner(object):
self.last_results = self._api.get_attached_devices() self.last_results = self._api.get_attached_devices()
self.date_updated = datetime.now()
return True return True
else: else:

View file

@ -12,7 +12,7 @@ import requests
import homeassistant.util as util import homeassistant.util as util
DOMAIN_DOWNLOADER = "downloader" DOMAIN = "downloader"
SERVICE_DOWNLOAD_FILE = "download_file" SERVICE_DOWNLOAD_FILE = "download_file"
@ -77,7 +77,7 @@ def setup(bus, download_path):
logger.exception("FileDownloader:ConnectionError occured for {}". logger.exception("FileDownloader:ConnectionError occured for {}".
format(service.data['url'])) format(service.data['url']))
bus.register_service(DOMAIN_DOWNLOADER, SERVICE_DOWNLOAD_FILE, bus.register_service(DOMAIN, SERVICE_DOWNLOAD_FILE,
download_file) download_file)
return True return True

View file

@ -20,7 +20,7 @@ def shutdown_devices(bus, statemachine):
def setup(bus, statemachine): def setup(bus, statemachine):
""" Setup services related to homeassistant. """ """ Setup services related to homeassistant. """
bus.register_service(ha.DOMAIN_HOMEASSISTANT, SERVICE_SHUTDOWN_DEVICES, bus.register_service(ha.DOMAIN, SERVICE_SHUTDOWN_DEVICES,
lambda service: shutdown_devices(bus, statemachine)) lambda service: shutdown_devices(bus, statemachine))
return True return True

View file

@ -0,0 +1,122 @@
"""
homeassistant.components.groups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to group devices that can be turned on or off.
"""
import logging
import homeassistant as ha
DOMAIN = "group"
STATE_CATEGORY_FORMAT = DOMAIN + ".{}"
STATE_ATTR_CATEGORIES = "categories"
_GROUP_TYPES = {
"on_off": (ha.STATE_ON, ha.STATE_OFF),
"home_not_home": (ha.STATE_HOME, ha.STATE_NOT_HOME)
}
def _get_group_type(state):
""" Determine the group type based on the given group type. """
for group_type, states in _GROUP_TYPES.items():
if state in states:
return group_type
return None
def get_categories(statemachine, group_name):
""" Get the categories that make up this group. """
state = statemachine.get_state(STATE_CATEGORY_FORMAT.format(group_name))
return state['attributes'][STATE_ATTR_CATEGORIES] if state else []
# pylint: disable=too-many-branches
def setup(bus, statemachine, name, categories):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
logger = logging.getLogger(__name__)
# Loop over the given categories to:
# - determine which group type this is (on_off, device_home)
# - if all states exist and have valid states
# - retrieve the current state of the group
errors = []
group_type, group_on, group_off, group_state = None, None, None, None
for cat in categories:
state = statemachine.get_state(cat)
# Try to determine group type if we didn't yet
if not group_type and state:
group_type = _get_group_type(state['state'])
if group_type:
group_on, group_off = _GROUP_TYPES[group_type]
group_state = group_off
else:
# We did not find a matching group_type
errors.append("Found unexpected state '{}'".format(
name, state['state']))
break
# Check if category exists
if not state:
errors.append("Category {} does not exist".format(cat))
# Check if category is valid state
elif state['state'] != group_off and state['state'] != group_on:
errors.append("State of {} is {} (expected: {}, {})".format(
cat, state['state'], group_off, group_on))
# Keep track of the group state to init later on
elif group_state == group_off and state['state'] == group_on:
group_state = group_on
if errors:
logger.error("Error setting up state group {}: {}".format(
name, ", ".join(errors)))
return False
group_cat = STATE_CATEGORY_FORMAT.format(name)
state_attr = {STATE_ATTR_CATEGORIES: categories}
# pylint: disable=unused-argument
def _update_group_state(category, old_state, new_state):
""" Updates the group state based on a state change by a tracked
category. """
cur_group_state = statemachine.get_state(group_cat)['state']
# if cur_group_state = OFF and new_state = ON: set ON
# if cur_group_state = ON and new_state = OFF: research
# else: ignore
if cur_group_state == group_off and new_state['state'] == group_on:
statemachine.set_state(group_cat, group_on, state_attr)
elif cur_group_state == group_on and new_state['state'] == group_off:
# Check if any of the other states is still on
if not any([statemachine.is_state(cat, group_on)
for cat in categories if cat != category]):
statemachine.set_state(group_cat, group_off, state_attr)
for cat in categories:
ha.track_state_change(bus, cat, _update_group_state)
statemachine.set_state(group_cat, group_state, state_attr)
return True

View file

@ -6,7 +6,7 @@ Provides functionality to emulate keyboard presses on host machine.
""" """
import logging import logging
DOMAIN_KEYBOARD = "keyboard" DOMAIN = "keyboard"
SERVICE_KEYBOARD_VOLUME_UP = "volume_up" SERVICE_KEYBOARD_VOLUME_UP = "volume_up"
SERVICE_KEYBOARD_VOLUME_DOWN = "volume_down" SERVICE_KEYBOARD_VOLUME_DOWN = "volume_down"
@ -29,27 +29,27 @@ def setup(bus):
keyboard = pykeyboard.PyKeyboard() keyboard = pykeyboard.PyKeyboard()
keyboard.special_key_assignment() keyboard.special_key_assignment()
bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_UP, bus.register_service(DOMAIN, SERVICE_KEYBOARD_VOLUME_UP,
lambda service: lambda service:
keyboard.tap_key(keyboard.volume_up_key)) keyboard.tap_key(keyboard.volume_up_key))
bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_DOWN, bus.register_service(DOMAIN, SERVICE_KEYBOARD_VOLUME_DOWN,
lambda service: lambda service:
keyboard.tap_key(keyboard.volume_down_key)) keyboard.tap_key(keyboard.volume_down_key))
bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_MUTE, bus.register_service(DOMAIN, SERVICE_KEYBOARD_VOLUME_MUTE,
lambda service: lambda service:
keyboard.tap_key(keyboard.volume_mute_key)) keyboard.tap_key(keyboard.volume_mute_key))
bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_PLAY_PAUSE, bus.register_service(DOMAIN, SERVICE_KEYBOARD_MEDIA_PLAY_PAUSE,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_play_pause_key)) keyboard.tap_key(keyboard.media_play_pause_key))
bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_NEXT_TRACK, bus.register_service(DOMAIN, SERVICE_KEYBOARD_MEDIA_NEXT_TRACK,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_next_track_key)) keyboard.tap_key(keyboard.media_next_track_key))
bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_MEDIA_PREV_TRACK, bus.register_service(DOMAIN, SERVICE_KEYBOARD_MEDIA_PREV_TRACK,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_prev_track_key)) keyboard.tap_key(keyboard.media_prev_track_key))

View file

@ -1,5 +1,5 @@
""" """
homeassistant.components.sun homeassistant.components.light
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with lights. Provides functionality to interact with lights.
@ -10,14 +10,15 @@ from datetime import datetime, timedelta
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
import homeassistant.components.group as group
DOMAIN = "light" DOMAIN = "light"
STATE_CATEGORY_ALL_LIGHTS = 'lights' STATE_GROUP_NAME_ALL_LIGHTS = 'all_lights'
STATE_CATEGORY_FORMAT = "lights.{}" STATE_CATEGORY_ALL_LIGHTS = group.STATE_CATEGORY_FORMAT.format(
STATE_GROUP_NAME_ALL_LIGHTS)
STATE_ON = "on" STATE_CATEGORY_FORMAT = DOMAIN + ".{}"
STATE_OFF = "off"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -27,7 +28,7 @@ def is_on(statemachine, light_id=None):
category = STATE_CATEGORY_FORMAT.format(light_id) if light_id \ category = STATE_CATEGORY_FORMAT.format(light_id) if light_id \
else STATE_CATEGORY_ALL_LIGHTS else STATE_CATEGORY_ALL_LIGHTS
return statemachine.is_state(category, STATE_ON) return statemachine.is_state(category, ha.STATE_ON)
def turn_on(bus, light_id=None, transition_seconds=None): def turn_on(bus, light_id=None, transition_seconds=None):
@ -56,14 +57,11 @@ def turn_off(bus, light_id=None, transition_seconds=None):
bus.call_service(DOMAIN, ha.SERVICE_TURN_OFF, data) 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): def setup(bus, statemachine, light_control):
""" Exposes light control via statemachine and services. """ """ Exposes light control via statemachine and services. """
logger = logging.getLogger(__name__)
def update_light_state(time): # pylint: disable=unused-argument def update_light_state(time): # pylint: disable=unused-argument
""" Track the state of the lights. """ """ Track the state of the lights. """
try: try:
@ -74,6 +72,8 @@ def setup(bus, statemachine, light_control):
should_update = True should_update = True
if should_update: if should_update:
logger.info("Updating light status")
update_light_state.last_updated = datetime.now() update_light_state.last_updated = datetime.now()
status = {light_id: light_control.is_light_on(light_id) status = {light_id: light_control.is_light_on(light_id)
@ -82,17 +82,21 @@ def setup(bus, statemachine, light_control):
for light_id, state in status.items(): for light_id, state in status.items():
state_category = STATE_CATEGORY_FORMAT.format(light_id) state_category = STATE_CATEGORY_FORMAT.format(light_id)
statemachine.set_state(state_category, new_state = ha.STATE_ON if state else ha.STATE_OFF
STATE_ON if state
else STATE_OFF)
statemachine.set_state(STATE_CATEGORY_ALL_LIGHTS, statemachine.set_state(state_category, new_state)
STATE_ON if True in status.values()
else STATE_OFF)
ha.track_time_change(bus, update_light_state, second=[0, 30]) ha.track_time_change(bus, update_light_state, second=[0, 30])
def handle_light_event(service): update_light_state(None)
# Track the all lights state
light_cats = [STATE_CATEGORY_FORMAT.format(light_id) for light_id
in light_control.light_ids]
group.setup(bus, statemachine, STATE_GROUP_NAME_ALL_LIGHTS, light_cats)
def handle_light_service(service):
""" Hande a turn light on or off service call. """ """ Hande a turn light on or off service call. """
light_id = service.data.get("light_id", None) light_id = service.data.get("light_id", None)
transition_seconds = service.data.get("transition_seconds", None) transition_seconds = service.data.get("transition_seconds", None)
@ -106,12 +110,10 @@ def setup(bus, statemachine, light_control):
# Listen for light on and light off events # Listen for light on and light off events
bus.register_service(DOMAIN, ha.SERVICE_TURN_ON, bus.register_service(DOMAIN, ha.SERVICE_TURN_ON,
handle_light_event) handle_light_service)
bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF, bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF,
handle_light_event) handle_light_service)
update_light_state(None)
return True return True