Skip to content

Commit

Permalink
Move hub remoteness into the state. Enables a mix of remoteness on mu…
Browse files Browse the repository at this point in the history
…ltiple hubs.

Also further improves isolation between tests to lessen spilling over.
  • Loading branch information
jinnatar committed Mar 4, 2018
1 parent 046725c commit 599de4e
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 44 deletions.
8 changes: 4 additions & 4 deletions cozify/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions cozify/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
73 changes: 51 additions & 22 deletions cozify/hub.py
Original file line number Diff line number Diff line change
@@ -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.
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
20 changes: 8 additions & 12 deletions cozify/test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
6 changes: 3 additions & 3 deletions cozify/test/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion cozify/test/test_hub_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)

0 comments on commit 599de4e

Please sign in to comment.