From 0a082022221f55a9c524f4a20fc5b6ba5e3ac210 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Mon, 12 Mar 2018 21:50:43 +0200 Subject: [PATCH 01/21] Next release mint will be v0.2.14 --- cozify/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cozify/__init__.py b/cozify/__init__.py index 11ef092..f3291e9 100644 --- a/cozify/__init__.py +++ b/cozify/__init__.py @@ -1 +1 @@ -__version__ = "0.2.13" +__version__ = "0.2.14" From fcc5b244f02d0c9619fe3531298debca2a2be3e0 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Wed, 14 Mar 2018 22:44:59 +0200 Subject: [PATCH 02/21] Theoretical fix for #12 (hub token expiring and not getting auto-renewed) Need to still device a way to test this reliably. Might need to implement a new destructive class of test. --- cozify/hub.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cozify/hub.py b/cozify/hub.py index 738fcea..c1d7b89 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -429,18 +429,19 @@ def autoremote(hub_id, new_state=None): _setAttr(hub_id, 'autoremote', new_state) return _getAttr(hub_id, 'autoremote', default=True, boolean=True) -def ping(**kwargs): +def ping(autorefresh=True, **kwargs): """Perform a cheap API call to trigger any potential APIError and return boolean for success/failure. For optional kwargs see cozify.hub_api.get() Args: + autorefresh(bool): Wether to perform a autorefresh after an initially failed ping. If successful, will still return True. Defaults to True. **hub_id(str): Hub to ping or default if neither id or name set. **hub_name(str): Hub to ping by name. Returns: bool: True for a valid and working hub authentication state. """ - _fill_kwargs(kwargs) try: + _fill_kwargs(kwargs) # this can raise an APIError if hub_token has expired if not kwargs['remote'] and kwargs['autoremote'] and not kwargs['host']: # flip state if no host known remote(kwargs['hub_id'], True) kwargs['remote'] = True @@ -449,7 +450,11 @@ def ping(**kwargs): logging.debug('Ping performed with tz call, response: {0}'.format(timezone)) except APIError as e: if e.status_code == 401: - logging.warn(e) + from cozify import cloud + logging.warn('Hub token has expired, hub.ping() attempting to renew it.') + logging.debug('Original APIError was: {0}'.format(e)) + if cloud.authenticate(trustHub=False): # if this fails we let it fail. + return True return False else: raise From de28d1e90dc741c19e1416ab1680543e57b6a18d Mon Sep 17 00:00:00 2001 From: Artanicus Date: Wed, 14 Mar 2018 23:27:09 +0200 Subject: [PATCH 03/21] Implement destructive test option --- cozify/conftest.py | 12 +++++++++++- cozify/test/test_hub.py | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cozify/conftest.py b/cozify/conftest.py index a791577..96b5936 100644 --- a/cozify/conftest.py +++ b/cozify/conftest.py @@ -4,11 +4,21 @@ def pytest_addoption(parser): parser.addoption("--live", action="store_true", default=False, help="run tests requiring a functional auth and a real hub.") + parser.addoption("--destructive", action="store_true", + default=False, help="run tests that require and modify the state of a real hub.") def pytest_collection_modifyitems(config, items): + live = False + destructive = False if config.getoption("--live"): + live = True + if config.getoption("--destructive"): return skip_live = pytest.mark.skip(reason="need --live option to run") + skip_destructive = pytest.mark.skip(reason="need --destructive option to run") + for item in items: - if "live" in item.keywords: + if "live" in item.keywords and not live: item.add_marker(skip_live) + if "destructive" in item.keywords and not destructive: + item.add_marker(skip_destructive) diff --git a/cozify/test/test_hub.py b/cozify/test/test_hub.py index 66d9a68..f42f431 100755 --- a/cozify/test/test_hub.py +++ b/cozify/test/test_hub.py @@ -53,3 +53,7 @@ def test_hub_devices_filter_and(tmp_hub): out = hub.devices(hub_id=tmp_hub.id, and_filter=True, capabilities=[hub.capability.COLOR_HS, hub.capability.COLOR_TEMP], mock_devices=devs) assert all(i in out for i in [ ids['lamp_osram'], ids['strip_osram'] ]) assert len(out) == 2 + +@pytest.mark.destructive +def test_hub_ping_autorefresh(live_hub): + pass From 1a4bcf544c25e39237bc270d4d1b026660a748e9 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Wed, 14 Mar 2018 23:31:05 +0200 Subject: [PATCH 04/21] Document current state of tests --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b87c4b3..7a73fb5 100644 --- a/README.rst +++ b/README.rst @@ -147,9 +147,9 @@ 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. +pytest is used for unit tests. +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. +Some tests are marked as "destructive" and will cause changes such as a light being turned on or tokens getting invalidated on purpose. During development you can run the test suite right from the source directory: @@ -158,14 +158,14 @@ During development you can run the test suite right from the source directory: pytest -v cozify/ # or include the live tests as well: pytest -v cozify/ --live + # or for the brave, also run destructive tests (also implies --live): + pytest -v cozify/ --destructive To run the test suite on an already installed python-cozify: .. code:: bash pytest -v --pyargs cozify - # or including live tests: - pytest -v --pyargs cozify --live Roadmap, aka. Current Limitations From ecd33132f3829490f6e4ddb1880d7bc678f41f4b Mon Sep 17 00:00:00 2001 From: Artanicus Date: Wed, 14 Mar 2018 23:37:39 +0200 Subject: [PATCH 05/21] Unit test for trashing a hub_token on purpose and having it renew by hub.ping() --- cozify/test/test_hub.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cozify/test/test_hub.py b/cozify/test/test_hub.py index f42f431..39e993a 100755 --- a/cozify/test/test_hub.py +++ b/cozify/test/test_hub.py @@ -56,4 +56,6 @@ def test_hub_devices_filter_and(tmp_hub): @pytest.mark.destructive def test_hub_ping_autorefresh(live_hub): - pass + hub_id = live_hub.default() + live_hub.token(hub_id=hub_id, new_token='destroyed-on-purpose-by-destructive-test') + assert hub,ping(autorenew=True) From 733a02cdbe6216edd48019b10817a583d7a808b7 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Wed, 14 Mar 2018 23:42:57 +0200 Subject: [PATCH 06/21] Actually make autorefresh optional and also test for failing if it's omitted --- cozify/hub.py | 12 +++++++----- cozify/test/test_hub.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cozify/hub.py b/cozify/hub.py index c1d7b89..39742c5 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -450,11 +450,13 @@ def ping(autorefresh=True, **kwargs): logging.debug('Ping performed with tz call, response: {0}'.format(timezone)) except APIError as e: if e.status_code == 401: - from cozify import cloud - logging.warn('Hub token has expired, hub.ping() attempting to renew it.') - logging.debug('Original APIError was: {0}'.format(e)) - if cloud.authenticate(trustHub=False): # if this fails we let it fail. - return True + if autorefresh: + from cozify import cloud + logging.warn('Hub token has expired, hub.ping() attempting to renew it.') + logging.debug('Original APIError was: {0}'.format(e)) + if cloud.authenticate(trustHub=False): # if this fails we let it fail. + return True + logging.warn(e) return False else: raise diff --git a/cozify/test/test_hub.py b/cozify/test/test_hub.py index 39e993a..59d06ed 100755 --- a/cozify/test/test_hub.py +++ b/cozify/test/test_hub.py @@ -58,4 +58,5 @@ def test_hub_devices_filter_and(tmp_hub): def test_hub_ping_autorefresh(live_hub): hub_id = live_hub.default() live_hub.token(hub_id=hub_id, new_token='destroyed-on-purpose-by-destructive-test') - assert hub,ping(autorenew=True) + assert not hub.ping(autorefresh=False) + assert hub.ping(autorefresh=True) From 7eb418690dc6fb14ea866d6e0ef07aa8cf269ce7 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Thu, 15 Mar 2018 18:14:38 -0400 Subject: [PATCH 07/21] Catch 403 auth errors and consider them a successful failure --- cozify/cloud.py | 2 +- cozify/hub.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index 2eab10c..df270b1 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -258,7 +258,7 @@ def _need_hub_token(trust=True): 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 - ping = hub.ping() + ping = hub.ping(autorefresh=False) # avoid compliating things by disabling autorefresh on failure. logging.debug("Testing hub.ping() for hub_token validity: {0}".format(ping)) return not ping diff --git a/cozify/hub.py b/cozify/hub.py index 39742c5..9a85929 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -449,7 +449,7 @@ def ping(autorefresh=True, **kwargs): timezone = tz(**kwargs) logging.debug('Ping performed with tz call, response: {0}'.format(timezone)) except APIError as e: - if e.status_code == 401: + if e.status_code == 401 or e.status_code == 403: if autorefresh: from cozify import cloud logging.warn('Hub token has expired, hub.ping() attempting to renew it.') From 941f675b144591c72b0d3569c42451a3930aa0ba Mon Sep 17 00:00:00 2001 From: Artanicus Date: Thu, 15 Mar 2018 18:15:15 -0400 Subject: [PATCH 08/21] Use live_hub --- cozify/test/test_hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cozify/test/test_hub.py b/cozify/test/test_hub.py index 59d06ed..4cd3e5e 100755 --- a/cozify/test/test_hub.py +++ b/cozify/test/test_hub.py @@ -58,5 +58,5 @@ def test_hub_devices_filter_and(tmp_hub): def test_hub_ping_autorefresh(live_hub): hub_id = live_hub.default() live_hub.token(hub_id=hub_id, new_token='destroyed-on-purpose-by-destructive-test') - assert not hub.ping(autorefresh=False) - assert hub.ping(autorefresh=True) + assert not live_hub.ping(autorefresh=False) + assert live_hub.ping(autorefresh=True) From 054e19db5550d7773ea1e8c8a64ed39bd0b3c144 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Fri, 16 Mar 2018 00:34:10 +0200 Subject: [PATCH 09/21] Change some AttributeErrors to ValueErrors. Still not sure on the split though or if any of this makes any sense. --- cozify/hub.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cozify/hub.py b/cozify/hub.py index 9a85929..6c12386 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -101,7 +101,7 @@ def device_on(device_id, **kwargs): if _is_eligible(device_id, capability.ON_OFF, **kwargs): hub_api.devices_command_on(device_id, **kwargs) else: - raise AttributeError('Device not found or not eligible for action.') + raise ValueError('Device not found or not eligible for action.') def device_off(device_id, **kwargs): """Turn off a device that is capable of turning off. Eligibility is determined by the capability ON_OFF. @@ -113,7 +113,7 @@ def device_off(device_id, **kwargs): if _is_eligible(device_id, capability.ON_OFF, **kwargs): hub_api.devices_command_off(device_id, **kwargs) else: - raise AttributeError('Device not found or not eligible for action.') + raise ValueError('Device not found or not eligible for action.') def light_temperature(device_id, temperature=2700, transition=0, **kwargs): """Set temperature of a light. @@ -142,15 +142,15 @@ def light_temperature(device_id, temperature=2700, transition=0, **kwargs): state['transitionMsec'] = transition hub_api.devices_command_state(device_id=device_id, state=state, **kwargs) else: - raise AttributeError('Device not found or not eligible for action.') + raise ValueError('Device not found or not eligible for action.') def light_color(device_id, hue, saturation=1.0, transition=0, **kwargs): """Set color (hue & saturation) of a light. Args: device_id(str): ID of the device to operate on. - hue(float): Hue in the range of [0, Pi*2]. If outside the range an AttributeError is raised. - saturation(float): Saturation in the range of [0, 1]. If outside the range an AttributeError is raised. Defaults to 1.0 (full saturation.) + hue(float): Hue in the range of [0, Pi*2]. If outside the range a ValueError is raised. + saturation(float): Saturation in the range of [0, 1]. If outside the range a ValueError is raised. Defaults to 1.0 (full saturation.) transition(int): Transition length in milliseconds. Defaults to instant. """ _fill_kwargs(kwargs) @@ -158,9 +158,9 @@ def light_color(device_id, hue, saturation=1.0, transition=0, **kwargs): if _is_eligible(device_id, capability.COLOR_HS, state=state, **kwargs): # Make sure hue & saturation are within bounds if hue < 0 or hue > math.pi * 2: - raise AttributeError('Hue out of bounds [0, pi*2]: {0}'.format(hue)) + raise ValueError('Hue out of bounds [0, pi*2]: {0}'.format(hue)) elif saturation < 0 or saturation > 1.0: - raise AttributeError('Saturation out of bounds [0, 1.0]: {0}'.format(saturation)) + raise ValueError('Saturation out of bounds [0, 1.0]: {0}'.format(saturation)) state = _clean_state(state) state['colorMode'] = 'hs' @@ -168,14 +168,14 @@ def light_color(device_id, hue, saturation=1.0, transition=0, **kwargs): state['saturation'] = saturation hub_api.devices_command_state(device_id=device_id, state=state, **kwargs) else: - raise AttributeError('Device not found or not eligible for action.') + raise ValueError('Device not found or not eligible for action.') def light_brightness(device_id, brightness, transition=0, **kwargs): """Set brightness of a light. Args: device_id(str): ID of the device to operate on. - brightness(float): Brightness in the range of [0, 1]. If outside the range an AttributeError is raised. + brightness(float): Brightness in the range of [0, 1]. If outside the range a ValueError is raised. transition(int): Transition length in milliseconds. Defaults to instant. """ _fill_kwargs(kwargs) @@ -183,13 +183,13 @@ def light_brightness(device_id, brightness, transition=0, **kwargs): if _is_eligible(device_id, capability.BRIGHTNESS, state=state, **kwargs): # Make sure hue & saturation are within bounds if brightness < 0 or brightness > 1.0: - raise AttributeError('Brightness out of bounds [0, 1.0]: {0}'.format(brightness)) + raise ValueError('Brightness out of bounds [0, 1.0]: {0}'.format(brightness)) state = _clean_state(state) state['brightness'] = brightness hub_api.devices_command_state(device_id=device_id, state=state, **kwargs) else: - raise AttributeError('Device not found or not eligible for action.') + raise ValueError('Device not found or not eligible for action.') def _is_eligible(device_id, capability_filter, devs=None, state=None, **kwargs): """Check if device matches a AND devices filter. From c94b827d77030cbc2bd17d18962a0c70b9b9b1df Mon Sep 17 00:00:00 2001 From: Artanicus Date: Fri, 16 Mar 2018 00:54:23 +0200 Subject: [PATCH 10/21] Move to using Abseil-py for logging --- cozify/cloud.py | 7 ++++--- cozify/hub.py | 4 ++-- cozify/test/debug.py | 4 ++-- setup.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index df270b1..47f8dbb 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -1,7 +1,8 @@ """Module for handling Cozify Cloud highlevel operations. """ -import logging, datetime +from absl import logging +import datetime from . import config from . import hub_api @@ -50,7 +51,7 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True): otp = _getotp() if not otp: message = "OTP unavailable, authentication cannot succeed. This may happen if running non-interactively (closed stdin)." - logging.critical(message) + logging.fatal(message) raise AuthenticationError(message) try: @@ -72,7 +73,7 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True): # TODO(artanicus): unknown what will happen if there is a local hub but another one remote. Needs testing by someone with multiple hubs. Issue #7 hubkeys = cloud_api.hubkeys(cloud_token) # get all registered hubs and their keys from the cloud. if not hubkeys: - logging.critical('You have not registered any hubs to the Cozify Cloud, hence a hub cannot be used yet.') + logging.fatal('You have not registered any hubs to the Cozify Cloud, hence a hub cannot be used yet.') # evaluate all returned Hubs and store them logging.debug('Listing all hubs returned by cloud hubkeys query:') diff --git a/cozify/hub.py b/cozify/hub.py index 6c12386..606ba6f 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -5,7 +5,7 @@ """ -import logging +from absl import logging import math from . import config from . import hub_api @@ -284,7 +284,7 @@ def default(): """ if 'default' not in config.state['Hubs']: - logging.critical('Default hub not known, you should run cozify.authenticate()') + logging.fatal('Default hub not known, you should run cozify.authenticate()') raise AttributeError else: return config.state['Hubs']['default'] diff --git a/cozify/test/debug.py b/cozify/test/debug.py index c67067b..eb9da06 100755 --- a/cozify/test/debug.py +++ b/cozify/test/debug.py @@ -2,6 +2,6 @@ """Set high log level """ -import logging +from absl import logging -logging.basicConfig(level=logging.DEBUG) +logging.set_verbosity(logging.DEBUG) diff --git a/setup.py b/setup.py index c00c308..8bdb2eb 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def run_tests(self): license = 'MIT', packages = ['cozify'], tests_require=['pytest'], - install_requires=['requests'], + install_requires=['requests', 'absl-py'], cmdclass={'test': PyTest}, classifiers = [ "Development Status :: 3 - Alpha", From b7ac573137506b825b726678b7b2663b0088de19 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Fri, 16 Mar 2018 19:00:19 +0200 Subject: [PATCH 11/21] Don't go poking at hub state if we know it doesn't exist yet --- cozify/cloud.py | 2 +- cozify/hub.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index 47f8dbb..100c3e7 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -98,7 +98,7 @@ 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(hub_id) and hub.remote(hub_id): + if hub.exists(hub_id) and hub.autoremote(hub_id) and hub.remote(hub_id): logging.info('[autoremote] Flipping hub remote status from remote to local.') hub.remote(hub_id, False) diff --git a/cozify/hub.py b/cozify/hub.py index 606ba6f..1d4e93c 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -319,6 +319,17 @@ def hub_id(hub_name): return section[5:] # cut out "Hubs." raise AttributeError('Hub not found: {0}'.format(hub_name)) +def exists(hub_id): + """Check for existance of hub in local state. + + Args: + hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. + """ + if 'Hubs.{0}'.format(hub_id) in config.state: + return True + else: + return False + def _getAttr(hub_id, attr, default=None, boolean=False): """Get hub state attributes by attr name. Optionally set a default value if attribute not found. From 09f513918ce5b73ce1910715040303fd9bfde994 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Fri, 16 Mar 2018 19:16:36 +0200 Subject: [PATCH 12/21] Convert device-toggle.py to use flags, --device now required --- util/device-toggle.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/util/device-toggle.py b/util/device-toggle.py index ab85af0..2915cb9 100755 --- a/util/device-toggle.py +++ b/util/device-toggle.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 from cozify import hub import pprint, sys +from absl import flags, app from cozify.test import debug -def main(device): - hub.device_toggle(device) +FLAGS = flags.FLAGS + +flags.DEFINE_string('device', None, 'Device to operate on.') + +def main(argv): + del argv + hub.device_toggle(FLAGS.device) if __name__ == "__main__": - if len(sys.argv) > 1: - main(sys.argv[1]) - else: - sys.exit(1) + flags.mark_flag_as_required('device') + app.run(main) From 443c56ce6c2f82682bd495b8cb7d4fbf2bc79c75 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sat, 17 Mar 2018 13:07:23 +0200 Subject: [PATCH 13/21] New device state observers --- cozify/hub.py | 52 +++++++++++++++++++++++++-------- cozify/test/fixtures_devices.py | 4 ++- cozify/test/test_hub.py | 15 ++++++++++ 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/cozify/hub.py b/cozify/hub.py index 1d4e93c..0200bf7 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -98,7 +98,7 @@ def device_on(device_id, **kwargs): device_id(str): ID of the device to operate on. """ _fill_kwargs(kwargs) - if _is_eligible(device_id, capability.ON_OFF, **kwargs): + if device_eligible(device_id, capability.ON_OFF, **kwargs): hub_api.devices_command_on(device_id, **kwargs) else: raise ValueError('Device not found or not eligible for action.') @@ -110,11 +110,19 @@ def device_off(device_id, **kwargs): device_id(str): ID of the device to operate on. """ _fill_kwargs(kwargs) - if _is_eligible(device_id, capability.ON_OFF, **kwargs): + if device_eligible(device_id, capability.ON_OFF, **kwargs): hub_api.devices_command_off(device_id, **kwargs) else: raise ValueError('Device not found or not eligible for action.') +def device_reachable(device_id, **kwargs): + _fill_kwargs(kwargs) + state = {} + if device_exists(device_id, state=state, **kwargs): + return state['reachable'] + else: + raise ValueError('Device not found: {}'.format(device_id)) + def light_temperature(device_id, temperature=2700, transition=0, **kwargs): """Set temperature of a light. @@ -124,8 +132,8 @@ def light_temperature(device_id, temperature=2700, transition=0, **kwargs): transition(int): Transition length in milliseconds. Defaults to instant. """ _fill_kwargs(kwargs) - state = {} # will be populated by _is_eligible - if _is_eligible(device_id, capability.COLOR_TEMP, state=state, **kwargs): + state = {} # will be populated by device_eligible + if device_eligible(device_id, capability.COLOR_TEMP, state=state, **kwargs): # Make sure temperature is within bounds [state.minTemperature, state.maxTemperature] minimum = state['minTemperature'] maximum = state['maxTemperature'] @@ -154,8 +162,8 @@ def light_color(device_id, hue, saturation=1.0, transition=0, **kwargs): transition(int): Transition length in milliseconds. Defaults to instant. """ _fill_kwargs(kwargs) - state = {} # will be populated by _is_eligible - if _is_eligible(device_id, capability.COLOR_HS, state=state, **kwargs): + state = {} # will be populated by device_eligible + if device_eligible(device_id, capability.COLOR_HS, state=state, **kwargs): # Make sure hue & saturation are within bounds if hue < 0 or hue > math.pi * 2: raise ValueError('Hue out of bounds [0, pi*2]: {0}'.format(hue)) @@ -179,8 +187,8 @@ def light_brightness(device_id, brightness, transition=0, **kwargs): transition(int): Transition length in milliseconds. Defaults to instant. """ _fill_kwargs(kwargs) - state = {} # will be populated by _is_eligible - if _is_eligible(device_id, capability.BRIGHTNESS, state=state, **kwargs): + state = {} # will be populated by device_eligible + if device_eligible(device_id, capability.BRIGHTNESS, state=state, **kwargs): # Make sure hue & saturation are within bounds if brightness < 0 or brightness > 1.0: raise ValueError('Brightness out of bounds [0, 1.0]: {0}'.format(brightness)) @@ -191,12 +199,12 @@ def light_brightness(device_id, brightness, transition=0, **kwargs): else: raise ValueError('Device not found or not eligible for action.') -def _is_eligible(device_id, capability_filter, devs=None, state=None, **kwargs): +def device_eligible(device_id, capability_filter, devs=None, state=None, **kwargs): """Check if device matches a AND devices filter. Args: device_id(str): ID of the device to check. - filter(hub.capability): Single hub.capability or a list of them to match against. + capability_filter(hub.capability): Single hub.capability or a list of them to match against. devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live. state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible. Returns: @@ -205,12 +213,32 @@ def _is_eligible(device_id, capability_filter, devs=None, state=None, **kwargs): if devs is None: # only retrieve if we didn't get them devs = devices(capabilities=capability_filter, **kwargs) if device_id in devs: - state.update(devs[device_id]['state']) - logging.debug('Implicitly returning state: {0}'.format(state)) + if state is not None: + state.update(devs[device_id]['state']) + logging.debug('Implicitly returning state: {0}'.format(state)) return True else: return False +def device_exists(device_id, devs=None, state=None, **kwargs): + """Check if device exists. + + Args: + device_id(str): ID of the device to check. + devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live. + state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible. + Returns: + bool: True if filter matches. + """ + if devs is None: # only retrieve if we didn't get them + devs = devices(**kwargs) + if device_id in devs: + if state is not None: + state.update(devs[device_id]['state']) + logging.debug('Implicitly returning state: {0}'.format(state)) + return True + else: + return False def _get_id(**kwargs): """Get a hub_id from various sources, meant so that you can just throw kwargs at it and get a valid id. diff --git a/cozify/test/fixtures_devices.py b/cozify/test/fixtures_devices.py index 3c3a297..f34e399 100644 --- a/cozify/test/fixtures_devices.py +++ b/cozify/test/fixtures_devices.py @@ -165,5 +165,7 @@ 'lamp_osram': lamp_osram['id'], 'strip_osram': strip_osram['id'], 'plafond_osram': plafond_osram['id'], - 'twilight_nexa': twilight_nexa['id'] + 'twilight_nexa': twilight_nexa['id'], + 'reachable': plafond_osram['id'], + 'not-reachable': lamp_osram['id'] } diff --git a/cozify/test/test_hub.py b/cozify/test/test_hub.py index 4cd3e5e..16b5160 100755 --- a/cozify/test/test_hub.py +++ b/cozify/test/test_hub.py @@ -60,3 +60,18 @@ def test_hub_ping_autorefresh(live_hub): live_hub.token(hub_id=hub_id, new_token='destroyed-on-purpose-by-destructive-test') assert not live_hub.ping(autorefresh=False) assert live_hub.ping(autorefresh=True) + +def test_hub_device_eligible(tmp_hub): + ids, devs = tmp_hub.devices() + assert hub.device_eligible(ids['lamp_osram'], hub.capability.COLOR_TEMP, mock_devices=devs) + assert not hub.device_eligible(ids['twilight_nexa'], hub.capability.COLOR_TEMP, mock_devices=devs) + +def test_hub_device_reachable(tmp_hub): + ids, devs = tmp_hub.devices() + assert hub.device_reachable(ids['reachable'], mock_devices=devs) + assert not hub.device_reachable(ids['not-reachable'], mock_devices=devs) + +def test_hub_device_exists(tmp_hub): + ids, devs = tmp_hub.devices() + assert hub.device_exists(ids['reachable'], mock_devices=devs) + assert not hub.device_exists('dead-beef', mock_devices=devs) From d1f7e5d2544124478f13c41d45fce7d8799f722a Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sat, 17 Mar 2018 18:01:59 +0200 Subject: [PATCH 14/21] Ignore local eggs, will be created if running setup.py test --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1197d68..4c919de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.cfg *.pyc *.egg-info +.eggs MANIFEST dist/ build/ From d3a7b5113596de2ae7b51a282d31b25dddc8204b Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sat, 17 Mar 2018 18:37:26 +0200 Subject: [PATCH 15/21] Re-order functions under logical sections --- cozify/hub.py | 433 +++++++++++++++++++++++++------------------------- 1 file changed, 220 insertions(+), 213 deletions(-) diff --git a/cozify/hub.py b/cozify/hub.py index 0200bf7..5ef0db0 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -11,37 +11,11 @@ from . import hub_api from enum import Enum - from .Error import APIError 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') -def getDevices(**kwargs): - """Deprecated, will be removed in v0.3. Get up to date full devices data set as a dict. - - Args: - **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. - **hubId(str): Deprecated. Compatibility keyword for hub_id, to be removed in v0.3 - **hubName(str): Deprecated. Compatibility keyword for hub_name, to be removed in v0.3 - - Returns: - dict: full live device state as returned by the API - - """ - from . import cloud - 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() - hostname = host(hub_id) - - if 'remote' not in kwargs: - kwargs['remote'] = remote - - return devices(**kwargs) +### Device data ### 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. @@ -72,6 +46,57 @@ def devices(*, capabilities=None, and_filter=False, **kwargs): else: # no filtering return devs +def device_reachable(device_id, **kwargs): + _fill_kwargs(kwargs) + state = {} + if device_exists(device_id, state=state, **kwargs): + return state['reachable'] + else: + raise ValueError('Device not found: {}'.format(device_id)) + +def device_exists(device_id, devs=None, state=None, **kwargs): + """Check if device exists. + + Args: + device_id(str): ID of the device to check. + devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live. + state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible. + Returns: + bool: True if filter matches. + """ + if devs is None: # only retrieve if we didn't get them + devs = devices(**kwargs) + if device_id in devs: + if state is not None: + state.update(devs[device_id]['state']) + logging.debug('Implicitly returning state: {0}'.format(state)) + return True + else: + return False + +def device_eligible(device_id, capability_filter, devs=None, state=None, **kwargs): + """Check if device matches a AND devices filter. + + Args: + device_id(str): ID of the device to check. + capability_filter(hub.capability): Single hub.capability or a list of them to match against. + devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live. + state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible. + Returns: + bool: True if filter matches. + """ + if devs is None: # only retrieve if we didn't get them + devs = devices(capabilities=capability_filter, **kwargs) + if device_id in devs: + if state is not None: + state.update(devs[device_id]['state']) + logging.debug('Implicitly returning state: {0}'.format(state)) + return True + else: + return False + +### Device control ### + def device_toggle(device_id, **kwargs): """Toggle power state of any device capable of it such as lamps. Eligibility is determined by the capability ON_OFF. @@ -115,14 +140,6 @@ def device_off(device_id, **kwargs): else: raise ValueError('Device not found or not eligible for action.') -def device_reachable(device_id, **kwargs): - _fill_kwargs(kwargs) - state = {} - if device_exists(device_id, state=state, **kwargs): - return state['reachable'] - else: - raise ValueError('Device not found: {}'.format(device_id)) - def light_temperature(device_id, temperature=2700, transition=0, **kwargs): """Set temperature of a light. @@ -199,136 +216,116 @@ def light_brightness(device_id, brightness, transition=0, **kwargs): else: raise ValueError('Device not found or not eligible for action.') -def device_eligible(device_id, capability_filter, devs=None, state=None, **kwargs): - """Check if device matches a AND devices filter. +### Hub modifiers ### + +def remote(hub_id, new_state=None): + """Get remote status of matching hub_id or set a new value for it. Args: - device_id(str): ID of the device to check. - capability_filter(hub.capability): Single hub.capability or a list of them to match against. - devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live. - state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible. + hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. + Returns: - bool: True if filter matches. + bool: True for a hub considered remote. """ - if devs is None: # only retrieve if we didn't get them - devs = devices(capabilities=capability_filter, **kwargs) - if device_id in devs: - if state is not None: - state.update(devs[device_id]['state']) - logging.debug('Implicitly returning state: {0}'.format(state)) - return True - else: - return False + if new_state: + _setAttr(hub_id, 'remote', new_state) + return _getAttr(hub_id, 'remote', default=False, boolean=True) -def device_exists(device_id, devs=None, state=None, **kwargs): - """Check if device exists. +def autoremote(hub_id, new_state=None): + """Get autoremote status of matching hub_id or set a new value for it. Args: - device_id(str): ID of the device to check. - devs(dict): Optional devices dictionary to use. If not defined, will be retrieved live. - state(dict): Optional state dictionary, will be populated with state of checked device if device is eligible. + hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. + Returns: - bool: True if filter matches. + bool: True for a hub with autoremote enabled. """ - if devs is None: # only retrieve if we didn't get them - devs = devices(**kwargs) - if device_id in devs: - if state is not None: - state.update(devs[device_id]['state']) - logging.debug('Implicitly returning state: {0}'.format(state)) - return True - else: - return False + if new_state: + _setAttr(hub_id, 'autoremote', new_state) + return _getAttr(hub_id, 'autoremote', default=True, boolean=True) -def _get_id(**kwargs): - """Get a hub_id from various sources, meant so that you can just throw kwargs at it and get a valid id. - If no data is available to determine which hub was meant, will default to the default hub. If even that fails, will raise an AttributeError. +### Hub info ### +def tz(**kwargs): + """Get timezone of given hub or default hub if no id is specified. For more optional kwargs see cozify.hub_api.get() Args: - **hub_id(str): Will be returned as-is if defined. - **hub_name(str): Name of hub. - hubName(str): Deprecated. Compatibility keyword for hub_name, to be removed in v0.3 - hubId(str): Deprecated. Compatibility keyword for hub_id, to be removed in v0.3 + **hub_id(str): Hub to query, by default the default hub is used. + + Returns: + str: Timezone of the hub, for example: 'Europe/Helsinki' """ - if 'hub_id' in kwargs or 'hubId' in kwargs: - logging.debug("Redundant hub._get_id call, resolving hub_id to itself.") - if 'hub_id' in kwargs: - return kwargs['hub_id'] - return kwargs['hubId'] - if 'hub_name' in kwargs or 'hubName' in kwargs: - if 'hub_name' in kwargs: - return hub_id(kwargs['hub_name']) - return getHubId(kwargs['hubName']) - return default() + _fill_kwargs(kwargs) -def _fill_kwargs(kwargs): - """Check that common items are present in kwargs and fill them if not. + return hub_api.tz(**kwargs) + +def ping(autorefresh=True, **kwargs): + """Perform a cheap API call to trigger any potential APIError and return boolean for success/failure. For optional kwargs see cozify.hub_api.get() Args: - kwargs(dict): kwargs dictionary to fill. Operated on directly. + autorefresh(bool): Wether to perform a autorefresh after an initially failed ping. If successful, will still return True. Defaults to True. + **hub_id(str): Hub to ping or default if neither id or name set. + **hub_name(str): Hub to ping by name. + Returns: + bool: True for a valid and working hub authentication state. """ - if 'hub_id' not in kwargs: - kwargs['hub_id'] = _get_id(**kwargs) - if 'remote' not in kwargs: - kwargs['remote'] = remote(kwargs['hub_id']) - if 'autoremote' not in kwargs: - kwargs['autoremote'] = True - if 'hub_token' not in kwargs: - kwargs['hub_token'] = token(kwargs['hub_id']) - if 'cloud_token' not in kwargs: - from . import cloud - kwargs['cloud_token'] = cloud.token() - if 'host' not in kwargs: - kwargs['host'] = host(kwargs['hub_id']) + try: + _fill_kwargs(kwargs) # this can raise an APIError if hub_token has expired + if not kwargs['remote'] and kwargs['autoremote'] and not kwargs['host']: # flip state if no host known + remote(kwargs['hub_id'], True) + kwargs['remote'] = True + logging.debug('Ping determined hub is remote and flipped state to remote.') + timezone = tz(**kwargs) + logging.debug('Ping performed with tz call, response: {0}'.format(timezone)) + except APIError as e: + if e.status_code == 401 or e.status_code == 403: + if autorefresh: + from cozify import cloud + logging.warn('Hub token has expired, hub.ping() attempting to renew it.') + logging.debug('Original APIError was: {0}'.format(e)) + if cloud.authenticate(trustHub=False): # if this fails we let it fail. + return True + logging.warn(e) + return False + else: + raise + else: + return True -def _clean_state(state): - """Return purged state of values so only wanted values can be modified. +def name(hub_id): + """Get hub name by it's id. Args: - state(dict): device state dictionary. Original won't be modified. - """ - out = {} - for k, v in state.items(): - if isinstance(v, dict): # recurse nested dicts - out[k] = _clean_state(v) - elif k == "type": # type values are kept - out[k] = v - else: # null out the rest - out[k] = None - return out - + hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. -def getDefaultHub(): - """Deprecated, use default(). Return id of default Hub. + Returns: + str: Hub name or None if the hub wasn't found. """ - logging.warn('hub.getDefaultHub is deprecated and will be removed soon. Use hub.default()') - return default() + return _getAttr(hub_id, 'hubname') -def default(): - """Return id of default Hub. +def host(hub_id): + """Get hostname of matching hub_id - If default hub isn't known an AttributeError will be raised. - """ + Args: + hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. - if 'default' not in config.state['Hubs']: - logging.fatal('Default hub not known, you should run cozify.authenticate()') - raise AttributeError - else: - return config.state['Hubs']['default'] + 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 and an ip address was previously locally known. + """ + return _getAttr(hub_id, 'host') -def getHubId(hub_name): - """Deprecated, use hub_id(). Return id of hub by it's name. +def token(hub_id, new_token=None): + """Get hub_token of matching hub_id or set a new value for it. Args: - hub_name(str): Name of hub to query. The name is given when registering a hub to an account. - str: hub_id on success, raises an attributeerror on failure. + hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. Returns: - str: Hub id or raises + str: Hub authentication token. """ - logging.warn('hub.getHubId is deprecated and will be removed soon. Use hub.hub_id()') - return hub_id(hub_name) + if new_token: + _setAttr(hub_id, 'hubtoken', new_token) + return _getAttr(hub_id, 'hubtoken') def hub_id(hub_name): """Get hub id by it's name. @@ -358,6 +355,20 @@ def exists(hub_id): else: return False +def default(): + """Return id of default Hub. + + If default hub isn't known an AttributeError will be raised. + """ + + if 'default' not in config.state['Hubs']: + logging.fatal('Default hub not known, you should run cozify.authenticate()') + raise AttributeError + else: + return config.state['Hubs']['default'] + +### Internals ### + def _getAttr(hub_id, attr, default=None, boolean=False): """Get hub state attributes by attr name. Optionally set a default value if attribute not found. @@ -406,112 +417,108 @@ def _setAttr(hub_id, attr, value, commit=True): logging.warning('Section {0} not found in state.'.format(section)) raise AttributeError - -def name(hub_id): - """Get hub name by it's id. +def _get_id(**kwargs): + """Get a hub_id from various sources, meant so that you can just throw kwargs at it and get a valid id. + If no data is available to determine which hub was meant, will default to the default hub. If even that fails, will raise an AttributeError. Args: - hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. - - Returns: - str: Hub name or None if the hub wasn't found. + **hub_id(str): Will be returned as-is if defined. + **hub_name(str): Name of hub. + hubName(str): Deprecated. Compatibility keyword for hub_name, to be removed in v0.3 + hubId(str): Deprecated. Compatibility keyword for hub_id, to be removed in v0.3 """ - return _getAttr(hub_id, 'hubname') + if 'hub_id' in kwargs or 'hubId' in kwargs: + logging.debug("Redundant hub._get_id call, resolving hub_id to itself.") + if 'hub_id' in kwargs: + return kwargs['hub_id'] + return kwargs['hubId'] + if 'hub_name' in kwargs or 'hubName' in kwargs: + if 'hub_name' in kwargs: + return hub_id(kwargs['hub_name']) + return getHubId(kwargs['hubName']) + return default() -def host(hub_id): - """Get hostname of matching hub_id +def _fill_kwargs(kwargs): + """Check that common items are present in kwargs and fill them if not. Args: - hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. + kwargs(dict): kwargs dictionary to fill. Operated on directly. - 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 and an ip address was previously locally known. """ - return _getAttr(hub_id, 'host') + if 'hub_id' not in kwargs: + kwargs['hub_id'] = _get_id(**kwargs) + if 'remote' not in kwargs: + kwargs['remote'] = remote(kwargs['hub_id']) + if 'autoremote' not in kwargs: + kwargs['autoremote'] = True + if 'hub_token' not in kwargs: + kwargs['hub_token'] = token(kwargs['hub_id']) + if 'cloud_token' not in kwargs: + from . import cloud + kwargs['cloud_token'] = cloud.token() + if 'host' not in kwargs: + kwargs['host'] = host(kwargs['hub_id']) -def token(hub_id, new_token=None): - """Get hub_token of matching hub_id or set a new value for it. +def _clean_state(state): + """Return purged state of values so only wanted values can be modified. Args: - hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. - - Returns: - str: Hub authentication token. + state(dict): device state dictionary. Original won't be modified. """ - if new_token: - _setAttr(hub_id, 'hubtoken', new_token) - return _getAttr(hub_id, 'hubtoken') - -def remote(hub_id, new_state=None): - """Get remote status of matching hub_id or set a new value for it. + out = {} + for k, v in state.items(): + if isinstance(v, dict): # recurse nested dicts + out[k] = _clean_state(v) + elif k == "type": # type values are kept + out[k] = v + else: # null out the rest + out[k] = None + return out - Args: - hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. +### Deprecated functions, will be removed in v0.3. Until then they'll merely cause a logging WARN to be emitted. - Returns: - bool: True for a hub considered remote. - """ - if new_state: - _setAttr(hub_id, 'remote', new_state) - return _getAttr(hub_id, 'remote', default=False, boolean=True) - -def autoremote(hub_id, new_state=None): - """Get autoremote status of matching hub_id or set a new value for it. +def getDevices(**kwargs): + """Deprecated, will be removed in v0.3. Get up to date full devices data set as a dict. Args: - hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. + **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. + **hubId(str): Deprecated. Compatibility keyword for hub_id, to be removed in v0.3 + **hubName(str): Deprecated. Compatibility keyword for hub_name, to be removed in v0.3 Returns: - bool: True for a hub with autoremote enabled. + dict: full live device state as returned by the API + """ - if new_state: - _setAttr(hub_id, 'autoremote', new_state) - return _getAttr(hub_id, 'autoremote', default=True, boolean=True) + from . import cloud + cloud.authenticate() # the old version of getDevices did more than it was supposed to, including making sure there was a valid connection -def ping(autorefresh=True, **kwargs): - """Perform a cheap API call to trigger any potential APIError and return boolean for success/failure. For optional kwargs see cozify.hub_api.get() + hub_id = _get_id(**kwargs) + hub_token = token(hub_id) + cloud_token = cloud.token() + hostname = host(hub_id) - Args: - autorefresh(bool): Wether to perform a autorefresh after an initially failed ping. If successful, will still return True. Defaults to True. - **hub_id(str): Hub to ping or default if neither id or name set. - **hub_name(str): Hub to ping by name. + if 'remote' not in kwargs: + kwargs['remote'] = remote - Returns: - bool: True for a valid and working hub authentication state. - """ - try: - _fill_kwargs(kwargs) # this can raise an APIError if hub_token has expired - if not kwargs['remote'] and kwargs['autoremote'] and not kwargs['host']: # flip state if no host known - remote(kwargs['hub_id'], True) - kwargs['remote'] = True - logging.debug('Ping determined hub is remote and flipped state to remote.') - timezone = tz(**kwargs) - logging.debug('Ping performed with tz call, response: {0}'.format(timezone)) - except APIError as e: - if e.status_code == 401 or e.status_code == 403: - if autorefresh: - from cozify import cloud - logging.warn('Hub token has expired, hub.ping() attempting to renew it.') - logging.debug('Original APIError was: {0}'.format(e)) - if cloud.authenticate(trustHub=False): # if this fails we let it fail. - return True - logging.warn(e) - return False - else: - raise - else: - return True + return devices(**kwargs) +def getDefaultHub(): + """Deprecated, use default(). Return id of default Hub. + """ + logging.warn('hub.getDefaultHub is deprecated and will be removed soon. Use hub.default()') + return default() -def tz(**kwargs): - """Get timezone of given hub or default hub if no id is specified. For more optional kwargs see cozify.hub_api.get() +def getHubId(hub_name): + """Deprecated, use hub_id(). Return id of hub by it's name. Args: - **hub_id(str): Hub to query, by default the default hub is used. + hub_name(str): Name of hub to query. The name is given when registering a hub to an account. + str: hub_id on success, raises an attributeerror on failure. Returns: - str: Timezone of the hub, for example: 'Europe/Helsinki' + str: Hub id or raises """ - _fill_kwargs(kwargs) - - return hub_api.tz(**kwargs) + logging.warn('hub.getHubId is deprecated and will be removed soon. Use hub.hub_id()') + return hub_id(hub_name) From 735e07826b1902bcf61275e799c774783cf76a11 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sun, 18 Mar 2018 16:01:13 -0400 Subject: [PATCH 16/21] Remove tmpfile when exiting live_cloud fixture --- cozify/test/fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cozify/test/fixtures.py b/cozify/test/fixtures.py index 0b7265c..ef37bb7 100755 --- a/cozify/test/fixtures.py +++ b/cozify/test/fixtures.py @@ -31,6 +31,7 @@ def live_cloud(): from cozify import cloud yield cloud config.setStatePath() + os.remove(configpath) @pytest.fixture def id(): From 7e2c6c3018810d1f086ee8f4edb2aab0174a0a24 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sun, 18 Mar 2018 22:18:56 +0200 Subject: [PATCH 17/21] No need to specify default hub anymore --- util/cleanSlate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/cleanSlate.py b/util/cleanSlate.py index b33cb6a..a78d464 100755 --- a/util/cleanSlate.py +++ b/util/cleanSlate.py @@ -8,7 +8,7 @@ def main(): assert cloud.authenticate() config.dump_state() - print(hub.tz(hub.default())) + print(hub.tz()) os.remove(tmp) if __name__ == "__main__": From 0e995e3c9ac051cb18ec7def30a8622b7c70791d Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sun, 18 Mar 2018 16:23:00 -0400 Subject: [PATCH 18/21] Require hub to exist before attempting to get autoremote. This fixes a clean slate situation when starting remotely --- cozify/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index 100c3e7..5903c4d 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -87,7 +87,7 @@ 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(hub_id) and not hub.remote(hub_id): + if if hub.exists(hub_id) and hub.autoremote(hub_id) and not hub.remote(hub_id): logging.info('[autoremote] Flipping hub remote status from local to remote.') hub.remote(hub_id, True) else: From 4eed3c26a528e47a94bb0366f84d06984666e430 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sun, 18 Mar 2018 16:24:02 -0400 Subject: [PATCH 19/21] Silly typo --- cozify/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index 5903c4d..a6133cf 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -87,7 +87,7 @@ 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 if hub.exists(hub_id) and hub.autoremote(hub_id) and not hub.remote(hub_id): + if hub.exists(hub_id) and hub.autoremote(hub_id) and not hub.remote(hub_id): logging.info('[autoremote] Flipping hub remote status from local to remote.') hub.remote(hub_id, True) else: From 0b5b8ea01a5e1767ba9ffcf298497181943709cd Mon Sep 17 00:00:00 2001 From: Artanicus Date: Thu, 22 Mar 2018 11:45:51 -0400 Subject: [PATCH 20/21] Fix initial remote determination when remote --- cozify/cloud.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index a6133cf..7847298 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -76,20 +76,25 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True): logging.fatal('You have not registered any hubs to the Cozify Cloud, hence a hub cannot be used yet.') # evaluate all returned Hubs and store them - logging.debug('Listing all hubs returned by cloud hubkeys query:') for hub_id, hub_token in hubkeys.items(): logging.debug('hub: {0} token: {1}'.format(hub_id, hub_token)) hub_info = None hub_ip = None + remote = None + autoremote = None + if not hub.exists(hub_id): + autoremote = True + else: + autoremote = hub.autoremote(hub_id=hub_id) # if we're remote, we didn't get a valid ip if not localHubs: - logging.info('No local Hubs detected, attempting authentication via Cozify Cloud.') + logging.info('No local Hubs detected, changing to remote mode.') 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.exists(hub_id) and hub.autoremote(hub_id) and not hub.remote(hub_id): + # if the hub wants autoremote we flip the state. If this is the first time the hub is seen, act as if autoremote=True, remote=False + if not hub.exists(hub_id) or (hub.autoremote(hub_id) and not hub.remote(hub_id)): logging.info('[autoremote] Flipping hub remote status from local to remote.') - hub.remote(hub_id, True) + remote = 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. @@ -97,10 +102,10 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True): logging.debug('data structure: {0}'.format(localHubs)) 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.exists(hub_id) and hub.autoremote(hub_id) and hub.remote(hub_id): + # if the hub wants autoremote we flip the state. If this is the first time the hub is seen, act as if autoremote=True, remote=False + if not hub.exists(hub_id) or (hub.autoremote(hub_id) and hub.remote(hub_id)): logging.info('[autoremote] Flipping hub remote status from remote to local.') - hub.remote(hub_id, False) + remote = False hub_name = hub_info['name'] if hub_id in hubkeys: @@ -122,6 +127,7 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True): hub._setAttr(hub_id, 'host', hub_ip, commit=False) hub._setAttr(hub_id, 'hubName', hub_name, commit=False) hub.token(hub_id, hub_token) + hub.remote(hub_id, remote) return True def resetState(): From 854a341791d262481210d0ed206f8c09e9f70c70 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Thu, 22 Mar 2018 11:52:39 -0400 Subject: [PATCH 21/21] Don't override parameters we actually get from the function call --- cozify/cloud.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index 7847298..fcdf072 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -81,8 +81,6 @@ def authenticate(trustCloud=True, trustHub=True, remote=False, autoremote=True): hub_info = None hub_ip = None - remote = None - autoremote = None if not hub.exists(hub_id): autoremote = True else: