Merge pull request #28919 from cro/hue_proxy_backport

Update Philips Hue proxy minion to support __proxy__ instead of proxymodule stored in __opts__
This commit is contained in:
Nicole Thomas 2015-11-21 08:31:36 -07:00
commit 27160b0454
3 changed files with 615 additions and 0 deletions

View file

@ -0,0 +1,52 @@
# -*- 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.
'''
Static grains for the Philips HUE lamps
.. versionadded:: 2015.8.3
'''
__proxyenabled__ = ['philips_hue']
__virtualname__ = 'hue'
def __virtual__():
if 'proxy' not in __opts__:
return False
else:
return __virtualname__
def kernel():
return {'kernel': 'RTOS'}
def os():
return {'os': 'FreeRTOS'}
def os_family():
return {'os_family': 'RTOS'}
def vendor():
return {'vendor': 'Philips'}
def product():
return {'product': 'HUE'}

View file

@ -0,0 +1,62 @@
# -*- 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
'''
from __future__ import absolute_import
import sys
__virtualname__ = 'hue'
__proxyenabled__ = ['philips_hue']
def _proxy():
'''
Get proxy.
'''
return __proxy__
def __virtual__():
'''
Start the Philips HUE only for proxies.
'''
if not _proxy():
return False
def _mkf(cmd_name, doc):
'''
Nested function to help move proxy functions into sys.modules
'''
def _cmd(*args, **kw):
'''
Call commands in proxy
'''
proxyfn = 'philips_hue.'+cmd_name
return __proxy__[proxyfn](*args, **kw)
return _cmd
import salt.proxy.philips_hue as hue
for method in dir(hue):
if method.startswith('call_'):
setattr(sys.modules[__name__], method[5:], _mkf(method, getattr(hue, method).__doc__))
del hue
return _proxy() and __virtualname__ or False

501
salt/proxy/philips_hue.py Normal file
View file

@ -0,0 +1,501 @@
# -*- 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
'''
from __future__ import absolute_import
# Import python libs
import logging
# INFO: This is going to be removed anytime soon in favor of salt.utils.http
# But it needs a separate PR!
try:
import requests
except ImportError as err:
requests = None
import time
import json
from salt.exceptions import (CommandExecutionError, MinionError)
__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 requests is not None
def init(cnf):
'''
Initialize the module.
'''
host = cnf.get('proxy', {}).get('host')
if not host:
raise MinionError(message="Cannot find 'host' parameter in the proxy configuration")
user = cnf.get('proxy', {}).get('user')
if not user:
raise MinionError(message="Cannot find 'user' parameter in the proxy configuration")
CONFIG['url'] = "http://{0}/api/{1}".format(host, 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 _set(lamp_id, state, method="state"):
'''
Set state to the device by ID.
:param lamp_id:
:param state:
:return:
'''
url = "{0}/lights/{1}".format(CONFIG['url'], lamp_id) + (method and "/{0}".format(method) or '')
res = None
try:
res = json.loads(requests.put(url, json=state).content)
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:
'''
try:
return json.loads(requests.get(CONFIG['url'] + "/lights").content)
except Exception as exc:
raise CommandExecutionError(exc)
# 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(str(dev_id)):
res[dev_id] = lights[str(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[str(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[str(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):
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