Skip to content

Commit 20902cf

Browse files
committed
Merge branch 'devel'
2 parents 743c5d6 + 93b97f9 commit 20902cf

File tree

12 files changed

+379
-169
lines changed

12 files changed

+379
-169
lines changed

README.rst

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,21 @@ And the expiry duration can be altered (also when calling cloud.ping()):
8686
# or
8787
cloud.ping(autorefresh=True, expiry=datetime.timedelta(days=20))
8888
89+
Sample projects
90+
---------------
91+
92+
- `github.com/Artanicus/cozify-temp <https://github.com/Artanicus/cozify-temp>`__
93+
- Store Multisensor data into InfluxDB
94+
- Take a look at the util/ directory for some crude small tools using the library that have been useful during development.
95+
- File an issue to get your project added here
96+
97+
Development
98+
-----------
99+
To develop python-cozify clone the devel branch and submit pull requests against the devel branch.
100+
New releases are cut from the devel branch as needed.
101+
89102
Tests
90-
-----
103+
~~~~~
91104
pytest is used for unit tests. Test coverage is still quite spotty and under active development.
92105
Certain tests are marked as "live" tests and require an active authentication state and a real hub to query against.
93106
Live tests are non-destructive.
@@ -109,22 +122,9 @@ To run the test suite on an already installed python-cozify:
109122
pytest -v --pyargs cozify --live
110123
111124
112-
Current limitations
113-
-------------------
114-
115-
- Token functionality is sanity-checked up to a point and renewal is
116-
attempted. This however is new code and may not be perfect.
117-
- For now there are only read calls. New API call requests are welcome
118-
as issues or pull requests!
119-
- authentication flow is as automatic as possible but if the Cozify
120-
Cloud token expires we can't help but request it and ask it to be
121-
entered. If you are running a daemon that requires authentication and
122-
your cloud token expires, run just the authenticate() flow in an
123-
interactive terminal and then restart your daemon.
125+
Roadmap, aka. Current Limitations
126+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
124127

125-
Sample projects
126-
---------------
127-
128-
- `github.com/Artanicus/cozify-temp <https://github.com/Artanicus/cozify-temp>`__
129-
- Store Multisensor data into InfluxDB
130-
- Report an issue to get your project added here
128+
- Authentication flow has been improved quite a bit but it would benefit a lot from real-world feedback.
129+
- For now there are only read calls. Next up is implementing ~all hub calls at the raw level and then wrapping them for ease of use. If there's something you want to use sooner than later file an issue so it can get prioritized!
130+
- Device model is non-existant and the old implementations are bad and deprecated. Active work ongoing to filter by capability at a low level first, then perhaps a more object oriented model on top of that.

cozify/__init__.py

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

cozify/cloud.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import logging, datetime
55

6-
from . import config as c
6+
from . import config
77
from . import hub
88
from . import hub_api
99
from . import cloud_api
@@ -60,7 +60,7 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
6060
raise
6161

6262
# save the successful cloud_token
63-
_setAttr('last_refresh', c._iso_now(), commit=False)
63+
_setAttr('last_refresh', config._iso_now(), commit=False)
6464
_setAttr('remoteToken', cloud_token, commit=True)
6565
else:
6666
# cloud_token already fine, let's just use it
@@ -110,11 +110,11 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
110110

111111
# if hub name not already known, create named section
112112
hubSection = 'Hubs.' + hub_id
113-
if hubSection not in c.state:
114-
c.state.add_section(hubSection)
113+
if hubSection not in config.state:
114+
config.state.add_section(hubSection)
115115
# if default hub not set, set this hub as the first as the default
116-
if 'default' not in c.state['Hubs']:
117-
c.state['Hubs']['default'] = hub_id
116+
if 'default' not in config.state['Hubs']:
117+
config.state['Hubs']['default'] = hub_id
118118

119119
# store Hub data under it's named section
120120
hub._setAttr(hub_id, 'host', hub_ip, commit=False)
@@ -129,8 +129,8 @@ def resetState():
129129
Hub state is left intact.
130130
"""
131131

132-
c.state['Cloud'] = {}
133-
c.stateWrite()
132+
config.state['Cloud'] = {}
133+
config.stateWrite()
134134

135135
def ping(autorefresh=True, expiry=None):
136136
"""Test cloud token validity. On success will also trigger a refresh if it's needed by the current key expiry.
@@ -181,7 +181,7 @@ def refresh(force=False, expiry=datetime.timedelta(days=1)):
181181
else:
182182
raise
183183
else:
184-
_setAttr('last_refresh', c._iso_now(), commit=False)
184+
_setAttr('last_refresh', config._iso_now(), commit=False)
185185
token(cloud_token)
186186
logging.info('cloud_token has been successfully refreshed.')
187187

@@ -230,8 +230,8 @@ def _need_cloud_token(trust=True):
230230
"""
231231

232232
# check if we've got a cloud_token before doing expensive checks
233-
if trust and 'remoteToken' in c.state['Cloud']:
234-
if c.state['Cloud']['remoteToken'] is None:
233+
if trust and 'remoteToken' in config.state['Cloud']:
234+
if config.state['Cloud']['remoteToken'] is None:
235235
return True
236236
else: # perform more expensive check
237237
return not ping()
@@ -251,7 +251,7 @@ def _need_hub_token(trust=True):
251251
return True
252252

253253
# First do quick checks, i.e. do we even have a token already
254-
if 'default' not in c.state['Hubs'] or 'hubtoken' not in c.state['Hubs.' + c.state['Hubs']['default']]:
254+
if 'default' not in config.state['Hubs'] or 'hubtoken' not in config.state['Hubs.' + config.state['Hubs']['default']]:
255255
logging.debug("We don't have a valid hubtoken or it's not trusted.")
256256
return True
257257
else: # if we have a token, we need to test if the API is callable
@@ -277,8 +277,8 @@ def _getAttr(attr):
277277
str: Value of attribute or exception on failure
278278
"""
279279
section = 'Cloud'
280-
if section in c.state and attr in c.state[section]:
281-
return c.state[section][attr]
280+
if section in config.state and attr in config.state[section]:
281+
return config.state[section][attr]
282282
else:
283283
logging.warning('Cloud attribute {0} not found in state.'.format(attr))
284284
raise AttributeError
@@ -292,12 +292,12 @@ def _setAttr(attr, value, commit=True):
292292
commit(bool): True to commit state after set. Defaults to True.
293293
"""
294294
section = 'Cloud'
295-
if section in c.state:
296-
if attr not in c.state[section]:
295+
if section in config.state:
296+
if attr not in config.state[section]:
297297
logging.info("Attribute {0} was not already in {1} state, new attribute created.".format(attr, section))
298-
c.state[section][attr] = value
298+
config.state[section][attr] = value
299299
if commit:
300-
c.stateWrite()
300+
config.stateWrite()
301301
else:
302302
logging.warning('Section {0} not found in state.'.format(section))
303303
raise AttributeError
@@ -308,7 +308,7 @@ def _isAttr(attr):
308308
Returns:
309309
bool: True if attribute exists
310310
"""
311-
return attr in c.state['Cloud'] and c.state['Cloud'][attr]
311+
return attr in config.state['Cloud'] and config.state['Cloud'][attr]
312312

313313
def token(new_token=None):
314314
"""Get currently used cloud_token or set a new one.

cozify/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import pytest
2+
from cozify.test.fixtures import *
3+
24
def pytest_addoption(parser):
35
parser.addoption("--live", action="store_true",
46
default=False, help="run tests requiring a functional auth and a real hub.")

cozify/hub.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99

1010
import requests, logging
11-
from . import config as c
11+
from . import config
1212
from . import cloud
1313
from . import hub_api
1414
from enum import Enum
@@ -19,7 +19,7 @@
1919
remote = False
2020
autoremote = True
2121

22-
capability = Enum('capability', 'BASS BRIGHTNESS COLOR_HS COLOR_LOOP COLOR_TEMP CONTACT DEVICE HUMIDITY LOUDNESS MUTE NEXT ON_OFF PAUSE PLAY PREVIOUS SEEK STOP TEMPERATURE TRANSITION TREBLE USER_PRESENCE VOLUME')
22+
capability = Enum('capability', 'ALERT BASS BRIGHTNESS COLOR_HS COLOR_LOOP COLOR_TEMP CONTACT DEVICE HUMIDITY LOUDNESS MUTE NEXT ON_OFF PAUSE PLAY PREVIOUS SEEK STOP TEMPERATURE TRANSITION TREBLE TWILIGHT USER_PRESENCE VOLUME')
2323

2424
def getDevices(**kwargs):
2525
"""Deprecated, will be removed in v0.3. Get up to date full devices data set as a dict.
@@ -35,6 +35,8 @@ def getDevices(**kwargs):
3535
dict: full live device state as returned by the API
3636
3737
"""
38+
cloud.authenticate() # the old version of getDevices did more than it was supposed to, including making sure there was a valid connection
39+
3840
hub_id = _get_id(**kwargs)
3941
hub_token = token(hub_id)
4042
cloud_token = cloud.token()
@@ -45,11 +47,12 @@ def getDevices(**kwargs):
4547

4648
return devices(capability=None, **kwargs)
4749

48-
def devices(*, capability=None, **kwargs):
50+
def devices(*, capabilities=None, and_filter=False, **kwargs):
4951
"""Get up to date full devices data set as a dict. Optionally can be filtered to only include certain devices.
5052
5153
Args:
52-
capability(cozify.hub.capability): Capability to filter by, for example: cozify.hub.capability.TEMPERATURE. Defaults to no filtering.
54+
capabilities(cozify.hub.capability): Single or list of cozify.hub.capability types to filter by, for example: [ cozify.hub.capability.TEMPERATURE, cozify.hub.capability.HUMIDITY ]. Defaults to no filtering.
55+
and_filter(bool): Multi-filter by AND instead of default OR. Defaults to False.
5356
**hub_name(str): optional name of hub to query. Will get converted to hubId for use.
5457
**hub_id(str): optional id of hub to query. A specified hub_id takes presedence over a hub_name or default Hub. Providing incorrect hub_id's will create cruft in your state but it won't hurt anything beyond failing the current operation.
5558
**remote(bool): Remote or local query.
@@ -64,11 +67,20 @@ def devices(*, capability=None, **kwargs):
6467
hub_token = token(hub_id)
6568
cloud_token = cloud.token()
6669
hostname = host(hub_id)
70+
if remote not in kwargs:
71+
kwargs['remote'] = remote
6772

68-
devs = hub_api.devices(host=hostname, hub_token=hub_token, remote=remote, cloud_token=cloud_token)
69-
if capability:
70-
return { key : value for key, value in devs.items() if capability.name in value['capabilities']['values'] }
71-
else:
73+
devs = hub_api.devices(host=hostname, hub_token=hub_token, cloud_token=cloud_token, **kwargs)
74+
if capabilities:
75+
if isinstance(capabilities, capability): # single capability given
76+
logging.debug("single capability {0}".format(capabilities.name))
77+
return { key : value for key, value in devs.items() if capabilities.name in value['capabilities']['values'] }
78+
else: # multi-filter
79+
if and_filter:
80+
return { key : value for key, value in devs.items() if all(c.name in value['capabilities']['values'] for c in capabilities) }
81+
else: # or_filter
82+
return { key : value for key, value in devs.items() if any(c.name in value['capabilities']['values'] for c in capabilities) }
83+
else: # no filtering
7284
return devs
7385

7486
def _get_id(**kwargs):
@@ -98,11 +110,11 @@ def getDefaultHub():
98110
If default hub isn't known, run authentication to make it known.
99111
"""
100112

101-
if 'default' not in c.state['Hubs']:
113+
if 'default' not in config.state['Hubs']:
102114
logging.critical('no hub name given and no default known, you should run cozify.authenticate()')
103115
raise AttributeError
104116
else:
105-
return c.state['Hubs']['default']
117+
return config.state['Hubs']['default']
106118

107119
def getHubId(hub_name):
108120
"""Get hub id by it's name.
@@ -114,10 +126,10 @@ def getHubId(hub_name):
114126
str: Hub id or None if the hub wasn't found.
115127
"""
116128

117-
for section in c.state.sections():
129+
for section in config.state.sections():
118130
if section.startswith("Hubs."):
119131
logging.debug('Found hub {0}'.format(section))
120-
if c.state[section]['hubname'] == hub_name:
132+
if config.state[section]['hubname'] == hub_name:
121133
return section[5:] # cut out "Hubs."
122134
return None
123135

@@ -131,8 +143,8 @@ def _getAttr(hub_id, attr):
131143
str: Value of attribute or exception on failure.
132144
"""
133145
section = 'Hubs.' + hub_id
134-
if section in c.state and attr in c.state[section]:
135-
return c.state[section][attr]
146+
if section in config.state and attr in config.state[section]:
147+
return config.state[section][attr]
136148
else:
137149
logging.warning('Hub id "{0}" not found in state or attribute {1} not set for hub.'.format(hub_id, attr))
138150
raise AttributeError
@@ -147,12 +159,12 @@ def _setAttr(hub_id, attr, value, commit=True):
147159
commit(bool): True to commit state after set. Defaults to True.
148160
"""
149161
section = 'Hubs.' + hub_id
150-
if section in c.state:
151-
if attr not in c.state[section]:
162+
if section in config.state:
163+
if attr not in config.state[section]:
152164
logging.info("Attribute {0} was not already in {1} state, new attribute created.".format(attr, section))
153-
c.state[section][attr] = value
165+
config.state[section][attr] = value
154166
if commit:
155-
c.stateWrite()
167+
config.stateWrite()
156168
else:
157169
logging.warning('Section {0} not found in state.'.format(section))
158170
raise AttributeError
@@ -176,7 +188,7 @@ def host(hub_id):
176188
hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub.
177189
178190
Returns:
179-
str: ip address of matching hub. Be aware that this may be empty if the hub is only known remotely and will still give you an ip address even if the hub is currently remote.
191+
str: ip address of matching hub. Be aware that this may be empty if the hub is only known remotely and will still give you an ip address even if the hub is currently remote and an ip address was previously locally known.
180192
"""
181193
return _getAttr(hub_id, 'host')
182194

@@ -213,7 +225,7 @@ def ping(hub_id=None, hub_name=None, **kwargs):
213225
config_name = 'Hubs.' + hub_id
214226
hub_token = _getAttr(hub_id, 'hubtoken')
215227
hub_host = _getAttr(hub_id, 'host')
216-
cloud_token = c.state['Cloud']['remotetoken']
228+
cloud_token = config.state['Cloud']['remotetoken']
217229

218230
# if we don't have a stored host then we assume the hub is remote
219231
global remote

cozify/hub_api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from .Error import APIError
1212

13-
apiPath = '/cc/1.7'
13+
apiPath = '/cc/1.8'
1414

1515
def _getBase(host, port=8893, api=apiPath):
1616
return 'http://%s:%s%s' % (host, port, api)
@@ -92,11 +92,17 @@ def tz(**kwargs):
9292
return get('/hub/tz', **kwargs)
9393

9494
def devices(**kwargs):
95-
"""1:1 implementation of /devices API call. For kwargs see cozify.hub_api.get()
95+
"""1:1 implementation of /devices API call. For remaining kwargs see cozify.hub_api.get()
96+
97+
Args:
98+
**mock_devices(dict): If defined, returned as-is as if that were the result we received.
9699
97100
Returns:
98-
json: Full live device state as returned by the API
101+
dict: Full live device state as returned by the API
99102
"""
103+
if 'mock_devices' in kwargs:
104+
return kwargs['mock_devices']
105+
100106
return get('/devices', **kwargs)
101107

102108
def devices_command(command, **kwargs):

0 commit comments

Comments
 (0)