Refactored light to be more reusable
parent
ca336bef57
commit
47dea785a8
|
@ -116,6 +116,54 @@ def extract_entity_ids(hass, service):
|
|||
return entity_ids
|
||||
|
||||
|
||||
class ToggleDevice(object):
|
||||
""" ABC for devices that can be turned on and off. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
entity_id = None
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return None
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
pass
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
pass
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return False
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state from the real device. """
|
||||
pass
|
||||
|
||||
def update_ha_state(self, hass, force_refresh=False):
|
||||
"""
|
||||
Updates Home Assistant with current state of device.
|
||||
If force_refresh == True will update device before setting state.
|
||||
"""
|
||||
if self.entity_id is None:
|
||||
raise ha.NoEntitySpecifiedError(
|
||||
"No entity specified for device {}".format(self.get_name()))
|
||||
|
||||
if force_refresh:
|
||||
self.update()
|
||||
|
||||
state = STATE_ON if self.is_on() else STATE_OFF
|
||||
|
||||
return hass.states.set(self.entity_id, state,
|
||||
self.get_state_attributes())
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
""" Setup general services related to homeassistant. """
|
||||
|
|
|
@ -57,10 +57,9 @@ import csv
|
|||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components import (group, extract_entity_ids,
|
||||
STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
from homeassistant.components import (
|
||||
ToggleDevice, group, extract_entity_ids, STATE_ON,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
|
@ -90,6 +89,8 @@ ATTR_PROFILE = "profile"
|
|||
PHUE_CONFIG_FILE = "phue.conf"
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if the lights are on based on the statemachine. """
|
||||
|
@ -142,90 +143,42 @@ def turn_off(hass, entity_id=None, transition=None):
|
|||
def setup(hass, config):
|
||||
""" Exposes light control via statemachine and services. """
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger):
|
||||
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
|
||||
return False
|
||||
|
||||
light_type = config[DOMAIN][ha.CONF_TYPE]
|
||||
|
||||
if light_type == 'hue':
|
||||
light_init = HueLightControl
|
||||
light_init = get_hue_lights
|
||||
|
||||
else:
|
||||
logger.error("Unknown light type specified: %s", light_type)
|
||||
_LOGGER.error("Unknown light type specified: %s", light_type)
|
||||
|
||||
return False
|
||||
|
||||
light_control = light_init(hass, config[DOMAIN])
|
||||
lights = light_init(hass, config[DOMAIN])
|
||||
|
||||
if len(lights) == 0:
|
||||
_LOGGER.error("No lights found")
|
||||
return False
|
||||
|
||||
ent_to_light = {}
|
||||
light_to_ent = {}
|
||||
|
||||
def _update_light_state(light_id, light_state):
|
||||
""" Update statemachine based on the LightState passed in. """
|
||||
name = light_control.get_name(light_id) or "Unknown Light"
|
||||
no_name_count = 1
|
||||
|
||||
try:
|
||||
entity_id = light_to_ent[light_id]
|
||||
except KeyError:
|
||||
# We have not seen this light before, set it up
|
||||
for light in lights:
|
||||
name = light.get_name()
|
||||
|
||||
# Create entity id
|
||||
logger.info("Found new light %s", name)
|
||||
if name is None:
|
||||
name = "Light #{}".format(no_name_count)
|
||||
no_name_count += 1
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
list(ent_to_light.keys()))
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
list(ent_to_light.keys()))
|
||||
|
||||
ent_to_light[entity_id] = light_id
|
||||
light_to_ent[light_id] = entity_id
|
||||
|
||||
state_attr = {ATTR_FRIENDLY_NAME: name}
|
||||
|
||||
if light_state.on:
|
||||
state = STATE_ON
|
||||
|
||||
if light_state.brightness:
|
||||
state_attr[ATTR_BRIGHTNESS] = light_state.brightness
|
||||
|
||||
if light_state.color:
|
||||
state_attr[ATTR_XY_COLOR] = light_state.color
|
||||
|
||||
else:
|
||||
state = STATE_OFF
|
||||
|
||||
hass.states.set(entity_id, state, state_attr)
|
||||
|
||||
def update_light_state(light_id):
|
||||
""" Update the state of specified light. """
|
||||
_update_light_state(light_id, light_control.get(light_id))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_lights_state(time, force_reload=False):
|
||||
""" Update the state of all the lights. """
|
||||
|
||||
# First time this method gets called, force_reload should be True
|
||||
if force_reload or \
|
||||
datetime.now() - update_lights_state.last_updated > \
|
||||
MIN_TIME_BETWEEN_SCANS:
|
||||
|
||||
logger.info("Updating light status")
|
||||
update_lights_state.last_updated = datetime.now()
|
||||
|
||||
for light_id, light_state in light_control.gets().items():
|
||||
_update_light_state(light_id, light_state)
|
||||
|
||||
# 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_group(
|
||||
hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values(), False)
|
||||
light.entity_id = entity_id
|
||||
ent_to_light[entity_id] = light
|
||||
|
||||
# Load built-in profiles and custom profiles
|
||||
profile_paths = [os.path.join(os.path.dirname(__file__),
|
||||
|
@ -250,28 +203,41 @@ def setup(hass, config):
|
|||
except ValueError:
|
||||
# ValueError if not 4 values per row
|
||||
# ValueError if convert to float/int failed
|
||||
logger.error(
|
||||
_LOGGER.error(
|
||||
"Error parsing light profiles from %s", profile_path)
|
||||
|
||||
return False
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_lights_state(now):
|
||||
""" Update the states of all the lights. """
|
||||
for light in lights:
|
||||
light.update_ha_state(hass)
|
||||
|
||||
update_lights_state(None)
|
||||
|
||||
# Track all lights in a group
|
||||
group.setup_group(
|
||||
hass, GROUP_NAME_ALL_LIGHTS, ent_to_light.keys(), False)
|
||||
|
||||
def handle_light_service(service):
|
||||
""" Hande a turn light on or off service call. """
|
||||
# Get and validate data
|
||||
dat = service.data
|
||||
|
||||
# Convert the entity ids to valid light ids
|
||||
light_ids = [ent_to_light[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in ent_to_light]
|
||||
lights = [ent_to_light[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in ent_to_light]
|
||||
|
||||
if not light_ids:
|
||||
light_ids = list(ent_to_light.values())
|
||||
if not lights:
|
||||
lights = list(ent_to_light.values())
|
||||
|
||||
transition = util.convert(dat.get(ATTR_TRANSITION), int)
|
||||
|
||||
if service.service == SERVICE_TURN_OFF:
|
||||
light_control.turn_light_off(light_ids, transition)
|
||||
for light in lights:
|
||||
light.turn_off(transition=transition)
|
||||
|
||||
else:
|
||||
# Processing extra data for turn light on request
|
||||
|
@ -317,14 +283,12 @@ def setup(hass, config):
|
|||
# ValueError if not all values can be converted to int
|
||||
pass
|
||||
|
||||
light_control.turn_light_on(light_ids, transition, bright, color)
|
||||
for light in lights:
|
||||
light.turn_on(transition=transition, brightness=bright,
|
||||
xy_color=color)
|
||||
|
||||
# Update state of lights touched. If there was only 1 light selected
|
||||
# then just update that light else update all
|
||||
if len(light_ids) == 1:
|
||||
update_light_state(light_ids[0])
|
||||
else:
|
||||
update_lights_state(None, True)
|
||||
for light in lights:
|
||||
light.update_ha_state(hass, True)
|
||||
|
||||
# Update light state every 30 seconds
|
||||
hass.track_time_change(update_lights_state, second=[0, 30])
|
||||
|
@ -339,140 +303,134 @@ def setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
LightState = namedtuple("LightState", ['on', 'brightness', 'color'])
|
||||
def get_hue_lights(hass, config):
|
||||
""" Gets the Hue lights. """
|
||||
host = config.get(ha.CONF_HOST, None)
|
||||
|
||||
|
||||
def _hue_to_light_state(info):
|
||||
""" Helper method to convert a Hue state to a LightState. """
|
||||
try:
|
||||
return LightState(info['state']['reachable'] and info['state']['on'],
|
||||
info['state']['bri'], info['state']['xy'])
|
||||
except KeyError:
|
||||
# KeyError if one of the keys didn't exist
|
||||
return None
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.phue.phue as phue
|
||||
except ImportError:
|
||||
_LOGGER.exception("Hue:Error while importing dependency phue.")
|
||||
|
||||
return []
|
||||
|
||||
class HueLightControl(object):
|
||||
""" Class to interface with the Hue light system. """
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
|
||||
except socket.error: # Error connecting using Phue
|
||||
_LOGGER.exception((
|
||||
"Hue:Error while connecting to the bridge. "
|
||||
"Did you follow the instructions to set it up?"))
|
||||
|
||||
def __init__(self, hass, config):
|
||||
logger = logging.getLogger("{}.{}".format(__name__, "HueLightControl"))
|
||||
return []
|
||||
|
||||
host = config.get(ha.CONF_HOST, None)
|
||||
lights = {}
|
||||
|
||||
def update_lights(force_reload=False):
|
||||
""" Updates the light states. """
|
||||
now = datetime.now()
|
||||
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.phue.phue as phue
|
||||
except ImportError:
|
||||
logger.exception("Error while importing dependency phue.")
|
||||
time_scans = now - update_lights.last_updated
|
||||
|
||||
self.success_init = False
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
self._bridge = phue.Bridge(host,
|
||||
config_file_path=hass.get_config_path(
|
||||
PHUE_CONFIG_FILE))
|
||||
except socket.error: # Error connecting using Phue
|
||||
logger.exception((
|
||||
"Error while connecting to the bridge. "
|
||||
"Did you follow the instructions to set it up?"))
|
||||
|
||||
self.success_init = False
|
||||
|
||||
return
|
||||
|
||||
# Dict mapping light_id to name
|
||||
self._lights = {}
|
||||
self._update_lights()
|
||||
|
||||
if len(self._lights) == 0:
|
||||
logger.error("Could not find any lights. ")
|
||||
|
||||
self.success_init = False
|
||||
else:
|
||||
self.success_init = True
|
||||
|
||||
def _update_lights(self):
|
||||
""" Helper method to update the known names from Hue. """
|
||||
try:
|
||||
self._lights = {int(item[0]): item[1]['name'] for item
|
||||
in self._bridge.get_light().items()}
|
||||
|
||||
except (socket.error, KeyError):
|
||||
# socket.error because sometimes we cannot reach Hue
|
||||
# KeyError if we got unexpected data
|
||||
# We don't do anything, keep old values
|
||||
# force_reload == True, return if updated in last second
|
||||
# force_reload == False, return if last update was less then
|
||||
# MIN_TIME_BETWEEN_SCANS ago
|
||||
if force_reload and time_scans.seconds < 1 or \
|
||||
not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS:
|
||||
return
|
||||
except AttributeError:
|
||||
# First time we run last_updated is not set, continue as usual
|
||||
pass
|
||||
|
||||
def get_name(self, light_id):
|
||||
""" Return name for specified light_id or None if no name known. """
|
||||
if light_id not in self._lights:
|
||||
self._update_lights()
|
||||
|
||||
return self._lights.get(light_id)
|
||||
|
||||
def get(self, light_id):
|
||||
""" Return a LightState representing light light_id. """
|
||||
try:
|
||||
info = self._bridge.get_light(light_id)
|
||||
|
||||
return _hue_to_light_state(info)
|
||||
|
||||
except socket.error:
|
||||
# socket.error when we cannot reach Hue
|
||||
return None
|
||||
|
||||
def gets(self):
|
||||
""" Return a dict with id mapped to LightState objects. """
|
||||
states = {}
|
||||
update_lights.last_updated = now
|
||||
|
||||
try:
|
||||
api = self._bridge.get_api()
|
||||
|
||||
api = bridge.get_api()
|
||||
except socket.error:
|
||||
# socket.error when we cannot reach Hue
|
||||
return states
|
||||
_LOGGER.exception("Hue:Cannot reach the bridge")
|
||||
return
|
||||
|
||||
api_states = api.get('lights')
|
||||
|
||||
if not isinstance(api_states, dict):
|
||||
return states
|
||||
_LOGGER.error("Hue:Got unexpected result from Hue API")
|
||||
return
|
||||
|
||||
for light_id, info in api_states.items():
|
||||
state = _hue_to_light_state(info)
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
|
||||
if state:
|
||||
states[int(light_id)] = state
|
||||
update_lights()
|
||||
|
||||
return states
|
||||
return list(lights.values())
|
||||
|
||||
def turn_light_on(self, light_ids, transition, brightness, xy_color):
|
||||
|
||||
class HueLight(ToggleDevice):
|
||||
""" Represents a Hue light """
|
||||
|
||||
def __init__(self, light_id, info, bridge, update_lights):
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
|
||||
def get_name(self):
|
||||
""" Get the mame of the Hue light. """
|
||||
return self.info['name']
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the specified or all lights on. """
|
||||
command = {'on': True}
|
||||
|
||||
if transition is not None:
|
||||
if kwargs.get('transition') is not None:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, transition * 10)
|
||||
command['transitiontime'] = min(9000, kwargs['transition'] * 10)
|
||||
|
||||
if brightness is not None:
|
||||
command['bri'] = brightness
|
||||
if kwargs.get('brightness') is not None:
|
||||
command['bri'] = kwargs['brightness']
|
||||
|
||||
if xy_color:
|
||||
command['xy'] = xy_color
|
||||
if kwargs.get('xy_color') is not None:
|
||||
command['xy'] = kwargs['xy_color']
|
||||
|
||||
self._bridge.set_light(light_ids, command)
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def turn_light_off(self, light_ids, transition):
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the specified or all lights off. """
|
||||
command = {'on': False}
|
||||
|
||||
if transition is not None:
|
||||
if kwargs.get('transition') is not None:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, transition * 10)
|
||||
command['transitiontime'] = min(9000, kwargs['transition'] * 10)
|
||||
|
||||
self._bridge.set_light(light_ids, command)
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.update_lights()
|
||||
|
||||
return self.info['state']['reachable'] and self.info['state']['on']
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_FRIENDLY_NAME: self.get_name()
|
||||
}
|
||||
|
||||
if self.is_on():
|
||||
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
|
||||
attr[ATTR_XY_COLOR] = self.info['state']['xy']
|
||||
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
""" Synchronize state with bridge. """
|
||||
self.update_lights(True)
|
||||
|
|
|
@ -8,10 +8,10 @@ from datetime import datetime, timedelta
|
|||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components import (group, extract_entity_ids,
|
||||
STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
from homeassistant.components import (
|
||||
ToggleDevice, group, extract_entity_ids, STATE_ON,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
|
||||
DOMAIN = 'switch'
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
@ -143,48 +143,6 @@ def setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
class Switch(object):
|
||||
""" ABC for Switches within Home Assistant. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
entity_id = None
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return None
|
||||
|
||||
def turn_on(self, dimming=100):
|
||||
"""
|
||||
Turns the switch on.
|
||||
Dimming is a number between 0-100 and specifies how much switch has
|
||||
to be dimmed. There is no guarantee that the switch supports dimming.
|
||||
"""
|
||||
pass
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
pass
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return False
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return None
|
||||
|
||||
def update_ha_state(self, hass):
|
||||
""" Updates Home Assistant with its current state. """
|
||||
if self.entity_id is None:
|
||||
raise ha.NoEntitySpecifiedError(
|
||||
"No entity specified for switch {}".format(self.get_name()))
|
||||
|
||||
state = STATE_ON if self.is_on() else STATE_OFF
|
||||
|
||||
return hass.states.set(self.entity_id, state,
|
||||
self.get_state_attributes())
|
||||
|
||||
|
||||
def get_wemo_switches(config):
|
||||
""" Find and return WeMo switches. """
|
||||
|
||||
|
@ -213,7 +171,7 @@ def get_wemo_switches(config):
|
|||
if isinstance(switch, pywemo.Switch)]
|
||||
|
||||
|
||||
class WemoSwitch(Switch):
|
||||
class WemoSwitch(ToggleDevice):
|
||||
""" represents a WeMo switch within home assistant. """
|
||||
def __init__(self, wemo):
|
||||
self.wemo = wemo
|
||||
|
@ -223,7 +181,7 @@ class WemoSwitch(Switch):
|
|||
""" Returns the name of the switch if any. """
|
||||
return self.wemo.name
|
||||
|
||||
def turn_on(self, dimming=100):
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
||||
|
|
Loading…
Reference in New Issue