salt/proxy/philips_hue.py
# -*- coding: utf-8 -*-
#
# Copyright 2015 SUSE LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
Philips HUE lamps module for proxy.
.. versionadded:: 2015.8.3
First create a new user on the Hue bridge by following the
`Meet hue <https://www.developers.meethue.com/documentation/getting-started>`_ instructions.
To configure the proxy minion:
.. code-block:: yaml
proxy:
proxytype: philips_hue
host: [hostname or ip]
user: [username]
'''
# pylint: disable=import-error,no-name-in-module,redefined-builtin
from __future__ import absolute_import, print_function, unicode_literals
import salt.ext.six.moves.http_client as http_client
# Import python libs
import logging
import time
import salt.utils.json
from salt.exceptions import (CommandExecutionError, MinionError)
from salt.ext import six
__proxyenabled__ = ['philips_hue']
CONFIG = {}
log = logging.getLogger(__file__)
class Const(object):
'''
Constants for the lamp operations.
'''
LAMP_ON = {"on": True, "transitiontime": 0}
LAMP_OFF = {"on": False, "transitiontime": 0}
COLOR_WHITE = {"xy": [0.3227, 0.329]}
COLOR_DAYLIGHT = {"xy": [0.3806, 0.3576]}
COLOR_RED = {"hue": 0, "sat": 254}
COLOR_GREEN = {"hue": 25500, "sat": 254}
COLOR_ORANGE = {"hue": 12000, "sat": 254}
COLOR_PINK = {"xy": [0.3688, 0.2095]}
COLOR_BLUE = {"hue": 46920, "sat": 254}
COLOR_YELLOW = {"xy": [0.4432, 0.5154]}
COLOR_PURPLE = {"xy": [0.3787, 0.1724]}
def __virtual__():
'''
Validate the module.
'''
return True
def init(cnf):
'''
Initialize the module.
'''
CONFIG['host'] = cnf.get('proxy', {}).get('host')
if not CONFIG['host']:
raise MinionError(message="Cannot find 'host' parameter in the proxy configuration")
CONFIG['user'] = cnf.get('proxy', {}).get('user')
if not CONFIG['user']:
raise MinionError(message="Cannot find 'user' parameter in the proxy configuration")
CONFIG['uri'] = "/api/{0}".format(CONFIG['user'])
def ping(*args, **kw):
'''
Ping the lamps.
'''
# Here blink them
return True
def shutdown(opts, *args, **kw):
'''
Shuts down the service.
'''
# This is no-op method, which is required but makes nothing at this point.
return True
def _query(lamp_id, state, action='', method='GET'):
'''
Query the URI
:return:
'''
# Because salt.utils.query is that dreadful... :(
err = None
url = "{0}/lights{1}".format(CONFIG['uri'],
lamp_id and '/{0}'.format(lamp_id) or '') \
+ (action and "/{0}".format(action) or '')
conn = http_client.HTTPConnection(CONFIG['host'])
if method == 'PUT':
conn.request(method, url, salt.utils.json.dumps(state))
else:
conn.request(method, url)
resp = conn.getresponse()
if resp.status == http_client.OK:
res = salt.utils.json.loads(resp.read())
else:
err = "HTTP error: {0}, {1}".format(resp.status, resp.reason)
conn.close()
if err:
raise CommandExecutionError(err)
return res
def _set(lamp_id, state, method="state"):
'''
Set state to the device by ID.
:param lamp_id:
:param state:
:return:
'''
try:
res = _query(lamp_id, state, action=method, method='PUT')
except Exception as err:
raise CommandExecutionError(err)
res = len(res) > 1 and res[-1] or res[0]
if res.get('success'):
res = {'result': True}
elif res.get('error'):
res = {'result': False,
'description': res['error']['description'],
'type': res['error']['type']}
return res
def _get_devices(params):
'''
Parse device(s) ID(s) from the common params.
:param params:
:return:
'''
if 'id' not in params:
raise CommandExecutionError("Parameter ID is required.")
return type(params['id']) == int and [params['id']] \
or [int(dev) for dev in params['id'].split(",")]
def _get_lights():
'''
Get all available lighting devices.
'''
return _query(None, None)
# Callers
def call_lights(*args, **kwargs):
'''
Get info about all available lamps.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
CLI Example:
.. code-block:: bash
salt '*' hue.lights
salt '*' hue.lights id=1
salt '*' hue.lights id=1,2,3
'''
res = dict()
lights = _get_lights()
for dev_id in 'id' in kwargs and _get_devices(kwargs) or sorted(lights.keys()):
if lights.get(six.text_type(dev_id)):
res[dev_id] = lights[six.text_type(dev_id)]
return res or False
def call_switch(*args, **kwargs):
'''
Switch lamp ON/OFF.
If no particular state is passed,
then lamp will be switched to the opposite state.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
* **on**: True or False. Inverted current, if omitted
CLI Example:
.. code-block:: bash
salt '*' hue.switch
salt '*' hue.switch id=1
salt '*' hue.switch id=1,2,3 on=True
'''
out = dict()
devices = _get_lights()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
if 'on' in kwargs:
state = kwargs['on'] and Const.LAMP_ON or Const.LAMP_OFF
else:
# Invert the current state
state = devices[six.text_type(dev_id)]['state']['on'] and Const.LAMP_OFF or Const.LAMP_ON
out[dev_id] = _set(dev_id, state)
return out
def call_blink(*args, **kwargs):
'''
Blink a lamp. If lamp is ON, then blink ON-OFF-ON, otherwise OFF-ON-OFF.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
* **pause**: Time in seconds. Can be less than 1, i.e. 0.7, 0.5 sec.
CLI Example:
.. code-block:: bash
salt '*' hue.blink id=1
salt '*' hue.blink id=1,2,3
'''
devices = _get_lights()
pause = kwargs.get('pause', 0)
res = dict()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
state = devices[six.text_type(dev_id)]['state']['on']
_set(dev_id, state and Const.LAMP_OFF or Const.LAMP_ON)
if pause:
time.sleep(pause)
res[dev_id] = _set(dev_id, not state and Const.LAMP_OFF or Const.LAMP_ON)
return res
def call_ping(*args, **kwargs):
'''
Ping the lamps by issuing a short inversion blink to all available devices.
CLI Example:
.. code-block:: bash
salt '*' hue.ping
'''
errors = dict()
for dev_id, dev_status in call_blink().items():
if not dev_status['result']:
errors[dev_id] = False
return errors or True
def call_status(*args, **kwargs):
'''
Return the status of the lamps.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
CLI Example:
.. code-block:: bash
salt '*' hue.status
salt '*' hue.status id=1
salt '*' hue.status id=1,2,3
'''
res = dict()
devices = _get_lights()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
dev_id = six.text_type(dev_id)
res[dev_id] = {
'on': devices[dev_id]['state']['on'],
'reachable': devices[dev_id]['state']['reachable']
}
return res
def call_rename(*args, **kwargs):
'''
Rename a device.
Options:
* **id**: Specifies a device ID. Only one device at a time.
* **title**: Title of the device.
CLI Example:
.. code-block:: bash
salt '*' hue.rename id=1 title='WC for cats'
'''
dev_id = _get_devices(kwargs)
if len(dev_id) > 1:
raise CommandExecutionError("Only one device can be renamed at a time")
if 'title' not in kwargs:
raise CommandExecutionError("Title is missing")
return _set(dev_id[0], {"name": kwargs['title']}, method="")
def call_alert(*args, **kwargs):
'''
Lamp alert
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
* **on**: Turns on or off an alert. Default is True.
CLI Example:
.. code-block:: bash
salt '*' hue.alert
salt '*' hue.alert id=1
salt '*' hue.alert id=1,2,3 on=false
'''
res = dict()
devices = _get_lights()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
res[dev_id] = _set(dev_id, {"alert": kwargs.get("on", True) and "lselect" or "none"})
return res
def call_effect(*args, **kwargs):
'''
Set an effect to the lamp.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
* **type**: Type of the effect. Possible values are "none" or "colorloop". Default "none".
CLI Example:
.. code-block:: bash
salt '*' hue.effect
salt '*' hue.effect id=1
salt '*' hue.effect id=1,2,3 type=colorloop
'''
res = dict()
devices = _get_lights()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
res[dev_id] = _set(dev_id, {"effect": kwargs.get("type", "none")})
return res
def call_color(*args, **kwargs):
'''
Set a color to the lamp.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
* **color**: Fixed color. Values are: red, green, blue, orange, pink, white,
yellow, daylight, purple. Default white.
* **transition**: Transition 0~200.
Advanced:
* **gamut**: XY coordinates. Use gamut according to the Philips HUE devices documentation.
More: http://www.developers.meethue.com/documentation/hue-xy-values
CLI Example:
.. code-block:: bash
salt '*' hue.color
salt '*' hue.color id=1
salt '*' hue.color id=1,2,3 oolor=red transition=30
salt '*' hue.color id=1 gamut=0.3,0.5
'''
res = dict()
colormap = {
'red': Const.COLOR_RED,
'green': Const.COLOR_GREEN,
'blue': Const.COLOR_BLUE,
'orange': Const.COLOR_ORANGE,
'pink': Const.COLOR_PINK,
'white': Const.COLOR_WHITE,
'yellow': Const.COLOR_YELLOW,
'daylight': Const.COLOR_DAYLIGHT,
'purple': Const.COLOR_PURPLE,
}
devices = _get_lights()
color = kwargs.get("gamut")
if color:
color = color.split(",")
if len(color) == 2:
try:
color = {"xy": [float(color[0]), float(color[1])]}
except Exception as ex:
color = None
else:
color = None
if not color:
color = colormap.get(kwargs.get("color", 'white'), Const.COLOR_WHITE)
color.update({"transitiontime": max(min(kwargs.get("transition", 0), 200), 0)})
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
res[dev_id] = _set(dev_id, color)
return res
def call_brightness(*args, **kwargs):
'''
Set an effect to the lamp.
Arguments:
* **value**: 0~255 brightness of the lamp.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
* **transition**: Transition 0~200. Default 0.
CLI Example:
.. code-block:: bash
salt '*' hue.brightness value=100
salt '*' hue.brightness id=1 value=150
salt '*' hue.brightness id=1,2,3 value=255
'''
res = dict()
if 'value' not in kwargs:
raise CommandExecutionError("Parameter 'value' is missing")
try:
brightness = max(min(int(kwargs['value']), 244), 1)
except Exception as err:
raise CommandExecutionError("Parameter 'value' does not contains an integer")
try:
transition = max(min(int(kwargs['transition']), 200), 0)
except Exception as err:
transition = 0
devices = _get_lights()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
res[dev_id] = _set(dev_id, {"bri": brightness, "transitiontime": transition})
return res
def call_temperature(*args, **kwargs):
'''
Set the mired color temperature. More: http://en.wikipedia.org/wiki/Mired
Arguments:
* **value**: 150~500.
Options:
* **id**: Specifies a device ID. Can be a comma-separated values. All, if omitted.
CLI Example:
.. code-block:: bash
salt '*' hue.temperature value=150
salt '*' hue.temperature value=150 id=1
salt '*' hue.temperature value=150 id=1,2,3
'''
res = dict()
if 'value' not in kwargs:
raise CommandExecutionError("Parameter 'value' (150~500) is missing")
try:
value = max(min(int(kwargs['value']), 500), 150)
except Exception as err:
raise CommandExecutionError("Parameter 'value' does not contains an integer")
devices = _get_lights()
for dev_id in 'id' not in kwargs and sorted(devices.keys()) or _get_devices(kwargs):
res[dev_id] = _set(dev_id, {"ct": value})
return res