From 48026c28c101eec23e3ec21f6705af1f31b950d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Jan 2014 17:55:05 -0800 Subject: [PATCH] Added state groups and migrated code base to use them. --- home-assistant.conf.default | 18 ++- homeassistant/__init__.py | 43 +++--- homeassistant/bootstrap.py | 62 +++++---- homeassistant/components/browser.py | 4 +- homeassistant/components/chromecast.py | 32 ++--- .../components/device_sun_light_trigger.py | 47 ++++--- .../{device.py => device_tracker.py} | 62 ++++----- homeassistant/components/downloader.py | 4 +- homeassistant/components/general.py | 2 +- homeassistant/components/group.py | 122 ++++++++++++++++++ homeassistant/components/keyboard.py | 14 +- homeassistant/components/light.py | 46 +++---- 12 files changed, 296 insertions(+), 160 deletions(-) rename homeassistant/components/{device.py => device_tracker.py} (91%) create mode 100644 homeassistant/components/group.py diff --git a/home-assistant.conf.default b/home-assistant.conf.default index 8f6ff39594c8..4bcfdc5cb799 100644 --- a/home-assistant.conf.default +++ b/home-assistant.conf.default @@ -5,16 +5,16 @@ longitude=-117.22743 [httpinterface] api_password=mypass -[hue] +[light.hue] host=192.168.1.2 -[tomato] +[device_tracker.tomato] host=192.168.1.1 username=admin password=PASSWORD http_id=aaaaaaaaaaaaaaa -[netgear] +[device_tracker.netgear] host=192.168.1.1 username=admin password=PASSWORD @@ -23,4 +23,14 @@ password=PASSWORD host=192.168.1.3 [downloader] -download_dir=downloads \ No newline at end of file +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 diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index fee5397b981b..cc3185f06ddf 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -16,7 +16,12 @@ logging.basicConfig(level=logging.INFO) 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_OFF = "turn_off" @@ -40,7 +45,7 @@ def start_home_assistant(bus): """ Start home assistant. """ request_shutdown = threading.Event() - bus.register_service(DOMAIN_HOMEASSISTANT, SERVICE_HOMEASSISTANT_STOP, + bus.register_service(DOMAIN, SERVICE_HOMEASSISTANT_STOP, lambda service: request_shutdown.set()) Timer(bus) @@ -88,25 +93,19 @@ 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. +def split_state_category(category): + """ 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 - 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 filter_categories(categories, domain_filter=None, object_id_only=False): + """ Filter a list of categories based on domain. Setting object_id_only + will only return the object_ids. """ + return [ + split_state_category(cat)[1] if object_id_only else cat + for cat in categories if + not domain_filter or cat.startswith(domain_filter) + ] 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)} -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. """ - from_state = _ensure_list(from_state) - to_state = _ensure_list(to_state) + from_state = _ensure_list(from_state) if from_state else [ALL_EVENTS] + to_state = _ensure_list(to_state) if to_state else [ALL_EVENTS] def listener(event): """ State change listener that listens for specific state changes. """ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ffd6735d9f08..10396475cbdd 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -7,9 +7,9 @@ import logging import homeassistant as ha from homeassistant.components import (general, chromecast, - device_sun_light_trigger, device, + device_sun_light_trigger, device_tracker, downloader, keyboard, light, sun, - browser, httpinterface) + browser, httpinterface, group) # 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 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 dev_scan = None @@ -41,23 +48,23 @@ def from_config_file(config_path): # For the error message if not all option fields exist opt_fields = "host, username, password" - if has_section('tomato'): + if has_section('device_tracker.tomato'): dev_scan_name = "Tomato" opt_fields += ", http_id" - dev_scan = device.TomatoDeviceScanner( - get_opt('tomato', 'host'), - get_opt('tomato', 'username'), - get_opt('tomato', 'password'), - get_opt('tomato', 'http_id')) + dev_scan = device_tracker.TomatoDeviceScanner( + get_opt('device_tracker.tomato', 'host'), + get_opt('device_tracker.tomato', 'username'), + get_opt('device_tracker.tomato', 'password'), + get_opt('device_tracker.tomato', 'http_id')) - elif has_section('netgear'): + elif has_section('device_tracker.netgear'): dev_scan_name = "Netgear" - dev_scan = device.NetgearDeviceScanner( - get_opt('netgear', 'host'), - get_opt('netgear', 'username'), - get_opt('netgear', 'password')) + dev_scan = device_tracker.NetgearDeviceScanner( + get_opt('device_tracker.netgear', 'host'), + get_opt('device_tracker.netgear', 'username'), + get_opt('device_tracker.netgear', 'password')) except ConfigParser.NoOptionError: # If one of the options didn't exist @@ -76,7 +83,7 @@ def from_config_file(config_path): # Device Tracker if dev_scan: - device.DeviceTracker(bus, statemachine, dev_scan) + device_tracker.DeviceTracker(bus, statemachine, dev_scan) add_status("Device Tracker", True) @@ -100,11 +107,8 @@ def from_config_file(config_path): chromecast_started = False # Light control - if has_section("hue"): - if has_opt("hue", "host"): - light_control = light.HueLightControl(get_opt("hue", "host")) - else: - light_control = light.HueLightControl() + if has_section("light.hue"): + light_control = light.HueLightControl(get_opt_safe("hue", "host")) add_status("Light Control - Hue", light_control.success_init) @@ -112,11 +116,6 @@ def from_config_file(config_path): else: 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"): add_status("Downloader", downloader.setup( bus, get_opt("downloader", "download_dir"))) @@ -137,6 +136,21 @@ def from_config_file(config_path): 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: status = "initialized" if success_init else "Failed to initialize" diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index bc5741caa20c..de8cb6a782ec 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -5,7 +5,7 @@ homeassistant.components.browser Provides functionality to launch a webbrowser on the host machine. """ -DOMAIN_BROWSER = "browser" +DOMAIN = "browser" SERVICE_BROWSE_URL = "browse_url" @@ -16,7 +16,7 @@ def setup(bus): import webbrowser - bus.register_service(DOMAIN_BROWSER, SERVICE_BROWSE_URL, + bus.register_service(DOMAIN, SERVICE_BROWSE_URL, lambda service: webbrowser.open(service.data['url'])) return True diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index 70c4f00dcfdf..49666301505f 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -4,6 +4,7 @@ homeassistant.components.chromecast Provides functionality to interact with Chromecasts. """ +import logging from homeassistant.external import pychromecast @@ -11,11 +12,11 @@ import homeassistant as ha import homeassistant.util as util -DOMAIN_CHROMECAST = "chromecast" +DOMAIN = "chromecast" SERVICE_YOUTUBE_VIDEO = "play_youtube_video" -STATE_CATEGORY_FORMAT = 'chromecasts.{}' +STATE_CATEGORY_FORMAT = DOMAIN + '.{}' STATE_NO_APP = "none" ATTR_FRIENDLY_NAME = "friendly_name" @@ -24,24 +25,12 @@ 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) + else ha.filter_categories(statemachine.categories, DOMAIN) for cat in cats: state = statemachine.get_state(cat) @@ -55,34 +44,39 @@ def turn_off(statemachine, cc_id=None): def setup(bus, statemachine, host): """ Listen for chromecast events. """ + logger = logging.getLogger(__name__) + + logger.info("Getting device status") device = pychromecast.get_device_status(host) if not device: + logger.error("Could not find Chromecast") return False category = STATE_CATEGORY_FORMAT.format(util.slugify( device.friendly_name)) - bus.register_service(DOMAIN_CHROMECAST, ha.SERVICE_TURN_OFF, + bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF, lambda service: turn_off(statemachine, service.data.get("cc_id", None))) - bus.register_service(DOMAIN_CHROMECAST, "start_fireplace", + bus.register_service(DOMAIN, "start_fireplace", lambda service: pychromecast.play_youtube_video(host, "eyU3bRy2x44")) - bus.register_service(DOMAIN_CHROMECAST, "start_epic_sax", + bus.register_service(DOMAIN, "start_epic_sax", lambda service: pychromecast.play_youtube_video(host, "kxopViU98Xo")) - bus.register_service(DOMAIN_CHROMECAST, SERVICE_YOUTUBE_VIDEO, + bus.register_service(DOMAIN, 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. """ + logger.info("Updating app status") status = pychromecast.get_app_status(host) if status: diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index aa875058475b..284ff65940d8 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -10,29 +10,35 @@ from datetime import datetime, timedelta import homeassistant as ha -from . import light, sun, device, general +from . import light, sun, device_tracker, general, group LIGHT_TRANSITION_TIME = timedelta(minutes=15) # 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. """ 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: - logger.error("LightTrigger:No devices given to track") + if not device_state_categories: + logger.error("LightTrigger:No devices found to track") 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: - logger.error("LightTrigger:No lights found to turn on") + # 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 ") return False @@ -50,7 +56,7 @@ def setup(bus, statemachine): 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 + if (device_tracker.is_home(statemachine) and not light.is_on(statemachine, light_id)): 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 # pre-sun set event - ha.track_state_change(bus, sun.STATE_CATEGORY, sun.STATE_BELOW_HORIZON, - sun.STATE_ABOVE_HORIZON, handle_sun_rising) + ha.track_state_change(bus, sun.STATE_CATEGORY, handle_sun_rising, + sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) # If the sun is already above horizon # 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)) # Specific device came home ? - if (category != device.STATE_CATEGORY_ALL_DEVICES and - new_state['state'] == device.STATE_HOME): + if (category != device_tracker.STATE_CATEGORY_ALL_DEVICES and + new_state['state'] == ha.STATE_HOME): # These variables are needed for the elif check now = datetime.now() @@ -120,8 +126,8 @@ def setup(bus, statemachine): 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): + elif (category == device_tracker.STATE_CATEGORY_ALL_DEVICES and + new_state['state'] == ha.STATE_NOT_HOME and lights_are_on): logger.info( "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 for category in device_state_categories: - ha.track_state_change(bus, category, - device.STATE_NOT_HOME, device.STATE_HOME, - handle_device_state_change) + ha.track_state_change(bus, category, handle_device_state_change, + ha.STATE_NOT_HOME, ha.STATE_HOME) # 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) + ha.track_state_change(bus, device_tracker.STATE_CATEGORY_ALL_DEVICES, + handle_device_state_change, ha.STATE_HOME, + ha.STATE_NOT_HOME) return True diff --git a/homeassistant/components/device.py b/homeassistant/components/device_tracker.py similarity index 91% rename from homeassistant/components/device.py rename to homeassistant/components/device_tracker.py index bbeb29ffb610..1a56415d829f 100644 --- a/homeassistant/components/device.py +++ b/homeassistant/components/device_tracker.py @@ -1,5 +1,5 @@ """ -homeassistant.components.sun +homeassistant.components.tracker ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to keep track of devices. @@ -16,23 +16,23 @@ import requests import homeassistant as ha import homeassistant.util as util +import homeassistant.components.group as group import homeassistant.external.pynetgear as pynetgear -DOMAIN_DEVICE_TRACKER = "device_tracker" +DOMAIN = "device_tracker" SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" -STATE_CATEGORY_ALL_DEVICES = 'devices' -STATE_CATEGORY_FORMAT = 'devices.{}' - -STATE_NOT_HOME = 'device_not_home' -STATE_HOME = 'device_home' +STATE_GROUP_NAME_ALL_DEVICES = 'all_tracked_devices' +STATE_CATEGORY_ALL_DEVICES = group.STATE_CATEGORY_FORMAT.format( + STATE_GROUP_NAME_ALL_DEVICES) +STATE_CATEGORY_FORMAT = DOMAIN + '.{}' # After how much time do we consider a device not home if # 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 MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) @@ -41,26 +41,15 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) 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): """ Returns if any or specified device is home. """ category = STATE_CATEGORY_FORMAT.format(device_id) if device_id \ 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 that tracks which devices are home and which are not. """ @@ -88,12 +77,15 @@ class DeviceTracker(object): self.update_devices( device_scanner.scan_devices())) - bus.register_service(DOMAIN_DEVICE_TRACKER, + bus.register_service(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, lambda service: self._read_known_devices_file()) self.update_devices(device_scanner.scan_devices()) + group.setup(bus, statemachine, STATE_GROUP_NAME_ALL_DEVICES, + list(self.device_state_categories)) + @property def device_state_categories(self): """ Returns a set containing all categories @@ -119,7 +111,7 @@ class DeviceTracker(object): self.known_devices[device]['last_seen'] = now 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 # 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.known_devices[device]['category'], - 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) + ha.STATE_NOT_HOME) # 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 @@ -407,7 +388,14 @@ class NetgearDeviceScanner(object): self.date_updated = None self.last_results = [] - self.success_init = self._update_info() + self.logger.info("Netgear:Logging in") + if self._api.login(): + self.success_init = self._update_info() + + else: + self.logger.error("Netgear:Failed to Login") + + self.success_init = False def scan_devices(self): """ Scans for new devices and return a @@ -446,6 +434,8 @@ class NetgearDeviceScanner(object): self.last_results = self._api.get_attached_devices() + self.date_updated = datetime.now() + return True else: diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 8c57f959a338..3c2f4dd6211d 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -12,7 +12,7 @@ import requests import homeassistant.util as util -DOMAIN_DOWNLOADER = "downloader" +DOMAIN = "downloader" SERVICE_DOWNLOAD_FILE = "download_file" @@ -77,7 +77,7 @@ def setup(bus, download_path): logger.exception("FileDownloader:ConnectionError occured for {}". format(service.data['url'])) - bus.register_service(DOMAIN_DOWNLOADER, SERVICE_DOWNLOAD_FILE, + bus.register_service(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file) return True diff --git a/homeassistant/components/general.py b/homeassistant/components/general.py index 711f3459f7c5..ddcb37c57aaf 100644 --- a/homeassistant/components/general.py +++ b/homeassistant/components/general.py @@ -20,7 +20,7 @@ def shutdown_devices(bus, statemachine): def setup(bus, statemachine): """ 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)) return True diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py new file mode 100644 index 000000000000..ff1ef2ed1bd0 --- /dev/null +++ b/homeassistant/components/group.py @@ -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 diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 374d88b804cc..746aed6564d5 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -6,7 +6,7 @@ Provides functionality to emulate keyboard presses on host machine. """ import logging -DOMAIN_KEYBOARD = "keyboard" +DOMAIN = "keyboard" SERVICE_KEYBOARD_VOLUME_UP = "volume_up" SERVICE_KEYBOARD_VOLUME_DOWN = "volume_down" @@ -29,27 +29,27 @@ def setup(bus): keyboard = pykeyboard.PyKeyboard() keyboard.special_key_assignment() - bus.register_service(DOMAIN_KEYBOARD, SERVICE_KEYBOARD_VOLUME_UP, + bus.register_service(DOMAIN, SERVICE_KEYBOARD_VOLUME_UP, lambda service: 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: 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: 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: 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: 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: keyboard.tap_key(keyboard.media_prev_track_key)) diff --git a/homeassistant/components/light.py b/homeassistant/components/light.py index 94648ad33083..061dc9d69196 100644 --- a/homeassistant/components/light.py +++ b/homeassistant/components/light.py @@ -1,5 +1,5 @@ """ -homeassistant.components.sun +homeassistant.components.light ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to interact with lights. @@ -10,14 +10,15 @@ from datetime import datetime, timedelta import homeassistant as ha import homeassistant.util as util +import homeassistant.components.group as group DOMAIN = "light" -STATE_CATEGORY_ALL_LIGHTS = 'lights' -STATE_CATEGORY_FORMAT = "lights.{}" +STATE_GROUP_NAME_ALL_LIGHTS = 'all_lights' +STATE_CATEGORY_ALL_LIGHTS = group.STATE_CATEGORY_FORMAT.format( + STATE_GROUP_NAME_ALL_LIGHTS) -STATE_ON = "on" -STATE_OFF = "off" +STATE_CATEGORY_FORMAT = DOMAIN + ".{}" 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 \ 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): @@ -56,14 +57,11 @@ def turn_off(bus, light_id=None, transition_seconds=None): 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. """ + logger = logging.getLogger(__name__) + def update_light_state(time): # pylint: disable=unused-argument """ Track the state of the lights. """ try: @@ -74,6 +72,8 @@ def setup(bus, statemachine, light_control): should_update = True if should_update: + logger.info("Updating light status") + update_light_state.last_updated = datetime.now() 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(): state_category = STATE_CATEGORY_FORMAT.format(light_id) - statemachine.set_state(state_category, - STATE_ON if state - else STATE_OFF) + new_state = ha.STATE_ON if state else ha.STATE_OFF - statemachine.set_state(STATE_CATEGORY_ALL_LIGHTS, - STATE_ON if True in status.values() - else STATE_OFF) + statemachine.set_state(state_category, new_state) 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. """ light_id = service.data.get("light_id", 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 bus.register_service(DOMAIN, ha.SERVICE_TURN_ON, - handle_light_event) + handle_light_service) bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF, - handle_light_event) - - update_light_state(None) + handle_light_service) return True