Skip to content

Commit

Permalink
Merge branch 'devel' for publishing.
Browse files Browse the repository at this point in the history
  • Loading branch information
jinnatar committed Mar 12, 2018
2 parents 643e419 + 3e7e854 commit fdbbe25
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 46 deletions.
2 changes: 1 addition & 1 deletion cozify/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.12"
__version__ = "0.2.13"
160 changes: 144 additions & 16 deletions cozify/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
"""

import logging
import math
from . import config
from . import hub_api
from enum import Enum


from .Error import APIError

capability = Enum('capability', 'ALERT BASS BATTERY_U BRIGHTNESS COLOR_HS COLOR_LOOP COLOR_TEMP CONTACT CONTROL_LIGHT CONTROL_POWER DEVICE DIMMER_CONTROL GENERATE_ALERT HUMIDITY IDENTIFY LOUDNESS MOISTURE MUTE NEXT ON_OFF PAUSE PLAY PREVIOUS PUSH_NOTIFICATION REMOTE_CONTROL SEEK SMOKE STOP TEMPERATURE TRANSITION TREBLE TWILIGHT USER_PRESENCE VOLUME')
capability = Enum('capability', 'ALERT BASS BATTERY_U BRIGHTNESS COLOR_HS COLOR_LOOP COLOR_TEMP CONTACT CONTROL_LIGHT CONTROL_POWER DEVICE DIMMER_CONTROL GENERATE_ALERT HUMIDITY IDENTIFY LOUDNESS LUX MOISTURE MOTION MUTE NEXT ON_OFF PAUSE PLAY PREVIOUS PUSH_NOTIFICATION REMOTE_CONTROL SEEK SMOKE STOP TEMPERATURE TRANSITION TREBLE TWILIGHT USER_PRESENCE VOLUME')

def getDevices(**kwargs):
"""Deprecated, will be removed in v0.3. Get up to date full devices data set as a dict.
Expand Down Expand Up @@ -62,7 +63,6 @@ def devices(*, capabilities=None, and_filter=False, **kwargs):
devs = hub_api.devices(**kwargs)
if capabilities:
if isinstance(capabilities, capability): # single capability given
logging.debug("single capability {0}".format(capabilities.name))
return { key : value for key, value in devs.items() if capabilities.name in value['capabilities']['values'] }
else: # multi-filter
if and_filter:
Expand All @@ -72,11 +72,11 @@ def devices(*, capabilities=None, and_filter=False, **kwargs):
else: # no filtering
return devs

def toggle(device_id, **kwargs):
def device_toggle(device_id, **kwargs):
"""Toggle power state of any device capable of it such as lamps. Eligibility is determined by the capability ON_OFF.
Args:
device_id: ID of the device to toggle.
device_id(str): ID of the device to toggle.
**hub_id(str): optional id of hub to operate on. A specified hub_id takes presedence over a hub_name or default Hub.
**hub_name(str): optional name of hub to operate on.
**remote(bool): Remote or local query.
Expand All @@ -86,16 +86,131 @@ def toggle(device_id, **kwargs):
# Get list of devices known to support toggle and find the device and it's state.
devs = devices(capabilities=capability.ON_OFF, **kwargs)
dev_state = devs[device_id]['state']
current_state = dev_state['isOn']
current_power = dev_state['isOn']
new_state = _clean_state(dev_state)
new_state['isOn'] = not current_state # reverse state
new_state['isOn'] = not current_power # reverse power state
hub_api.devices_command_state(device_id=device_id, state=new_state, **kwargs)

def device_on(device_id, **kwargs):
"""Turn on a device that is capable of turning on. Eligibility is determined by the capability ON_OFF.
Args:
device_id(str): ID of the device to operate on.
"""
_fill_kwargs(kwargs)
if _is_eligible(device_id, capability.ON_OFF, **kwargs):
hub_api.devices_command_on(device_id, **kwargs)
else:
raise AttributeError('Device not found or not eligible for action.')

def device_off(device_id, **kwargs):
"""Turn off a device that is capable of turning off. Eligibility is determined by the capability ON_OFF.
Args:
device_id(str): ID of the device to operate on.
"""
_fill_kwargs(kwargs)
if _is_eligible(device_id, capability.ON_OFF, **kwargs):
hub_api.devices_command_off(device_id, **kwargs)
else:
raise AttributeError('Device not found or not eligible for action.')

def light_temperature(device_id, temperature=2700, transition=0, **kwargs):
"""Set temperature of a light.
Args:
device_id(str): ID of the device to operate on.
temperature(float): Temperature in Kelvins. If outside the operating range of the device the extreme value is used. Defaults to 2700K.
transition(int): Transition length in milliseconds. Defaults to instant.
"""
_fill_kwargs(kwargs)
state = {} # will be populated by _is_eligible
if _is_eligible(device_id, capability.COLOR_TEMP, state=state, **kwargs):
# Make sure temperature is within bounds [state.minTemperature, state.maxTemperature]
minimum = state['minTemperature']
maximum = state['maxTemperature']
if temperature < minimum:
logging.warn('Device does not support temperature {0}K, using minimum instead: {1}'.format(temperature, minimum))
temperature = minimum
elif temperature > maximum:
logging.warn('Device does not support temperature {0}K, using maximum instead: {1}'.format(temperature, maximum))
temperature = maximum

state = _clean_state(state)
state['colorMode'] = 'ct'
state['temperature'] = temperature
state['transitionMsec'] = transition
hub_api.devices_command_state(device_id=device_id, state=state, **kwargs)
else:
raise AttributeError('Device not found or not eligible for action.')

def light_color(device_id, hue, saturation=1.0, transition=0, **kwargs):
"""Set color (hue & saturation) of a light.
Args:
device_id(str): ID of the device to operate on.
hue(float): Hue in the range of [0, Pi*2]. If outside the range an AttributeError is raised.
saturation(float): Saturation in the range of [0, 1]. If outside the range an AttributeError is raised. Defaults to 1.0 (full saturation.)
transition(int): Transition length in milliseconds. Defaults to instant.
"""
_fill_kwargs(kwargs)
state = {} # will be populated by _is_eligible
if _is_eligible(device_id, capability.COLOR_HS, state=state, **kwargs):
# Make sure hue & saturation are within bounds
if hue < 0 or hue > math.pi * 2:
raise AttributeError('Hue out of bounds [0, pi*2]: {0}'.format(hue))
elif saturation < 0 or saturation > 1.0:
raise AttributeError('Saturation out of bounds [0, 1.0]: {0}'.format(saturation))

state = _clean_state(state)
state['colorMode'] = 'hs'
state['hue'] = hue
state['saturation'] = saturation
hub_api.devices_command_state(device_id=device_id, state=state, **kwargs)
else:
raise AttributeError('Device not found or not eligible for action.')

def light_brightness(device_id, brightness, transition=0, **kwargs):
"""Set brightness of a light.
Args:
device_id(str): ID of the device to operate on.
brightness(float): Brightness in the range of [0, 1]. If outside the range an AttributeError is raised.
transition(int): Transition length in milliseconds. Defaults to instant.
"""
_fill_kwargs(kwargs)
state = {} # will be populated by _is_eligible
if _is_eligible(device_id, capability.BRIGHTNESS, state=state, **kwargs):
# Make sure hue & saturation are within bounds
if brightness < 0 or brightness > 1.0:
raise AttributeError('Brightness out of bounds [0, 1.0]: {0}'.format(brightness))

state = _clean_state(state)
state['brightness'] = brightness
hub_api.devices_command_state(device_id=device_id, state=state, **kwargs)
else:
raise AttributeError('Device not found or not eligible for action.')

def _is_eligible(device_id, capability_filter, devs=None, state=None, **kwargs):
"""Check if device matches a AND devices filter.
Args:
device_id(str): ID of the device to check.
filter(hub.capability): Single hub.capability or a list of them to match against.
devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live.
state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible.
Returns:
bool: True if filter matches.
"""
if devs is None: # only retrieve if we didn't get them
devs = devices(capabilities=capability_filter, **kwargs)
if device_id in devs:
state.update(devs[device_id]['state'])
logging.debug('Implicitly returning state: {0}'.format(state))
return True
else:
return False

command = {
"type": "CMD_DEVICE",
"id": device_id,
"state": new_state
}
hub_api.devices_command(command, **kwargs)

def _get_id(**kwargs):
"""Get a hub_id from various sources, meant so that you can just throw kwargs at it and get a valid id.
Expand All @@ -114,7 +229,7 @@ def _get_id(**kwargs):
return kwargs['hubId']
if 'hub_name' in kwargs or 'hubName' in kwargs:
if 'hub_name' in kwargs:
return getHubId(kwargs['hub_name'])
return hub_id(kwargs['hub_name'])
return getHubId(kwargs['hubName'])
return default()

Expand Down Expand Up @@ -175,21 +290,34 @@ def default():
return config.state['Hubs']['default']

def getHubId(hub_name):
"""Deprecated, use hub_id(). Return id of hub by it's name.
Args:
hub_name(str): Name of hub to query. The name is given when registering a hub to an account.
str: hub_id on success, raises an attributeerror on failure.
Returns:
str: Hub id or raises
"""
logging.warn('hub.getHubId is deprecated and will be removed soon. Use hub.hub_id()')
return hub_id(hub_name)

def hub_id(hub_name):
"""Get hub id by it's name.
Args:
hub_name(str): Name of hub to query. The name is given when registering a hub to an account.
Returns:
str: Hub id or None if the hub wasn't found.
str: hub_id on success, raises an attributeerror on failure.
"""

for section in config.state.sections():
if section.startswith("Hubs."):
logging.debug('Found hub {0}'.format(section))
logging.debug('Found hub: {0}'.format(section))
if config.state[section]['hubname'] == hub_name:
return section[5:] # cut out "Hubs."
return None
raise AttributeError('Hub not found: {0}'.format(hub_name))

def _getAttr(hub_id, attr, default=None, boolean=False):
"""Get hub state attributes by attr name. Optionally set a default value if attribute not found.
Expand Down
90 changes: 73 additions & 17 deletions cozify/hub_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@

apiPath = '/cc/1.8'

def _getBase(host, port=8893, api=apiPath):
return 'http://%s:%s%s' % (host, port, api)

def _headers(hub_token):
return { 'Authorization': hub_token }
def _getBase(host, port=8893):
return 'http://{0}:{1}'.format(host, port)

def get(call, hub_token_header=True, base=apiPath, **kwargs):
"""GET method for calling hub API.
Expand All @@ -32,9 +29,8 @@ def get(call, hub_token_header=True, base=apiPath, **kwargs):
**cloud_token(str): Cloud authentication token. Only needed if remote = True.
"""
return _call(method=requests.get,
call=call,
call='{0}{1}'.format(base, call),
hub_token_header=hub_token_header,
base=base,
**kwargs
)

Expand All @@ -48,33 +44,38 @@ def put(call, payload, hub_token_header=True, base=apiPath, **kwargs):
base(str): Base path to call from API instead of global apiPath. Defaults to apiPath.
"""
return _call(method=requests.put,
call=call,
call='{0}{1}'.format(base, call),
hub_token_header=hub_token_header,
base=base,
payload=payload,
**kwargs
)

def _call(*, call, method, base, hub_token_header, payload=None, **kwargs):
def _call(*, call, method, hub_token_header, payload=None, **kwargs):
"""Backend for get & put
Args:
call(str): Full API path to call.
method(function): requests.get|put function to use for call.
"""
response = None
headers = None
headers = {}
if hub_token_header:
headers = _headers(kwargs['hub_token'])
if 'hub_token' not in kwargs:
raise AttributeError('Asked to do a call to the hub but no hub_token provided.')
headers['Authorization'] = kwargs['hub_token']
if payload is not None:
headers['content-type'] = 'application/json'

if kwargs['remote']: # remote call
if 'cloud_token' not in kwargs:
raise AttributeError('Asked to do remote call but no cloud_token provided.')
logging.debug('_call routing to cloud.remote()')
response = cloud_api.remote(apicall=base + call, payload=payload, **kwargs)
response = cloud_api.remote(apicall=call, payload=payload, **kwargs)
else: # local call
if not kwargs['host']:
raise AttributeError('Local call but no hostname was provided. Either set keyword remote or host.')
if hub_token_header:
headers = _headers(kwargs['hub_token'])
try:
response = method(_getBase(host=kwargs['host'], api=base) + call, headers=headers, data=payload)
response = method(_getBase(host=kwargs['host']) + call, headers=headers, data=payload)
except RequestException as e:
raise APIError('connection failure', 'issues connection to \'{0}\': {1}'.format(kwargs['host'], e))

Expand Down Expand Up @@ -123,8 +124,63 @@ def devices_command(command, **kwargs):
command(dict): dictionary of type DeviceData containing the changes wanted. Will be converted to json.
Returns:
str: What ever the API replied or an APIException on failure.
str: What ever the API replied or raises an APIEerror on failure.
"""
command = json.dumps(command)
logging.debug('command json to send: {0}'.format(command))
return put('/devices/command', command, **kwargs)

def devices_command_generic(*, device_id, command=None, request_type, **kwargs):
"""Command helper for CMD type of actions.
No checks are made wether the device supports the command or not. For kwargs see cozify.hub_api.put()
Args:
device_id(str): ID of the device to operate on.
request_type(str): Type of CMD to run, e.g. CMD_DEVICE_OFF
command(dict): Optional dictionary to override command sent. Defaults to None which is interpreted as { device_id, type }
Returns:
str: What ever the API replied or raises an APIError on failure.
"""
if command is None:
command = [{
"id": device_id,
"type": request_type
}]
return devices_command(command, **kwargs)

def devices_command_state(*, device_id, state, **kwargs):
"""Command helper for CMD type of actions.
No checks are made wether the device supports the command or not. For kwargs see cozify.hub_api.put()
Args:
device_id(str): ID of the device to operate on.
state(dict): New state dictionary containing changes.
Returns:
str: What ever the API replied or raises an APIError on failure.
"""
command = [{
"id": device_id,
"type": 'CMD_DEVICE',
"state": state
}]
return devices_command(command, **kwargs)

def devices_command_on(device_id, **kwargs):
"""Command helper for CMD_DEVICE_ON.
Args:
device_id(str): ID of the device to operate on.
Returns:
str: What ever the API replied or raises an APIError on failure.
"""
return devices_command_generic(device_id=device_id, request_type='CMD_DEVICE_ON', **kwargs)

def devices_command_off(device_id, **kwargs):
"""Command helper for CMD_DEVICE_OFF.
Args:
device_id(str): ID of the device to operate on.
Returns:
str: What ever the API replied or raises an APIException on failure.
"""
return devices_command_generic(device_id=device_id, request_type='CMD_DEVICE_OFF', **kwargs)
2 changes: 1 addition & 1 deletion cozify/test/test_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_hub_id_to_name(tmp_hub):
assert hub.name(tmp_hub.id) == tmp_hub.name

def test_hub_name_to_id(tmp_hub):
assert hub.getHubId(tmp_hub.name) == tmp_hub.id
assert hub.hub_id(tmp_hub.name) == tmp_hub.id

@pytest.mark.live
def test_multisensor(live_hub):
Expand Down
14 changes: 14 additions & 0 deletions util/device-off.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
from cozify import hub
import pprint, sys

from cozify.test import debug

def main(device):
hub.device_off(device)

if __name__ == "__main__":
if len(sys.argv) > 1:
main(sys.argv[1])
else:
sys.exit(1)
2 changes: 1 addition & 1 deletion util/toggle.py → util/device-on.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from cozify.test import debug

def main(device):
hub.toggle(device)
hub.device_on(device)

if __name__ == "__main__":
if len(sys.argv) > 1:
Expand Down
Loading

0 comments on commit fdbbe25

Please sign in to comment.