From 90769fc0ebbf6b7e310d38112b2d8d6d972271f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Mar 2014 00:08:50 -0700 Subject: [PATCH] Lights now support profiles --- home-assistant.conf.default | 2 + homeassistant/bootstrap.py | 4 +- .../components/device_sun_light_trigger.py | 21 ++- .../{light.py => light/__init__.py} | 127 +++++++++++++++--- .../components/light/light_profiles.csv | 5 + homeassistant/util.py | 7 +- 6 files changed, 132 insertions(+), 34 deletions(-) rename homeassistant/components/{light.py => light/__init__.py} (73%) create mode 100644 homeassistant/components/light/light_profiles.csv diff --git a/home-assistant.conf.default b/home-assistant.conf.default index 482ba28a869..ce6f7d9c14b 100644 --- a/home-assistant.conf.default +++ b/home-assistant.conf.default @@ -29,6 +29,8 @@ download_dir=downloads [device_sun_light_trigger] # Example how you can specify a specific group that has to be turned on # light_group=group.living_room +# Example how you can specify which light profile to use when turning lights on +# light_profile=relax # A comma seperated list of states that have to be tracked # As a single group diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 098b96ec52a..c0a8a902f26 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -184,10 +184,12 @@ def from_config_file(config_path): device_sun_light_trigger = load_module('device_sun_light_trigger') light_group = get_opt_safe("device_sun_light_trigger", "light_group") + light_profile = get_opt_safe("device_sun_light_trigger", + "light_profile") add_status("Device Sun Light Trigger", device_sun_light_trigger.setup(bus, statemachine, - light_group)) + light_group, light_profile)) for component, success_init in statusses: status = "initialized" if success_init else "Failed to initialize" diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 961f63f5968..cee60279f4c 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -15,12 +15,15 @@ from . import light, sun, device_tracker, group LIGHT_TRANSITION_TIME = timedelta(minutes=15) -LIGHT_BRIGHTNESS = 164 -LIGHT_XY_COLOR = [0.5119, 0.4147] + +# Light profile to be used if none given +LIGHT_PROFILE = 'relax' # pylint: disable=too-many-branches -def setup(bus, statemachine, light_group=None): +def setup(bus, statemachine, + light_group=light.GROUP_NAME_ALL_LIGHTS, + light_profile=LIGHT_PROFILE): """ Triggers to turn lights on or off based on device precense. """ logger = logging.getLogger(__name__) @@ -29,18 +32,16 @@ def setup(bus, statemachine, light_group=None): device_tracker.DOMAIN) if not device_entity_ids: - logger.error("LightTrigger:No devices found to track") + logger.error("No devices found to track") return False - light_group = light_group or light.GROUP_NAME_ALL_LIGHTS - # Get the light IDs from the specified group light_ids = util.filter_entity_ids( group.get_entity_ids(statemachine, light_group), light.DOMAIN) if not light_ids: - logger.error("LightTrigger:No lights found to turn on ") + logger.error("No lights found to turn on ") return False @@ -68,8 +69,7 @@ def setup(bus, statemachine, light_group=None): light.turn_on(bus, light_id, transition=LIGHT_TRANSITION_TIME.seconds, - brightness=LIGHT_BRIGHTNESS, - xy_color=LIGHT_XY_COLOR) + profile=light_profile) def turn_on(light_id): """ Lambda can keep track of function parameters but not local @@ -121,8 +121,7 @@ def setup(bus, statemachine, light_group=None): # So we skip fetching the entity ids again. for light_id in light_ids: light.turn_on(bus, light_id, - brightness=LIGHT_BRIGHTNESS, - xy_color=LIGHT_XY_COLOR) + profile=light_profile) # Are we in the time span were we would turn on the lights # if someone would be home? diff --git a/homeassistant/components/light.py b/homeassistant/components/light/__init__.py similarity index 73% rename from homeassistant/components/light.py rename to homeassistant/components/light/__init__.py index 4d3d179209d..d8dd2c4bdb7 100644 --- a/homeassistant/components/light.py +++ b/homeassistant/components/light/__init__.py @@ -3,12 +3,57 @@ homeassistant.components.light ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to interact with lights. + +It offers the following services: + +TURN_OFF - Turns one or multiple lights off. + +Supports following parameters: + - transition + Integer that represents the time the light should take to transition to + the new state. + - entity_id + String or list of strings that point at entity_ids of lights. + +TURN_ON - Turns one or multiple lights on and change attributes. + +Supports following parameters: + - transition + Integer that represents the time the light should take to transition to + the new state. + + - entity_id + String or list of strings that point at entity_ids of lights. + + - profile + String with the name of one of the built-in profiles (relax, energize, + concentrate, reading) or one of the custom profiles defined in + light_profiles.csv in the current working directory. + + Light profiles define a xy color and a brightness. + + If a profile is given and a brightness or xy color then the profile values + will be overwritten. + + - xy_color + A list containing two floats representing the xy color you want the light + to be. + + - rgb_color + A list containing three integers representing the xy color you want the + light to be. + + - brightness + Integer between 0 and 255 representing how bright you want the light to be. + """ import logging import socket from datetime import datetime, timedelta from collections import namedtuple +import os +import csv import homeassistant as ha import homeassistant.util as util @@ -38,6 +83,11 @@ ATTR_XY_COLOR = "xy_color" # int with value 0 .. 255 representing brightness of the light ATTR_BRIGHTNESS = "brightness" +# String representing a profile (built-in ones or external defined) +ATTR_PROFILE = "profile" + +LIGHT_PROFILES_FILE = "light_profiles.csv" + def is_on(statemachine, entity_id=None): """ Returns if the lights are on based on the statemachine. """ @@ -48,23 +98,26 @@ def is_on(statemachine, entity_id=None): # pylint: disable=too-many-arguments def turn_on(bus, entity_id=None, transition=None, brightness=None, - rgb_color=None, xy_color=None): + rgb_color=None, xy_color=None, profile=None): """ Turns all or specified light on. """ data = {} if entity_id: data[ATTR_ENTITY_ID] = entity_id + if profile: + data[ATTR_PROFILE] = profile + if transition is not None: data[ATTR_TRANSITION] = transition if brightness is not None: data[ATTR_BRIGHTNESS] = brightness - if rgb_color is not None: + if rgb_color: data[ATTR_RGB_COLOR] = rgb_color - if xy_color is not None: + if xy_color: data[ATTR_XY_COLOR] = xy_color bus.call_service(DOMAIN, SERVICE_TURN_ON, data) @@ -83,7 +136,7 @@ def turn_off(bus, entity_id=None, transition=None): bus.call_service(DOMAIN, SERVICE_TURN_OFF, data) -# pylint: disable=too-many-branches +# pylint: disable=too-many-branches, too-many-locals def setup(bus, statemachine, light_control): """ Exposes light control via statemachine and services. """ @@ -149,10 +202,42 @@ def setup(bus, statemachine, light_control): # Update light state and discover lights for tracking the group update_lights_state(None, True) + if len(ent_to_light) == 0: + logger.error("No lights found") + return False + # Track all lights in a group group.setup(bus, statemachine, GROUP_NAME_ALL_LIGHTS, light_to_ent.values()) + # Load built-in profiles and custom profiles + profile_paths = [os.path.dirname(__file__), os.getcwd()] + profiles = {} + + for dir_path in profile_paths: + file_path = os.path.join(dir_path, LIGHT_PROFILES_FILE) + + if os.path.isfile(file_path): + with open(file_path, 'rb') as inp: + reader = csv.reader(inp) + + # Skip the header + next(reader, None) + + try: + for profile_id, color_x, color_y, brightness in reader: + profiles[profile_id] = (float(color_x), float(color_y), + int(brightness)) + + except ValueError: + # ValueError if not 4 values per row + # ValueError if convert to float/int failed + logger.error( + "Error parsing light profiles from {}".format( + file_path)) + + return False + def handle_light_service(service): """ Hande a turn light on or off service call. """ # Get and validate data @@ -166,36 +251,46 @@ def setup(bus, statemachine, light_control): if not light_ids: light_ids = ent_to_light.values() - transition = util.dict_get_convert(dat, ATTR_TRANSITION, int, None) + transition = util.convert(dat.get(ATTR_TRANSITION), int) if service.service == SERVICE_TURN_OFF: light_control.turn_light_off(light_ids, transition) else: # Processing extra data for turn light on request - bright = util.dict_get_convert(dat, ATTR_BRIGHTNESS, int, 164) - color = None - xy_color = dat.get(ATTR_XY_COLOR) - rgb_color = dat.get(ATTR_RGB_COLOR) + # We process the profile first so that we get the desired + # behavior that extra service data attributes overwrite + # profile values + profile = profiles.get(dat.get(ATTR_PROFILE)) - if xy_color: + if profile: + color = profile[0:2] + bright = profile[2] + else: + color = None + bright = None + + if ATTR_BRIGHTNESS in dat: + bright = util.convert(dat.get(ATTR_BRIGHTNESS), int) + + if ATTR_XY_COLOR in dat: try: # xy_color should be a list containing 2 floats - xy_color = [float(val) for val in xy_color] + xy_color = [float(val) for val in dat.get(ATTR_XY_COLOR)] if len(xy_color) == 2: color = xy_color except (TypeError, ValueError): - # TypeError if xy_color was not iterable + # TypeError if dat[ATTR_XY_COLOR] is not iterable # ValueError if value could not be converted to float pass - if not color and rgb_color: + if ATTR_RGB_COLOR in dat: try: # rgb_color should be a list containing 3 ints - rgb_color = [int(val) for val in rgb_color] + rgb_color = [int(val) for val in dat.get(ATTR_RGB_COLOR)] if len(rgb_color) == 3: color = util.color_RGB_to_xy(rgb_color[0], @@ -203,8 +298,8 @@ def setup(bus, statemachine, light_control): rgb_color[2]) except (TypeError, ValueError): - # TypeError if color has no len - # ValueError if not all values convertable to int + # TypeError if dat[ATTR_RGB_COLOR] is not iterable + # ValueError if not all values can be converted to int pass light_control.turn_light_on(light_ids, transition, bright, color) diff --git a/homeassistant/components/light/light_profiles.csv b/homeassistant/components/light/light_profiles.csv new file mode 100644 index 00000000000..7a3a66e1cd2 --- /dev/null +++ b/homeassistant/components/light/light_profiles.csv @@ -0,0 +1,5 @@ +id,x,y,brightness +relax,0.5119,0.4147,144 +concentrate,0.5119,0.4147,219 +energize,0.368,0.3686,203 +reading,0.4448,0.4066,240 diff --git a/homeassistant/util.py b/homeassistant/util.py index c29284541e2..22bbfc58855 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -105,15 +105,10 @@ def color_RGB_to_xy(R, G, B): return X / (X + Y + Z), Y / (X + Y + Z) -def dict_get_convert(dic, key, value_type, default=None): - """ Get a value from a dic and ensure it is value_type. """ - return convert(dic[key], value_type, default) if key in dic else default - - def convert(value, to_type, default=None): """ Converts value to to_type, returns default if fails. """ try: - return to_type(value) + return default if value is None else to_type(value) except ValueError: # If value could not be converted return default