Skip to content

Commit fdbbe25

Browse files
committed
Merge branch 'devel' for publishing.
2 parents 643e419 + 3e7e854 commit fdbbe25

File tree

8 files changed

+258
-46
lines changed

8 files changed

+258
-46
lines changed

cozify/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.12"
1+
__version__ = "0.2.13"

cozify/hub.py

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
"""
77

88
import logging
9+
import math
910
from . import config
1011
from . import hub_api
1112
from enum import Enum
1213

1314

1415
from .Error import APIError
1516

16-
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')
17+
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')
1718

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

75-
def toggle(device_id, **kwargs):
75+
def device_toggle(device_id, **kwargs):
7676
"""Toggle power state of any device capable of it such as lamps. Eligibility is determined by the capability ON_OFF.
7777
7878
Args:
79-
device_id: ID of the device to toggle.
79+
device_id(str): ID of the device to toggle.
8080
**hub_id(str): optional id of hub to operate on. A specified hub_id takes presedence over a hub_name or default Hub.
8181
**hub_name(str): optional name of hub to operate on.
8282
**remote(bool): Remote or local query.
@@ -86,16 +86,131 @@ def toggle(device_id, **kwargs):
8686
# Get list of devices known to support toggle and find the device and it's state.
8787
devs = devices(capabilities=capability.ON_OFF, **kwargs)
8888
dev_state = devs[device_id]['state']
89-
current_state = dev_state['isOn']
89+
current_power = dev_state['isOn']
9090
new_state = _clean_state(dev_state)
91-
new_state['isOn'] = not current_state # reverse state
91+
new_state['isOn'] = not current_power # reverse power state
92+
hub_api.devices_command_state(device_id=device_id, state=new_state, **kwargs)
93+
94+
def device_on(device_id, **kwargs):
95+
"""Turn on a device that is capable of turning on. Eligibility is determined by the capability ON_OFF.
96+
97+
Args:
98+
device_id(str): ID of the device to operate on.
99+
"""
100+
_fill_kwargs(kwargs)
101+
if _is_eligible(device_id, capability.ON_OFF, **kwargs):
102+
hub_api.devices_command_on(device_id, **kwargs)
103+
else:
104+
raise AttributeError('Device not found or not eligible for action.')
105+
106+
def device_off(device_id, **kwargs):
107+
"""Turn off a device that is capable of turning off. Eligibility is determined by the capability ON_OFF.
108+
109+
Args:
110+
device_id(str): ID of the device to operate on.
111+
"""
112+
_fill_kwargs(kwargs)
113+
if _is_eligible(device_id, capability.ON_OFF, **kwargs):
114+
hub_api.devices_command_off(device_id, **kwargs)
115+
else:
116+
raise AttributeError('Device not found or not eligible for action.')
117+
118+
def light_temperature(device_id, temperature=2700, transition=0, **kwargs):
119+
"""Set temperature of a light.
120+
121+
Args:
122+
device_id(str): ID of the device to operate on.
123+
temperature(float): Temperature in Kelvins. If outside the operating range of the device the extreme value is used. Defaults to 2700K.
124+
transition(int): Transition length in milliseconds. Defaults to instant.
125+
"""
126+
_fill_kwargs(kwargs)
127+
state = {} # will be populated by _is_eligible
128+
if _is_eligible(device_id, capability.COLOR_TEMP, state=state, **kwargs):
129+
# Make sure temperature is within bounds [state.minTemperature, state.maxTemperature]
130+
minimum = state['minTemperature']
131+
maximum = state['maxTemperature']
132+
if temperature < minimum:
133+
logging.warn('Device does not support temperature {0}K, using minimum instead: {1}'.format(temperature, minimum))
134+
temperature = minimum
135+
elif temperature > maximum:
136+
logging.warn('Device does not support temperature {0}K, using maximum instead: {1}'.format(temperature, maximum))
137+
temperature = maximum
138+
139+
state = _clean_state(state)
140+
state['colorMode'] = 'ct'
141+
state['temperature'] = temperature
142+
state['transitionMsec'] = transition
143+
hub_api.devices_command_state(device_id=device_id, state=state, **kwargs)
144+
else:
145+
raise AttributeError('Device not found or not eligible for action.')
146+
147+
def light_color(device_id, hue, saturation=1.0, transition=0, **kwargs):
148+
"""Set color (hue & saturation) of a light.
149+
150+
Args:
151+
device_id(str): ID of the device to operate on.
152+
hue(float): Hue in the range of [0, Pi*2]. If outside the range an AttributeError is raised.
153+
saturation(float): Saturation in the range of [0, 1]. If outside the range an AttributeError is raised. Defaults to 1.0 (full saturation.)
154+
transition(int): Transition length in milliseconds. Defaults to instant.
155+
"""
156+
_fill_kwargs(kwargs)
157+
state = {} # will be populated by _is_eligible
158+
if _is_eligible(device_id, capability.COLOR_HS, state=state, **kwargs):
159+
# Make sure hue & saturation are within bounds
160+
if hue < 0 or hue > math.pi * 2:
161+
raise AttributeError('Hue out of bounds [0, pi*2]: {0}'.format(hue))
162+
elif saturation < 0 or saturation > 1.0:
163+
raise AttributeError('Saturation out of bounds [0, 1.0]: {0}'.format(saturation))
164+
165+
state = _clean_state(state)
166+
state['colorMode'] = 'hs'
167+
state['hue'] = hue
168+
state['saturation'] = saturation
169+
hub_api.devices_command_state(device_id=device_id, state=state, **kwargs)
170+
else:
171+
raise AttributeError('Device not found or not eligible for action.')
172+
173+
def light_brightness(device_id, brightness, transition=0, **kwargs):
174+
"""Set brightness of a light.
175+
176+
Args:
177+
device_id(str): ID of the device to operate on.
178+
brightness(float): Brightness in the range of [0, 1]. If outside the range an AttributeError is raised.
179+
transition(int): Transition length in milliseconds. Defaults to instant.
180+
"""
181+
_fill_kwargs(kwargs)
182+
state = {} # will be populated by _is_eligible
183+
if _is_eligible(device_id, capability.BRIGHTNESS, state=state, **kwargs):
184+
# Make sure hue & saturation are within bounds
185+
if brightness < 0 or brightness > 1.0:
186+
raise AttributeError('Brightness out of bounds [0, 1.0]: {0}'.format(brightness))
187+
188+
state = _clean_state(state)
189+
state['brightness'] = brightness
190+
hub_api.devices_command_state(device_id=device_id, state=state, **kwargs)
191+
else:
192+
raise AttributeError('Device not found or not eligible for action.')
193+
194+
def _is_eligible(device_id, capability_filter, devs=None, state=None, **kwargs):
195+
"""Check if device matches a AND devices filter.
196+
197+
Args:
198+
device_id(str): ID of the device to check.
199+
filter(hub.capability): Single hub.capability or a list of them to match against.
200+
devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live.
201+
state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible.
202+
Returns:
203+
bool: True if filter matches.
204+
"""
205+
if devs is None: # only retrieve if we didn't get them
206+
devs = devices(capabilities=capability_filter, **kwargs)
207+
if device_id in devs:
208+
state.update(devs[device_id]['state'])
209+
logging.debug('Implicitly returning state: {0}'.format(state))
210+
return True
211+
else:
212+
return False
92213

93-
command = {
94-
"type": "CMD_DEVICE",
95-
"id": device_id,
96-
"state": new_state
97-
}
98-
hub_api.devices_command(command, **kwargs)
99214

100215
def _get_id(**kwargs):
101216
"""Get a hub_id from various sources, meant so that you can just throw kwargs at it and get a valid id.
@@ -114,7 +229,7 @@ def _get_id(**kwargs):
114229
return kwargs['hubId']
115230
if 'hub_name' in kwargs or 'hubName' in kwargs:
116231
if 'hub_name' in kwargs:
117-
return getHubId(kwargs['hub_name'])
232+
return hub_id(kwargs['hub_name'])
118233
return getHubId(kwargs['hubName'])
119234
return default()
120235

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

177292
def getHubId(hub_name):
293+
"""Deprecated, use hub_id(). Return id of hub by it's name.
294+
295+
Args:
296+
hub_name(str): Name of hub to query. The name is given when registering a hub to an account.
297+
str: hub_id on success, raises an attributeerror on failure.
298+
299+
Returns:
300+
str: Hub id or raises
301+
"""
302+
logging.warn('hub.getHubId is deprecated and will be removed soon. Use hub.hub_id()')
303+
return hub_id(hub_name)
304+
305+
def hub_id(hub_name):
178306
"""Get hub id by it's name.
179307
180308
Args:
181309
hub_name(str): Name of hub to query. The name is given when registering a hub to an account.
182310
183311
Returns:
184-
str: Hub id or None if the hub wasn't found.
312+
str: hub_id on success, raises an attributeerror on failure.
185313
"""
186314

187315
for section in config.state.sections():
188316
if section.startswith("Hubs."):
189-
logging.debug('Found hub {0}'.format(section))
317+
logging.debug('Found hub: {0}'.format(section))
190318
if config.state[section]['hubname'] == hub_name:
191319
return section[5:] # cut out "Hubs."
192-
return None
320+
raise AttributeError('Hub not found: {0}'.format(hub_name))
193321

194322
def _getAttr(hub_id, attr, default=None, boolean=False):
195323
"""Get hub state attributes by attr name. Optionally set a default value if attribute not found.

cozify/hub_api.py

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@
1313

1414
apiPath = '/cc/1.8'
1515

16-
def _getBase(host, port=8893, api=apiPath):
17-
return 'http://%s:%s%s' % (host, port, api)
18-
19-
def _headers(hub_token):
20-
return { 'Authorization': hub_token }
16+
def _getBase(host, port=8893):
17+
return 'http://{0}:{1}'.format(host, port)
2118

2219
def get(call, hub_token_header=True, base=apiPath, **kwargs):
2320
"""GET method for calling hub API.
@@ -32,9 +29,8 @@ def get(call, hub_token_header=True, base=apiPath, **kwargs):
3229
**cloud_token(str): Cloud authentication token. Only needed if remote = True.
3330
"""
3431
return _call(method=requests.get,
35-
call=call,
32+
call='{0}{1}'.format(base, call),
3633
hub_token_header=hub_token_header,
37-
base=base,
3834
**kwargs
3935
)
4036

@@ -48,33 +44,38 @@ def put(call, payload, hub_token_header=True, base=apiPath, **kwargs):
4844
base(str): Base path to call from API instead of global apiPath. Defaults to apiPath.
4945
"""
5046
return _call(method=requests.put,
51-
call=call,
47+
call='{0}{1}'.format(base, call),
5248
hub_token_header=hub_token_header,
53-
base=base,
5449
payload=payload,
5550
**kwargs
5651
)
5752

58-
def _call(*, call, method, base, hub_token_header, payload=None, **kwargs):
53+
def _call(*, call, method, hub_token_header, payload=None, **kwargs):
5954
"""Backend for get & put
55+
56+
Args:
57+
call(str): Full API path to call.
58+
method(function): requests.get|put function to use for call.
6059
"""
6160
response = None
62-
headers = None
61+
headers = {}
6362
if hub_token_header:
64-
headers = _headers(kwargs['hub_token'])
63+
if 'hub_token' not in kwargs:
64+
raise AttributeError('Asked to do a call to the hub but no hub_token provided.')
65+
headers['Authorization'] = kwargs['hub_token']
66+
if payload is not None:
67+
headers['content-type'] = 'application/json'
6568

6669
if kwargs['remote']: # remote call
6770
if 'cloud_token' not in kwargs:
6871
raise AttributeError('Asked to do remote call but no cloud_token provided.')
6972
logging.debug('_call routing to cloud.remote()')
70-
response = cloud_api.remote(apicall=base + call, payload=payload, **kwargs)
73+
response = cloud_api.remote(apicall=call, payload=payload, **kwargs)
7174
else: # local call
7275
if not kwargs['host']:
7376
raise AttributeError('Local call but no hostname was provided. Either set keyword remote or host.')
74-
if hub_token_header:
75-
headers = _headers(kwargs['hub_token'])
7677
try:
77-
response = method(_getBase(host=kwargs['host'], api=base) + call, headers=headers, data=payload)
78+
response = method(_getBase(host=kwargs['host']) + call, headers=headers, data=payload)
7879
except RequestException as e:
7980
raise APIError('connection failure', 'issues connection to \'{0}\': {1}'.format(kwargs['host'], e))
8081

@@ -123,8 +124,63 @@ def devices_command(command, **kwargs):
123124
command(dict): dictionary of type DeviceData containing the changes wanted. Will be converted to json.
124125
125126
Returns:
126-
str: What ever the API replied or an APIException on failure.
127+
str: What ever the API replied or raises an APIEerror on failure.
127128
"""
128129
command = json.dumps(command)
129130
logging.debug('command json to send: {0}'.format(command))
130131
return put('/devices/command', command, **kwargs)
132+
133+
def devices_command_generic(*, device_id, command=None, request_type, **kwargs):
134+
"""Command helper for CMD type of actions.
135+
No checks are made wether the device supports the command or not. For kwargs see cozify.hub_api.put()
136+
137+
Args:
138+
device_id(str): ID of the device to operate on.
139+
request_type(str): Type of CMD to run, e.g. CMD_DEVICE_OFF
140+
command(dict): Optional dictionary to override command sent. Defaults to None which is interpreted as { device_id, type }
141+
Returns:
142+
str: What ever the API replied or raises an APIError on failure.
143+
"""
144+
if command is None:
145+
command = [{
146+
"id": device_id,
147+
"type": request_type
148+
}]
149+
return devices_command(command, **kwargs)
150+
151+
def devices_command_state(*, device_id, state, **kwargs):
152+
"""Command helper for CMD type of actions.
153+
No checks are made wether the device supports the command or not. For kwargs see cozify.hub_api.put()
154+
155+
Args:
156+
device_id(str): ID of the device to operate on.
157+
state(dict): New state dictionary containing changes.
158+
Returns:
159+
str: What ever the API replied or raises an APIError on failure.
160+
"""
161+
command = [{
162+
"id": device_id,
163+
"type": 'CMD_DEVICE',
164+
"state": state
165+
}]
166+
return devices_command(command, **kwargs)
167+
168+
def devices_command_on(device_id, **kwargs):
169+
"""Command helper for CMD_DEVICE_ON.
170+
171+
Args:
172+
device_id(str): ID of the device to operate on.
173+
Returns:
174+
str: What ever the API replied or raises an APIError on failure.
175+
"""
176+
return devices_command_generic(device_id=device_id, request_type='CMD_DEVICE_ON', **kwargs)
177+
178+
def devices_command_off(device_id, **kwargs):
179+
"""Command helper for CMD_DEVICE_OFF.
180+
181+
Args:
182+
device_id(str): ID of the device to operate on.
183+
Returns:
184+
str: What ever the API replied or raises an APIException on failure.
185+
"""
186+
return devices_command_generic(device_id=device_id, request_type='CMD_DEVICE_OFF', **kwargs)

cozify/test/test_hub.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_hub_id_to_name(tmp_hub):
1919
assert hub.name(tmp_hub.id) == tmp_hub.name
2020

2121
def test_hub_name_to_id(tmp_hub):
22-
assert hub.getHubId(tmp_hub.name) == tmp_hub.id
22+
assert hub.hub_id(tmp_hub.name) == tmp_hub.id
2323

2424
@pytest.mark.live
2525
def test_multisensor(live_hub):

util/device-off.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env python3
2+
from cozify import hub
3+
import pprint, sys
4+
5+
from cozify.test import debug
6+
7+
def main(device):
8+
hub.device_off(device)
9+
10+
if __name__ == "__main__":
11+
if len(sys.argv) > 1:
12+
main(sys.argv[1])
13+
else:
14+
sys.exit(1)

util/toggle.py renamed to util/device-on.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from cozify.test import debug
66

77
def main(device):
8-
hub.toggle(device)
8+
hub.device_on(device)
99

1010
if __name__ == "__main__":
1111
if len(sys.argv) > 1:

0 commit comments

Comments
 (0)