Skip to content

Commit

Permalink
Merge branch 'devel' for release as v0.2.12
Browse files Browse the repository at this point in the history
  • Loading branch information
jinnatar committed Mar 4, 2018
2 parents 20902cf + cce24ab commit 643e419
Show file tree
Hide file tree
Showing 23 changed files with 392 additions and 197 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ MANIFEST
dist/
build/
.cache/
.pytest_cache/
58 changes: 52 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ Basic usage
-----------
These are merely some simple examples, for the full documentation see: `http://python-cozify.readthedocs.io/en/latest/`

read devices, extract multisensor data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
read devices by capability, print temperature data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: python
from cozify import hub, multisensor
devices = hub.getDevices()
print(multisensor.getMultisensorData(devices))
from cozify import hub
devices = hub.devices(capabilities=hub.capability.TEMPERATURE)
for id, dev in devices.items():
print('{0}: {1}C'.format(dev['name'], dev['state']['temperature']))
only authenticate
~~~~~~~~~~~~~~~~~
Expand All @@ -57,6 +58,24 @@ authenticate with a non-default state storage
# authentication and other useful data is now stored in the defined location instead of ~/.config/python-cozify/python-cozify.cfg
# you could also use the environment variable XDG_CONFIG_HOME to override where config files are stored
On Capabilities
---------------
The most practical way to "find" devices for operating on is currently to filter the devices list by their capabilties. The
most up to date list of recognized capabilities can be seen at `cozify/hub.py <cozify/hub.py#L21>`_

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
to those implemented is running the util/capabilities_list.py tool. It will list implemented and gathered capabilities from your live environment.
To get all of your previously unknown capabilities implemented, just copy-paste the full output of the utility into a new bug.

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.
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.
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
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:

.. code:: python
devices = hub.devices(capabilities=[ hub.capability.TEMPERATURE, hub.capability.HUMIDITY ], and_filter=True)
Keeping authentication valid
----------------------------
If the cloud token expires, the only option to get a new one is an interactive prompt for an OTP.
Expand Down Expand Up @@ -86,6 +105,33 @@ And the expiry duration can be altered (also when calling cloud.ping()):
# or
cloud.ping(autorefresh=True, expiry=datetime.timedelta(days=20))
Working Remotely
----------------
By default queries to the hub are attempted via local LAN. Also by default "remoteness" autodetection is on and thus
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.
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.

Using Multiple Hubs
-------------------
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.

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.

Enconding Pitfalls
------------------
The hub provides data encoded as a utf-8 json string. Python-cozify transforms this into a Python dictionary
where string values are kept as unicode strings. Normally this isn't an issue, as long as your system supports utf-8.
If not, you will run into trouble printing for example device names with non-ascii characters:

UnicodeEncodeError: 'ascii' codec can't encode character '\xe4' in position 34: ordinal not in range(128)

The solution is to change your system locale to support utf-8. How this is done is however system dependant.
As a first test try temporarily overriding your locale:

.. code:: bash
LC_ALL='en_US.utf8' python3 program.py
Sample projects
---------------

Expand All @@ -98,7 +144,7 @@ Development
-----------
To develop python-cozify clone the devel branch and submit pull requests against the devel branch.
New releases are cut from the devel branch as needed.

Tests
~~~~~
pytest is used for unit tests. Test coverage is still quite spotty and under active development.
Expand Down
2 changes: 1 addition & 1 deletion cozify/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.11"
__version__ = "0.2.12"
13 changes: 8 additions & 5 deletions cozify/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import logging, datetime

from . import config
from . import hub
from . import hub_api
from . import cloud_api

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

from . import hub

if not _isAttr('email'):
_setAttr('email', _getEmail())
email = _getAttr('email')
Expand Down Expand Up @@ -85,9 +86,9 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
logging.info('No local Hubs detected, attempting authentication via Cozify Cloud.')
hub_info = hub_api.hub(remote=True, cloud_token=cloud_token, hub_token=hub_token)
# if the hub wants autoremote we flip the state
if hub.autoremote and not hub.remote:
if hub.autoremote(hub_id) and not hub.remote(hub_id):
logging.info('[autoremote] Flipping hub remote status from local to remote.')
hub.remote = True
hub.remote(hub_id, True)
else:
# localHubs is valid so a hub is in the lan. A mixed environment cannot yet be detected.
# cloud_api.lan_ip cannot provide a map as to which ip is which hub. Thus we actually need to determine the right one.
Expand All @@ -96,9 +97,9 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
hub_ip = localHubs[0]
hub_info = hub_api.hub(host=hub_ip, remote=False)
# if the hub wants autoremote we flip the state
if hub.autoremote and hub.remote:
if hub.autoremote(hub_id) and hub.remote(hub_id):
logging.info('[autoremote] Flipping hub remote status from remote to local.')
hub.remote = False
hub.remote(hub_id, False)

hub_name = hub_info['name']
if hub_id in hubkeys:
Expand Down Expand Up @@ -246,6 +247,8 @@ def _need_hub_token(trust=True):
Returns:
bool: True to indicate a need to request token.
"""
from . import hub

if not trust:
logging.debug("hub_token not trusted so we'll say it needs to be renewed.")
return True
Expand Down
7 changes: 3 additions & 4 deletions cozify/cloud_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,14 @@ def refreshsession(cloud_token):
else:
raise APIError(response.status_code, response.text)

def remote(cloud_token, hub_token, apicall, put=False, payload=None, **kwargs):
def remote(cloud_token, hub_token, apicall, payload=None, **kwargs):
"""1:1 implementation of 'hub/remote'
Args:
cloud_token(str): Cloud remote authentication token.
hub_token(str): Hub authentication token.
apicall(str): Full API call that would normally go directly to hub, e.g. '/cc/1.6/hub/colors'
put(bool): Use PUT instead of GET.
payload(str): json string to use as payload if put = True.
payload(str): json string to use as payload, changes method to PUT.
Returns:
requests.response: Requests response object.
Expand All @@ -114,7 +113,7 @@ def remote(cloud_token, hub_token, apicall, put=False, payload=None, **kwargs):
'Authorization': cloud_token,
'X-Hub-Key': hub_token
}
if put:
if payload:
response = requests.put(cloudBase + 'hub/remote' + apicall, headers=headers, data=payload)
else:
response = requests.get(cloudBase + 'hub/remote' + apicall, headers=headers)
Expand Down
12 changes: 8 additions & 4 deletions cozify/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,28 @@ def stateWrite(tmpstate=None):
with open(state_file, 'w') as cf:
tmpstate.write(cf)

def setStatePath(filepath=_initXDG()):
def setStatePath(filepath=_initXDG(), copy_current=False):
"""Set state storage path. Useful for example for testing without affecting your normal state. Call with no arguments to reset back to autoconfigured location.
Args:
filepath(str): file path to use as new storage location. Defaults to XDG defined path.
copy_current(bool): Instead of initializing target file, dump previous state into it.
"""
global state_file
global state
state_file = filepath
state = _initState(state_file)
if copy_current:
stateWrite()
else:
state = _initState(state_file)

def dump_state():
"""Print out current state file to stdout. Long values are truncated since this is only for visualization.
"""
for section in state.sections():
print('[{0:.10}]'.format(section))
print('[{!s:.10}]'.format(section))
for option in state.options(section):
print(' {0:<13.13} = {1:>10.100}'.format(option, state[section][option]))
print(' {!s:<13.13} = {!s:>10.100}'.format(option, state[section][option]))

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

0 comments on commit 643e419

Please sign in to comment.