From 599de4ec4a5a7ac4907cc676cf8802635e64e267 Mon Sep 17 00:00:00 2001 From: Artanicus Date: Sun, 4 Mar 2018 16:35:20 +0200 Subject: [PATCH] Move hub remoteness into the state. Enables a mix of remoteness on multiple hubs. Also further improves isolation between tests to lessen spilling over. --- cozify/cloud.py | 8 ++-- cozify/config.py | 8 +++- cozify/hub.py | 73 ++++++++++++++++++++++++++----------- cozify/test/fixtures.py | 20 ++++------ cozify/test/test_cloud.py | 6 +-- cozify/test/test_hub_api.py | 2 +- 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/cozify/cloud.py b/cozify/cloud.py index 181d33a..2eab10c 100644 --- a/cozify/cloud.py +++ b/cozify/cloud.py @@ -86,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. @@ -97,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: diff --git a/cozify/config.py b/cozify/config.py index 2f93d29..556e793 100644 --- a/cozify/config.py +++ b/cozify/config.py @@ -48,16 +48,20 @@ 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. diff --git a/cozify/hub.py b/cozify/hub.py index 2cc6cbd..a9bbf33 100644 --- a/cozify/hub.py +++ b/cozify/hub.py @@ -1,8 +1,6 @@ """Module for handling highlevel Cozify Hub operations. Attributes: - remote(bool): Selector to treat a hub as being outside the LAN, i.e. calls will be routed via the Cozify Cloud remote call system. Defaults to False. - autoremote(bool): Selector to autodetect hub LAN presence and flip to remote mode if needed. Defaults to True. capability(capability): Enum of known device capabilities. Alphabetically sorted, numeric value not guaranteed to stay constant between versions if new capabilities are added. """ @@ -15,9 +13,6 @@ from .Error import APIError -remote = False -autoremote = True - 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 MOISTURE MUTE NEXT ON_OFF PAUSE PLAY PREVIOUS PUSH_NOTIFICATION REMOTE_CONTROL SEEK SMOKE STOP TEMPERATURE TRANSITION TREBLE TWILIGHT USER_PRESENCE VOLUME') def getDevices(**kwargs): @@ -127,17 +122,15 @@ def _fill_kwargs(kwargs): """Check that common items are present in kwargs and fill them if not. Args: - kwargs(dict): kwargs dictionary to fill. + kwargs(dict): kwargs dictionary to fill. Operated on directly. - Returns: - dict: Replacement kwargs dictionary with basic values filled. """ - if 'remote' not in kwargs: - kwargs['remote'] = remote - if 'autoremote' not in kwargs: - kwargs['autoremote'] = autoremote 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: @@ -198,21 +191,30 @@ def getHubId(hub_name): return section[5:] # cut out "Hubs." return None -def _getAttr(hub_id, attr): - """Get hub state attributes by attr name. +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. Args: hub_id(str): Id of hub to query. The id is a string of hexadecimal sections used internally to represent a hub. attr(str): Name of hub attribute to retrieve + default: Optional default value to set for unset attributes. If no default is provided these raise an AttributeError. + boolean: Retrieve and return value as a boolean instead of string. Defaults to False. Returns: str: Value of attribute or exception on failure. """ section = 'Hubs.' + hub_id - if section in config.state and attr in config.state[section]: - return config.state[section][attr] + if section in config.state: + if attr not in config.state[section]: + if default is not None: + _setAttr(hub_id, attr, default) + else: + raise AttributeError('Attribute {0} not set for hub {1}'.format(attr, hub_id)) + if boolean: + return config.state.getboolean(section, attr) + else: + 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 + raise AttributeError("Hub id '{0}' not found in state.".format(hub_id)) def _setAttr(hub_id, attr, value, commit=True): """Set hub state attributes by hub_id and attr name @@ -223,6 +225,9 @@ def _setAttr(hub_id, attr, value, commit=True): value(str): Value to store commit(bool): True to commit state after set. Defaults to True. """ + if isinstance(value, bool): + value = str(value) + section = 'Hubs.' + hub_id if section in config.state: if attr not in config.state[section]: @@ -270,6 +275,32 @@ def token(hub_id, new_token=None): _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. + + Args: + 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 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. + + Args: + 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 for a hub with autoremote enabled. + """ + if new_state: + _setAttr(hub_id, 'autoremote', new_state) + return _getAttr(hub_id, 'autoremote', default=True, boolean=True) + def ping(**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() @@ -282,10 +313,8 @@ def ping(**kwargs): """ _fill_kwargs(kwargs) try: - # if we don't have a stored host then we assume the hub is remote TODO(artanicus): need a second test as well so a failed call will attempt to flip - if not kwargs['remote'] and kwargs['autoremote'] and not kwargs['host']: # TODO(artanicus): I'm not sure if the last condition makes sense - global remote - remote = True + 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) diff --git a/cozify/test/fixtures.py b/cozify/test/fixtures.py index 6f30e94..0b7265c 100755 --- a/cozify/test/fixtures.py +++ b/cozify/test/fixtures.py @@ -16,7 +16,7 @@ def default_hub(): barehub.name = hub.name(barehub.hub_id) barehub.host = hub.host(barehub.hub_id) barehub.token = hub.token(barehub.hub_id) - barehub.remote = hub.remote + barehub.remote = hub.remote(barehub_hub_id) return barehub @pytest.fixture @@ -26,9 +26,11 @@ def tmp_cloud(): @pytest.fixture def live_cloud(): - config.setStatePath() # reset to default + configfile, configpath = tempfile.mkstemp() + config.setStatePath(configpath, copy_current=True) from cozify import cloud - return cloud + yield cloud + config.setStatePath() @pytest.fixture def id(): @@ -52,7 +54,6 @@ def live_hub(): config.dump_state() # dump state so it's visible in failed test output from cozify import hub yield hub - hub.remote = False # reset remote state at teardown class Tmp_cloud(): """Creates a temporary cloud state with test data. @@ -67,12 +68,12 @@ def __init__(self): self.iso_now = self.now.isoformat().split(".")[0] self.yesterday = self.now - datetime.timedelta(days=1) self.iso_yesterday = self.yesterday.isoformat().split(".")[0] - def __enter__(self): config.setStatePath(self.configpath) from cozify import cloud cloud._setAttr('email', self.email) cloud._setAttr('remotetoken', self.token) cloud._setAttr('last_refresh', self.iso_yesterday) + def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): os.remove(self.configpath) @@ -90,21 +91,16 @@ def __init__(self): self.host = '127.0.0.1' self.section = 'Hubs.{0}'.format(self.id) self.token = 'eyJkb20iOiJ1ayIsImFsZyI6IkhTNTEyIiwidHlwIjoiSldUIn0.eyJyb2xlIjo4LCJpYXQiOjE1MTI5ODg5NjksImV4cCI6MTUxNTQwODc2OSwidXNlcl9pZCI6ImRlYWRiZWVmLWFhYWEtYmJiYi1jY2NjLWRkZGRkZGRkZGRkZCIsImtpZCI6ImRlYWRiZWVmLWRkZGQtY2NjYy1iYmJiLWFhYWFhYWFhYWFhYSIsImlzcyI6IkNsb3VkIn0.QVKKYyfTJPks_BXeKs23uvslkcGGQnBTKodA-UGjgHg' # valid but useless jwt token. - def __enter__(self): self.cloud = Tmp_cloud() # this also initializes temporary state config.state.add_section(self.section) config.state[self.section]['hubname'] = self.name config.state[self.section]['host'] = self.host config.state[self.section]['hubtoken'] = self.token config.state['Hubs']['default'] = self.id - print('Temporary state:') - config.dump_state() + def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - if exc_type is not None: - logging.error("%s, %s, %s" % (exc_type, exc_value, traceback)) - return False - config.state.remove_section(self.section) + config.setStatePath() def devices(self): return dev.device_ids, dev.devices diff --git a/cozify/test/test_cloud.py b/cozify/test/test_cloud.py index da0375e..9a1e906 100755 --- a/cozify/test/test_cloud.py +++ b/cozify/test/test_cloud.py @@ -39,9 +39,9 @@ def test_cloud_refresh_expiry_not_over(tmp_cloud): ## integration tests for remote @pytest.mark.live -def test_cloud_remote_match(live_cloud): +def test_cloud_remote_match(live_cloud, live_hub): config.dump_state() - local_tz = hub.tz() - remote_tz = hub.tz(remote=True) + local_tz = live_hub.tz() + remote_tz = live_hub.tz(remote=True) assert local_tz == remote_tz diff --git a/cozify/test/test_hub_api.py b/cozify/test/test_hub_api.py index 665bbdd..2d5b85a 100755 --- a/cozify/test/test_hub_api.py +++ b/cozify/test/test_hub_api.py @@ -11,7 +11,7 @@ def test_hub(live_cloud, live_hub): assert hub_api.hub( hub_id = hub_id, host = live_hub.host(hub_id), - remote = live_hub.remote, + remote = live_hub.remote(hub_id), cloud_token = live_cloud.token(), hub_token = live_hub.token(hub_id) )