Skip to content

Commit

Permalink
Merge branch 'devel'
Browse files Browse the repository at this point in the history
  • Loading branch information
jinnatar committed Feb 8, 2018
2 parents 743c5d6 + 93b97f9 commit 20902cf
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 169 deletions.
38 changes: 19 additions & 19 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,21 @@ And the expiry duration can be altered (also when calling cloud.ping()):
# or
cloud.ping(autorefresh=True, expiry=datetime.timedelta(days=20))
Sample projects
---------------

- `github.com/Artanicus/cozify-temp <https://github.com/Artanicus/cozify-temp>`__
- Store Multisensor data into InfluxDB
- Take a look at the util/ directory for some crude small tools using the library that have been useful during development.
- File an issue to get your project added here

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.
Certain tests are marked as "live" tests and require an active authentication state and a real hub to query against.
Live tests are non-destructive.
Expand All @@ -109,22 +122,9 @@ To run the test suite on an already installed python-cozify:
pytest -v --pyargs cozify --live
Current limitations
-------------------

- Token functionality is sanity-checked up to a point and renewal is
attempted. This however is new code and may not be perfect.
- For now there are only read calls. New API call requests are welcome
as issues or pull requests!
- authentication flow is as automatic as possible but if the Cozify
Cloud token expires we can't help but request it and ask it to be
entered. If you are running a daemon that requires authentication and
your cloud token expires, run just the authenticate() flow in an
interactive terminal and then restart your daemon.
Roadmap, aka. Current Limitations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Sample projects
---------------

- `github.com/Artanicus/cozify-temp <https://github.com/Artanicus/cozify-temp>`__
- Store Multisensor data into InfluxDB
- Report an issue to get your project added here
- Authentication flow has been improved quite a bit but it would benefit a lot from real-world feedback.
- 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!
- 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.
2 changes: 1 addition & 1 deletion cozify/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.10"
__version__ = "0.2.11"
38 changes: 19 additions & 19 deletions cozify/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import logging, datetime

from . import config as c
from . import config
from . import hub
from . import hub_api
from . import cloud_api
Expand Down Expand Up @@ -60,7 +60,7 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True):
raise

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

# if hub name not already known, create named section
hubSection = 'Hubs.' + hub_id
if hubSection not in c.state:
c.state.add_section(hubSection)
if hubSection not in config.state:
config.state.add_section(hubSection)
# if default hub not set, set this hub as the first as the default
if 'default' not in c.state['Hubs']:
c.state['Hubs']['default'] = hub_id
if 'default' not in config.state['Hubs']:
config.state['Hubs']['default'] = hub_id

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

c.state['Cloud'] = {}
c.stateWrite()
config.state['Cloud'] = {}
config.stateWrite()

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

Expand Down Expand Up @@ -230,8 +230,8 @@ def _need_cloud_token(trust=True):
"""

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

# First do quick checks, i.e. do we even have a token already
if 'default' not in c.state['Hubs'] or 'hubtoken' not in c.state['Hubs.' + c.state['Hubs']['default']]:
if 'default' not in config.state['Hubs'] or 'hubtoken' not in config.state['Hubs.' + config.state['Hubs']['default']]:
logging.debug("We don't have a valid hubtoken or it's not trusted.")
return True
else: # if we have a token, we need to test if the API is callable
Expand All @@ -277,8 +277,8 @@ def _getAttr(attr):
str: Value of attribute or exception on failure
"""
section = 'Cloud'
if section in c.state and attr in c.state[section]:
return c.state[section][attr]
if section in config.state and attr in config.state[section]:
return config.state[section][attr]
else:
logging.warning('Cloud attribute {0} not found in state.'.format(attr))
raise AttributeError
Expand All @@ -292,12 +292,12 @@ def _setAttr(attr, value, commit=True):
commit(bool): True to commit state after set. Defaults to True.
"""
section = 'Cloud'
if section in c.state:
if attr not in c.state[section]:
if section in config.state:
if attr not in config.state[section]:
logging.info("Attribute {0} was not already in {1} state, new attribute created.".format(attr, section))
c.state[section][attr] = value
config.state[section][attr] = value
if commit:
c.stateWrite()
config.stateWrite()
else:
logging.warning('Section {0} not found in state.'.format(section))
raise AttributeError
Expand All @@ -308,7 +308,7 @@ def _isAttr(attr):
Returns:
bool: True if attribute exists
"""
return attr in c.state['Cloud'] and c.state['Cloud'][attr]
return attr in config.state['Cloud'] and config.state['Cloud'][attr]

def token(new_token=None):
"""Get currently used cloud_token or set a new one.
Expand Down
2 changes: 2 additions & 0 deletions cozify/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import pytest
from cozify.test.fixtures import *

def pytest_addoption(parser):
parser.addoption("--live", action="store_true",
default=False, help="run tests requiring a functional auth and a real hub.")
Expand Down
52 changes: 32 additions & 20 deletions cozify/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""

import requests, logging
from . import config as c
from . import config
from . import cloud
from . import hub_api
from enum import Enum
Expand All @@ -19,7 +19,7 @@
remote = False
autoremote = True

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')
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')

def getDevices(**kwargs):
"""Deprecated, will be removed in v0.3. Get up to date full devices data set as a dict.
Expand All @@ -35,6 +35,8 @@ def getDevices(**kwargs):
dict: full live device state as returned by the API
"""
cloud.authenticate() # the old version of getDevices did more than it was supposed to, including making sure there was a valid connection

hub_id = _get_id(**kwargs)
hub_token = token(hub_id)
cloud_token = cloud.token()
Expand All @@ -45,11 +47,12 @@ def getDevices(**kwargs):

return devices(capability=None, **kwargs)

def devices(*, capability=None, **kwargs):
def devices(*, capabilities=None, and_filter=False, **kwargs):
"""Get up to date full devices data set as a dict. Optionally can be filtered to only include certain devices.
Args:
capability(cozify.hub.capability): Capability to filter by, for example: cozify.hub.capability.TEMPERATURE. Defaults to no filtering.
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.
and_filter(bool): Multi-filter by AND instead of default OR. Defaults to False.
**hub_name(str): optional name of hub to query. Will get converted to hubId for use.
**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.
**remote(bool): Remote or local query.
Expand All @@ -64,11 +67,20 @@ def devices(*, capability=None, **kwargs):
hub_token = token(hub_id)
cloud_token = cloud.token()
hostname = host(hub_id)
if remote not in kwargs:
kwargs['remote'] = remote

devs = hub_api.devices(host=hostname, hub_token=hub_token, remote=remote, cloud_token=cloud_token)
if capability:
return { key : value for key, value in devs.items() if capability.name in value['capabilities']['values'] }
else:
devs = hub_api.devices(host=hostname, hub_token=hub_token, cloud_token=cloud_token, **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:
return { key : value for key, value in devs.items() if all(c.name in value['capabilities']['values'] for c in capabilities) }
else: # or_filter
return { key : value for key, value in devs.items() if any(c.name in value['capabilities']['values'] for c in capabilities) }
else: # no filtering
return devs

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

if 'default' not in c.state['Hubs']:
if 'default' not in config.state['Hubs']:
logging.critical('no hub name given and no default known, you should run cozify.authenticate()')
raise AttributeError
else:
return c.state['Hubs']['default']
return config.state['Hubs']['default']

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

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

Expand All @@ -131,8 +143,8 @@ def _getAttr(hub_id, attr):
str: Value of attribute or exception on failure.
"""
section = 'Hubs.' + hub_id
if section in c.state and attr in c.state[section]:
return c.state[section][attr]
if section in config.state and attr in config.state[section]:
return config.state[section][attr]
else:
logging.warning('Hub id "{0}" not found in state or attribute {1} not set for hub.'.format(hub_id, attr))
raise AttributeError
Expand All @@ -147,12 +159,12 @@ def _setAttr(hub_id, attr, value, commit=True):
commit(bool): True to commit state after set. Defaults to True.
"""
section = 'Hubs.' + hub_id
if section in c.state:
if attr not in c.state[section]:
if section in config.state:
if attr not in config.state[section]:
logging.info("Attribute {0} was not already in {1} state, new attribute created.".format(attr, section))
c.state[section][attr] = value
config.state[section][attr] = value
if commit:
c.stateWrite()
config.stateWrite()
else:
logging.warning('Section {0} not found in state.'.format(section))
raise AttributeError
Expand All @@ -176,7 +188,7 @@ def host(hub_id):
hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub.
Returns:
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.
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.
"""
return _getAttr(hub_id, 'host')

Expand Down Expand Up @@ -213,7 +225,7 @@ def ping(hub_id=None, hub_name=None, **kwargs):
config_name = 'Hubs.' + hub_id
hub_token = _getAttr(hub_id, 'hubtoken')
hub_host = _getAttr(hub_id, 'host')
cloud_token = c.state['Cloud']['remotetoken']
cloud_token = config.state['Cloud']['remotetoken']

# if we don't have a stored host then we assume the hub is remote
global remote
Expand Down
12 changes: 9 additions & 3 deletions cozify/hub_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from .Error import APIError

apiPath = '/cc/1.7'
apiPath = '/cc/1.8'

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

def devices(**kwargs):
"""1:1 implementation of /devices API call. For kwargs see cozify.hub_api.get()
"""1:1 implementation of /devices API call. For remaining kwargs see cozify.hub_api.get()
Args:
**mock_devices(dict): If defined, returned as-is as if that were the result we received.
Returns:
json: Full live device state as returned by the API
dict: Full live device state as returned by the API
"""
if 'mock_devices' in kwargs:
return kwargs['mock_devices']

return get('/devices', **kwargs)

def devices_command(command, **kwargs):
Expand Down
Loading

0 comments on commit 20902cf

Please sign in to comment.