Lights now support profiles

This commit is contained in:
Paulus Schoutsen 2014-03-26 00:08:50 -07:00
parent 2890f2d6cc
commit 90769fc0eb
6 changed files with 132 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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
1 id x y brightness
2 relax 0.5119 0.4147 144
3 concentrate 0.5119 0.4147 219
4 energize 0.368 0.3686 203
5 reading 0.4448 0.4066 240

View File

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