2013-12-11 08:07:30 +00:00
|
|
|
"""
|
2014-01-05 01:55:05 +00:00
|
|
|
homeassistant.components.light
|
2015-08-11 12:52:53 +00:00
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2013-12-11 08:07:30 +00:00
|
|
|
|
|
|
|
Provides functionality to interact with lights.
|
2014-03-26 07:08:50 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2015-10-28 23:12:16 +00:00
|
|
|
- color_temp
|
2015-10-27 22:34:49 +00:00
|
|
|
An INT in mireds represending the color temperature you want the light to be
|
|
|
|
|
2014-03-26 07:08:50 +00:00
|
|
|
- brightness
|
|
|
|
Integer between 0 and 255 representing how bright you want the light to be.
|
|
|
|
|
2013-12-11 08:07:30 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
import logging
|
2014-03-26 07:08:50 +00:00
|
|
|
import os
|
|
|
|
import csv
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2015-09-27 06:17:04 +00:00
|
|
|
from homeassistant.components import group, discovery, wink, isy994
|
|
|
|
from homeassistant.config import load_yaml_config_file
|
|
|
|
from homeassistant.const import (
|
|
|
|
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
2015-06-13 23:42:09 +00:00
|
|
|
from homeassistant.helpers.entity import ToggleEntity
|
2015-09-27 06:17:04 +00:00
|
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
2013-12-11 08:07:30 +00:00
|
|
|
import homeassistant.util as util
|
2015-07-07 07:01:17 +00:00
|
|
|
import homeassistant.util.color as color_util
|
2014-03-15 07:24:28 +00:00
|
|
|
|
2013-12-11 08:07:30 +00:00
|
|
|
|
|
|
|
DOMAIN = "light"
|
2014-08-13 12:28:45 +00:00
|
|
|
DEPENDENCIES = []
|
2015-03-01 09:35:58 +00:00
|
|
|
SCAN_INTERVAL = 30
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2015-01-20 07:47:18 +00:00
|
|
|
GROUP_NAME_ALL_LIGHTS = 'all lights'
|
|
|
|
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights')
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-01-20 07:37:40 +00:00
|
|
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-03-16 22:00:59 +00:00
|
|
|
# integer that represents transition time in seconds to make change
|
|
|
|
ATTR_TRANSITION = "transition"
|
|
|
|
|
|
|
|
# lists holding color values
|
|
|
|
ATTR_RGB_COLOR = "rgb_color"
|
|
|
|
ATTR_XY_COLOR = "xy_color"
|
2015-10-28 23:12:16 +00:00
|
|
|
ATTR_COLOR_TEMP = "color_temp"
|
2014-03-16 22:00:59 +00:00
|
|
|
|
|
|
|
# int with value 0 .. 255 representing brightness of the light
|
|
|
|
ATTR_BRIGHTNESS = "brightness"
|
|
|
|
|
2014-03-26 07:08:50 +00:00
|
|
|
# String representing a profile (built-in ones or external defined)
|
|
|
|
ATTR_PROFILE = "profile"
|
|
|
|
|
2014-12-09 07:02:38 +00:00
|
|
|
# If the light should flash, can be FLASH_SHORT or FLASH_LONG
|
|
|
|
ATTR_FLASH = "flash"
|
|
|
|
FLASH_SHORT = "short"
|
|
|
|
FLASH_LONG = "long"
|
|
|
|
|
2015-07-08 18:26:37 +00:00
|
|
|
# Apply an effect to the light, can be EFFECT_COLORLOOP
|
|
|
|
ATTR_EFFECT = "effect"
|
|
|
|
EFFECT_COLORLOOP = "colorloop"
|
|
|
|
|
2014-03-26 07:08:50 +00:00
|
|
|
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
|
|
|
|
2015-01-11 07:47:23 +00:00
|
|
|
# Maps discovered services to their platforms
|
|
|
|
DISCOVERY_PLATFORMS = {
|
|
|
|
wink.DISCOVER_LIGHTS: 'wink',
|
2015-04-04 10:13:27 +00:00
|
|
|
isy994.DISCOVER_LIGHTS: 'isy994',
|
2015-07-20 07:07:01 +00:00
|
|
|
discovery.SERVICE_HUE: 'hue',
|
2015-01-11 07:47:23 +00:00
|
|
|
}
|
|
|
|
|
2015-06-13 23:42:09 +00:00
|
|
|
PROP_TO_ATTR = {
|
|
|
|
'brightness': ATTR_BRIGHTNESS,
|
|
|
|
'color_xy': ATTR_XY_COLOR,
|
2015-10-28 23:12:16 +00:00
|
|
|
'color_temp': ATTR_COLOR_TEMP,
|
2015-06-13 23:42:09 +00:00
|
|
|
}
|
|
|
|
|
2014-11-09 23:12:23 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-04-24 07:40:45 +00:00
|
|
|
def is_on(hass, entity_id=None):
|
2013-12-11 08:07:30 +00:00
|
|
|
""" Returns if the lights are on based on the statemachine. """
|
2014-01-21 07:23:02 +00:00
|
|
|
entity_id = entity_id or ENTITY_ID_ALL_LIGHTS
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-04-24 07:40:45 +00:00
|
|
|
return hass.states.is_state(entity_id, STATE_ON)
|
2013-12-11 08:07:30 +00:00
|
|
|
|
|
|
|
|
2014-03-16 22:00:59 +00:00
|
|
|
# pylint: disable=too-many-arguments
|
2014-04-24 07:40:45 +00:00
|
|
|
def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
2015-10-28 23:12:16 +00:00
|
|
|
rgb_color=None, xy_color=None, color_temp=None, profile=None,
|
2015-10-27 22:34:49 +00:00
|
|
|
flash=None, effect=None):
|
2013-12-11 08:07:30 +00:00
|
|
|
""" Turns all or specified light on. """
|
2014-12-09 07:02:38 +00:00
|
|
|
data = {
|
|
|
|
key: value for key, value in [
|
|
|
|
(ATTR_ENTITY_ID, entity_id),
|
|
|
|
(ATTR_PROFILE, profile),
|
|
|
|
(ATTR_TRANSITION, transition),
|
|
|
|
(ATTR_BRIGHTNESS, brightness),
|
|
|
|
(ATTR_RGB_COLOR, rgb_color),
|
|
|
|
(ATTR_XY_COLOR, xy_color),
|
2015-10-28 23:12:16 +00:00
|
|
|
(ATTR_COLOR_TEMP, color_temp),
|
2014-12-09 07:02:38 +00:00
|
|
|
(ATTR_FLASH, flash),
|
2015-07-08 18:26:37 +00:00
|
|
|
(ATTR_EFFECT, effect),
|
2014-12-09 07:02:38 +00:00
|
|
|
] if value is not None
|
|
|
|
}
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-12-01 02:42:52 +00:00
|
|
|
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
2013-12-11 08:07:30 +00:00
|
|
|
|
|
|
|
|
2014-04-24 07:40:45 +00:00
|
|
|
def turn_off(hass, entity_id=None, transition=None):
|
2013-12-11 08:07:30 +00:00
|
|
|
""" Turns all or specified light off. """
|
2014-12-09 07:02:38 +00:00
|
|
|
data = {
|
|
|
|
key: value for key, value in [
|
|
|
|
(ATTR_ENTITY_ID, entity_id),
|
|
|
|
(ATTR_TRANSITION, transition),
|
|
|
|
] if value is not None
|
|
|
|
}
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-12-01 02:42:52 +00:00
|
|
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
2013-12-11 08:07:30 +00:00
|
|
|
|
|
|
|
|
2014-03-26 07:08:50 +00:00
|
|
|
# pylint: disable=too-many-branches, too-many-locals
|
2014-08-13 12:28:45 +00:00
|
|
|
def setup(hass, config):
|
2013-12-11 08:07:30 +00:00
|
|
|
""" Exposes light control via statemachine and services. """
|
|
|
|
|
2015-03-22 02:37:18 +00:00
|
|
|
component = EntityComponent(
|
2015-03-01 09:35:58 +00:00
|
|
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
|
|
|
GROUP_NAME_ALL_LIGHTS)
|
|
|
|
component.setup(config)
|
|
|
|
|
2014-11-26 07:16:07 +00:00
|
|
|
# Load built-in profiles and custom profiles
|
|
|
|
profile_paths = [os.path.join(os.path.dirname(__file__),
|
|
|
|
LIGHT_PROFILES_FILE),
|
2015-03-19 19:27:56 +00:00
|
|
|
hass.config.path(LIGHT_PROFILES_FILE)]
|
2014-11-26 07:16:07 +00:00
|
|
|
profiles = {}
|
|
|
|
|
|
|
|
for profile_path in profile_paths:
|
2015-08-03 15:42:28 +00:00
|
|
|
if not os.path.isfile(profile_path):
|
|
|
|
continue
|
|
|
|
with open(profile_path) 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 %s", profile_path)
|
|
|
|
|
|
|
|
return False
|
2014-11-26 07:16:07 +00:00
|
|
|
|
2014-01-05 01:55:05 +00:00
|
|
|
def handle_light_service(service):
|
2013-12-11 08:07:30 +00:00
|
|
|
""" Hande a turn light on or off service call. """
|
2014-03-16 22:00:59 +00:00
|
|
|
# Get and validate data
|
|
|
|
dat = service.data
|
|
|
|
|
2014-03-25 03:34:35 +00:00
|
|
|
# Convert the entity ids to valid light ids
|
2015-03-06 07:17:05 +00:00
|
|
|
target_lights = component.extract_from_service(service)
|
2014-03-16 22:00:59 +00:00
|
|
|
|
2014-11-26 05:28:43 +00:00
|
|
|
params = {}
|
|
|
|
|
2014-03-26 07:08:50 +00:00
|
|
|
transition = util.convert(dat.get(ATTR_TRANSITION), int)
|
2014-03-16 22:00:59 +00:00
|
|
|
|
2014-11-26 05:28:43 +00:00
|
|
|
if transition is not None:
|
|
|
|
params[ATTR_TRANSITION] = transition
|
|
|
|
|
2014-03-16 22:00:59 +00:00
|
|
|
if service.service == SERVICE_TURN_OFF:
|
2015-01-06 07:02:41 +00:00
|
|
|
for light in target_lights:
|
2014-11-26 05:28:43 +00:00
|
|
|
light.turn_off(**params)
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2015-08-04 16:13:55 +00:00
|
|
|
for light in target_lights:
|
|
|
|
if light.should_poll:
|
2015-08-03 15:42:28 +00:00
|
|
|
light.update_ha_state(True)
|
|
|
|
return
|
|
|
|
|
|
|
|
# Processing extra data for turn light on request
|
|
|
|
|
|
|
|
# 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 profile:
|
|
|
|
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile
|
|
|
|
|
|
|
|
if ATTR_BRIGHTNESS in dat:
|
|
|
|
# We pass in the old value as the default parameter if parsing
|
|
|
|
# of the new one goes wrong.
|
|
|
|
params[ATTR_BRIGHTNESS] = util.convert(
|
|
|
|
dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS))
|
|
|
|
|
|
|
|
if ATTR_XY_COLOR in dat:
|
|
|
|
try:
|
|
|
|
# xy_color should be a list containing 2 floats
|
|
|
|
xycolor = dat.get(ATTR_XY_COLOR)
|
|
|
|
|
|
|
|
# Without this check, a xycolor with value '99' would work
|
|
|
|
if not isinstance(xycolor, str):
|
|
|
|
params[ATTR_XY_COLOR] = [float(val) for val in xycolor]
|
|
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
# TypeError if xy_color is not iterable
|
|
|
|
# ValueError if value could not be converted to float
|
|
|
|
pass
|
|
|
|
|
2015-10-28 23:12:16 +00:00
|
|
|
if ATTR_COLOR_TEMP in dat:
|
|
|
|
# color_temp should be an int of mirads value
|
|
|
|
colortemp = dat.get(ATTR_COLOR_TEMP)
|
2015-10-27 22:34:49 +00:00
|
|
|
|
|
|
|
# Without this check, a ctcolor with value '99' would work
|
|
|
|
# These values are based on Philips Hue, may need ajustment later
|
2015-10-30 19:01:42 +00:00
|
|
|
if isinstance(colortemp, int) and 154 <= colortemp <= 500:
|
2015-10-30 19:15:38 +00:00
|
|
|
params[ATTR_COLOR_TEMP] = colortemp
|
2015-10-27 22:34:49 +00:00
|
|
|
|
2015-08-03 15:42:28 +00:00
|
|
|
if ATTR_RGB_COLOR in dat:
|
|
|
|
try:
|
|
|
|
# rgb_color should be a list containing 3 ints
|
|
|
|
rgb_color = dat.get(ATTR_RGB_COLOR)
|
|
|
|
|
|
|
|
if len(rgb_color) == 3:
|
2015-10-04 19:28:11 +00:00
|
|
|
params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color]
|
2015-08-03 15:42:28 +00:00
|
|
|
params[ATTR_XY_COLOR] = \
|
|
|
|
color_util.color_RGB_to_xy(int(rgb_color[0]),
|
|
|
|
int(rgb_color[1]),
|
|
|
|
int(rgb_color[2]))
|
|
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
# TypeError if rgb_color is not iterable
|
|
|
|
# ValueError if not all values can be converted to int
|
|
|
|
pass
|
|
|
|
|
|
|
|
if ATTR_FLASH in dat:
|
|
|
|
if dat[ATTR_FLASH] == FLASH_SHORT:
|
|
|
|
params[ATTR_FLASH] = FLASH_SHORT
|
|
|
|
|
|
|
|
elif dat[ATTR_FLASH] == FLASH_LONG:
|
|
|
|
params[ATTR_FLASH] = FLASH_LONG
|
|
|
|
|
|
|
|
if ATTR_EFFECT in dat:
|
|
|
|
if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
|
|
|
|
params[ATTR_EFFECT] = EFFECT_COLORLOOP
|
2015-07-08 18:26:37 +00:00
|
|
|
|
2015-08-03 15:42:28 +00:00
|
|
|
for light in target_lights:
|
|
|
|
light.turn_on(**params)
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2015-01-06 07:02:41 +00:00
|
|
|
for light in target_lights:
|
2015-06-13 23:42:09 +00:00
|
|
|
if light.should_poll:
|
|
|
|
light.update_ha_state(True)
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2014-03-15 07:24:28 +00:00
|
|
|
# Listen for light on and light off service calls
|
2015-09-27 06:17:04 +00:00
|
|
|
descriptions = load_yaml_config_file(
|
|
|
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
|
|
|
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
|
|
|
|
descriptions.get(SERVICE_TURN_ON))
|
2013-12-11 08:07:30 +00:00
|
|
|
|
2015-09-27 06:17:04 +00:00
|
|
|
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
|
|
|
descriptions.get(SERVICE_TURN_OFF))
|
2013-12-11 08:07:30 +00:00
|
|
|
|
|
|
|
return True
|
2015-06-13 23:42:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Light(ToggleEntity):
|
|
|
|
""" Represents a light within Home Assistant. """
|
|
|
|
# pylint: disable=no-self-use
|
|
|
|
|
|
|
|
@property
|
|
|
|
def brightness(self):
|
|
|
|
""" Brightness of this light between 0..255. """
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def color_xy(self):
|
|
|
|
""" XY color value [float, float]. """
|
|
|
|
return None
|
|
|
|
|
2015-10-27 22:34:49 +00:00
|
|
|
@property
|
2015-10-28 23:12:16 +00:00
|
|
|
def color_temp(self):
|
2015-10-27 22:34:49 +00:00
|
|
|
""" CT color value in mirads. """
|
|
|
|
return None
|
|
|
|
|
2015-06-13 23:42:09 +00:00
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
""" Returns device specific state attributes. """
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def state_attributes(self):
|
|
|
|
""" Returns optional state attributes. """
|
|
|
|
data = {}
|
|
|
|
|
|
|
|
if self.is_on:
|
|
|
|
for prop, attr in PROP_TO_ATTR.items():
|
|
|
|
value = getattr(self, prop)
|
|
|
|
if value:
|
|
|
|
data[attr] = value
|
|
|
|
|
|
|
|
device_attr = self.device_state_attributes
|
|
|
|
|
|
|
|
if device_attr is not None:
|
|
|
|
data.update(device_attr)
|
|
|
|
|
|
|
|
return data
|