Skip to content

Commit 643e419

Browse files
committed
Merge branch 'devel' for release as v0.2.12
2 parents 20902cf + cce24ab commit 643e419

23 files changed

Lines changed: 392 additions & 197 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ MANIFEST
55
dist/
66
build/
77
.cache/
8+
.pytest_cache/

README.rst

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ Basic usage
2727
-----------
2828
These are merely some simple examples, for the full documentation see: `http://python-cozify.readthedocs.io/en/latest/`
2929

30-
read devices, extract multisensor data
31-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30+
read devices by capability, print temperature data
31+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3232

3333
.. code:: python
3434
35-
from cozify import hub, multisensor
36-
devices = hub.getDevices()
37-
print(multisensor.getMultisensorData(devices))
35+
from cozify import hub
36+
devices = hub.devices(capabilities=hub.capability.TEMPERATURE)
37+
for id, dev in devices.items():
38+
print('{0}: {1}C'.format(dev['name'], dev['state']['temperature']))
3839
3940
only authenticate
4041
~~~~~~~~~~~~~~~~~
@@ -57,6 +58,24 @@ authenticate with a non-default state storage
5758
# authentication and other useful data is now stored in the defined location instead of ~/.config/python-cozify/python-cozify.cfg
5859
# you could also use the environment variable XDG_CONFIG_HOME to override where config files are stored
5960
61+
On Capabilities
62+
---------------
63+
The most practical way to "find" devices for operating on is currently to filter the devices list by their capabilties. The
64+
most up to date list of recognized capabilities can be seen at `cozify/hub.py <cozify/hub.py#L21>`_
65+
66+
If the capability you need is not yet supported, open a bug to get it added. One way to compare your live hub device's capabilities
67+
to those implemented is running the util/capabilities_list.py tool. It will list implemented and gathered capabilities from your live environment.
68+
To get all of your previously unknown capabilities implemented, just copy-paste the full output of the utility into a new bug.
69+
70+
In short capabilities are tags assigned to devices by Cozify that mostly guarantee the data related to that capability will be in the same format and structure.
71+
For example the capabilities based example code in this document filters all the devices that claim to support temperature and reads their name and temperature state.
72+
Multiple capabilities can be given in a filter by providing a list of capabilities. By default any capability in the list can match (OR filter) but it can be flipped to AND mode
73+
where every capability must be present on a device for it to qualify. For example, if you only want multi-sensors that support both temperature and humidity monitoring you could define a filter as:
74+
75+
.. code:: python
76+
77+
devices = hub.devices(capabilities=[ hub.capability.TEMPERATURE, hub.capability.HUMIDITY ], and_filter=True)
78+
6079
Keeping authentication valid
6180
----------------------------
6281
If the cloud token expires, the only option to get a new one is an interactive prompt for an OTP.
@@ -86,6 +105,33 @@ And the expiry duration can be altered (also when calling cloud.ping()):
86105
# or
87106
cloud.ping(autorefresh=True, expiry=datetime.timedelta(days=20))
88107
108+
Working Remotely
109+
----------------
110+
By default queries to the hub are attempted via local LAN. Also by default "remoteness" autodetection is on and thus
111+
if it is determined during cloud.authentication() or a hub.ping() call that you seem to not be in the same network, the state is flipped.
112+
Both the remote state and autodetection can be overriden in most if not all funcions by the boolean keyword arguments 'remote' and 'autoremote'. They can also be queried or permanently changed by the hub.remote() and hub.autoremote() functions.
113+
114+
Using Multiple Hubs
115+
-------------------
116+
Everything has been designed to support multiple hubs registered to the same Cozify Cloud account. All hub operations can be targeted by setting the keyword argument 'hub_id' or 'hub_name'. The developers do not as of yet have access to multiple hubs so proper testing of multi functionality has not been performed. If you run into trouble, please open bugs so things can be improved.
117+
118+
The remote state of hubs is kept separately so there should be no issues calling your home hub locally but operating on a summer cottage hub remotely at the same time.
119+
120+
Enconding Pitfalls
121+
------------------
122+
The hub provides data encoded as a utf-8 json string. Python-cozify transforms this into a Python dictionary
123+
where string values are kept as unicode strings. Normally this isn't an issue, as long as your system supports utf-8.
124+
If not, you will run into trouble printing for example device names with non-ascii characters:
125+
126+
UnicodeEncodeError: 'ascii' codec can't encode character '\xe4' in position 34: ordinal not in range(128)
127+
128+
The solution is to change your system locale to support utf-8. How this is done is however system dependant.
129+
As a first test try temporarily overriding your locale:
130+
131+
.. code:: bash
132+
133+
LC_ALL='en_US.utf8' python3 program.py
134+
89135
Sample projects
90136
---------------
91137

@@ -98,7 +144,7 @@ Development
98144
-----------
99145
To develop python-cozify clone the devel branch and submit pull requests against the devel branch.
100146
New releases are cut from the devel branch as needed.
101-
147+
102148
Tests
103149
~~~~~
104150
pytest is used for unit tests. Test coverage is still quite spotty and under active development.

cozify/__init__.py

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

cozify/cloud.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import logging, datetime
55

66
from . import config
7-
from . import hub
87
from . import hub_api
98
from . import cloud_api
109

@@ -34,6 +33,8 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
3433
bool: True on authentication success. Failure will result in an exception.
3534
"""
3635

36+
from . import hub
37+
3738
if not _isAttr('email'):
3839
_setAttr('email', _getEmail())
3940
email = _getAttr('email')
@@ -85,9 +86,9 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
8586
logging.info('No local Hubs detected, attempting authentication via Cozify Cloud.')
8687
hub_info = hub_api.hub(remote=True, cloud_token=cloud_token, hub_token=hub_token)
8788
# if the hub wants autoremote we flip the state
88-
if hub.autoremote and not hub.remote:
89+
if hub.autoremote(hub_id) and not hub.remote(hub_id):
8990
logging.info('[autoremote] Flipping hub remote status from local to remote.')
90-
hub.remote = True
91+
hub.remote(hub_id, True)
9192
else:
9293
# localHubs is valid so a hub is in the lan. A mixed environment cannot yet be detected.
9394
# cloud_api.lan_ip cannot provide a map as to which ip is which hub. Thus we actually need to determine the right one.
@@ -96,9 +97,9 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
9697
hub_ip = localHubs[0]
9798
hub_info = hub_api.hub(host=hub_ip, remote=False)
9899
# if the hub wants autoremote we flip the state
99-
if hub.autoremote and hub.remote:
100+
if hub.autoremote(hub_id) and hub.remote(hub_id):
100101
logging.info('[autoremote] Flipping hub remote status from remote to local.')
101-
hub.remote = False
102+
hub.remote(hub_id, False)
102103

103104
hub_name = hub_info['name']
104105
if hub_id in hubkeys:
@@ -246,6 +247,8 @@ def _need_hub_token(trust=True):
246247
Returns:
247248
bool: True to indicate a need to request token.
248249
"""
250+
from . import hub
251+
249252
if not trust:
250253
logging.debug("hub_token not trusted so we'll say it needs to be renewed.")
251254
return True

cozify/cloud_api.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,14 @@ def refreshsession(cloud_token):
9696
else:
9797
raise APIError(response.status_code, response.text)
9898

99-
def remote(cloud_token, hub_token, apicall, put=False, payload=None, **kwargs):
99+
def remote(cloud_token, hub_token, apicall, payload=None, **kwargs):
100100
"""1:1 implementation of 'hub/remote'
101101
102102
Args:
103103
cloud_token(str): Cloud remote authentication token.
104104
hub_token(str): Hub authentication token.
105105
apicall(str): Full API call that would normally go directly to hub, e.g. '/cc/1.6/hub/colors'
106-
put(bool): Use PUT instead of GET.
107-
payload(str): json string to use as payload if put = True.
106+
payload(str): json string to use as payload, changes method to PUT.
108107
109108
Returns:
110109
requests.response: Requests response object.
@@ -114,7 +113,7 @@ def remote(cloud_token, hub_token, apicall, put=False, payload=None, **kwargs):
114113
'Authorization': cloud_token,
115114
'X-Hub-Key': hub_token
116115
}
117-
if put:
116+
if payload:
118117
response = requests.put(cloudBase + 'hub/remote' + apicall, headers=headers, data=payload)
119118
else:
120119
response = requests.get(cloudBase + 'hub/remote' + apicall, headers=headers)

cozify/config.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,28 @@ def stateWrite(tmpstate=None):
4848
with open(state_file, 'w') as cf:
4949
tmpstate.write(cf)
5050

51-
def setStatePath(filepath=_initXDG()):
51+
def setStatePath(filepath=_initXDG(), copy_current=False):
5252
"""Set state storage path. Useful for example for testing without affecting your normal state. Call with no arguments to reset back to autoconfigured location.
5353
5454
Args:
5555
filepath(str): file path to use as new storage location. Defaults to XDG defined path.
56+
copy_current(bool): Instead of initializing target file, dump previous state into it.
5657
"""
5758
global state_file
5859
global state
5960
state_file = filepath
60-
state = _initState(state_file)
61+
if copy_current:
62+
stateWrite()
63+
else:
64+
state = _initState(state_file)
6165

6266
def dump_state():
6367
"""Print out current state file to stdout. Long values are truncated since this is only for visualization.
6468
"""
6569
for section in state.sections():
66-
print('[{0:.10}]'.format(section))
70+
print('[{!s:.10}]'.format(section))
6771
for option in state.options(section):
68-
print(' {0:<13.13} = {1:>10.100}'.format(option, state[section][option]))
72+
print(' {!s:<13.13} = {!s:>10.100}'.format(option, state[section][option]))
6973

7074
def _iso_now():
7175
"""Helper to return isoformat datetime stamp that's more compatible than the default.

0 commit comments

Comments
 (0)