def start_notify(self): # Notify the first signals right away self._notify() # Repeat notify every interval until cancelled self._notify_job = Job( self._notify, timedelta(seconds=self.interval.total_seconds()), True)
def start(self): """ Starts the block Begin the job to refresh the auth token every hour """ super().start() self._refresh_job = Job(self._refresh_auth, timedelta(seconds=self.authRefresh()), True)
class FirebaseBase(Base): config = ObjectProperty(FirebaseAuthProperty, title="Authentication", order=0) collection = StringProperty(title='Database Collection', default='[[FIREBASE_COLLECTION]]', order=3) userEmail = StringProperty(title='Authenticated User Email', default='[[USER_EMAIL]]', order=1) userPassword = StringProperty(title='Authenticated User Password', default='[[USER_PASSWORD]]', order=2) authRefresh = IntProperty(title='Auth Refresh Interval', default=1800, advanced=True, order=4) def __init__(self): super().__init__() self.user = None self.db = None self.auth = None self._refresh_job = None def configure(self, context): super().configure(context) self._create_firebase() def start(self): """ Starts the block Begin the job to refresh the auth token every hour """ super().start() self._refresh_job = Job(self._refresh_auth, timedelta(seconds=self.authRefresh()), True) def stop(self): """ Stops the block Cancel the token refresh job """ self._refresh_job.cancel() super().stop() def _refresh_auth(self): self.logger.info("Refeshing user token") self.user = self.auth.refresh(self.user['refreshToken']) def _create_firebase(self): firebase = pyrebase.initialize_app(self.config().get_auth_object()) self.auth = firebase.auth() self.user = self.auth.sign_in_with_email_and_password(self.userEmail(), self.userPassword()) self.db = firebase.database() def _get_collection(self, signal=None): collection = self.collection(signal) if not collection.startswith('/'): return "/{}".format(collection) else: return collection
class StaggerData(object): """ A class containing an interval and a stack of signals to notify """ def __init__(self, interval, num_groups, signals, notify_signals, logger): self.interval = interval self.num_groups = num_groups try: self.signals = deepcopy(signals) except: self.signals = copy(signals) self.notify_signals = notify_signals self.logger = logger self._build_deque() def start_notify(self): # Notify the first signals right away self._notify() # Repeat notify every interval until cancelled self._notify_job = Job( self._notify, timedelta(seconds=self.interval.total_seconds()), True) def _notify(self): if not self.signals_deque: self._notify_job.cancel() else: sigs_out = self.signals_deque.popleft() self.logger.debug("Notifying {} signals".format(len(sigs_out))) self.notify_signals(sigs_out) def _build_deque(self): """ Build the stack of signals based on number of groups we want """ self.signals_deque = deque() signals_included = 0 signals_per_group = len(self.signals) / self.num_groups for group_num in range(self.num_groups): # How many we should have after this iteration total_expected = (group_num + 1) * signals_per_group # Take what we expect to have and subtract the number of signals # we already have to determine how many to add for this iteration. # Round the number to space out uneven intervals signals_this_time = round(total_expected - signals_included) # Make sure to account for the signals we just added signals_included += signals_this_time # Build a list of signals for this interval and push it on to the # stack - pop the signals off the original list signals_to_include = list() for sig in range(signals_this_time): signals_to_include.append(self.signals.pop(0)) self.signals_deque.append(signals_to_include)
def start(self): """ Starts component Instantiates DeploymentHandler and DeploymentProxy Begins polling job if it is set """ super().start() self._api_proxy = DeploymentProxy(self._config_api_url_prefix, self) self._config_handler = DeploymentHandler(self) self._rest_manager.add_web_handler(self._config_handler) if self._poll_interval: self._poll_job = Job(self._run_config_update, timedelta(seconds=self._poll_interval), True) if self._poll_on_start: self._run_config_update()
def start(self): """ Starts component Instantiates DeploymentHandler and DeploymentProxy Begins polling job if it is set """ super().start() self._api_proxy = DeploymentProxy(self._config_api_url_prefix, self) self._config_handler = DeploymentHandler(self) self._rest_manager.add_web_handler(self._config_handler) if self._poll_interval: self._poll_job = Job(self._run_config_update, timedelta(seconds=self._poll_interval), True)
class FirebaseBase(Base): config = ObjectProperty(FirebaseAuthProperty, title="Authentication", order=0) collection = StringProperty(title='Database Collection', default='[[FIREBASE_COLLECTION]]', order=3) userEmail = StringProperty(title='Authenticated User Email', default='[[USER_EMAIL]]', order=1) userPassword = StringProperty(title='Authenticated User Password', default='[[USER_PASSWORD]]', order=2) authRefresh = IntProperty(title='Auth Refresh Interval', default=1800, advanced=True, order=4) def __init__(self): super().__init__() self.user = None self.db = None self.auth = None self._refresh_job = None def configure(self, context): super().configure(context) self._create_firebase() def start(self): """ Starts the block Begin the job to refresh the auth token every hour """ super().start() self._refresh_job = Job(self._refresh_auth, timedelta(seconds=self.authRefresh()), True) def stop(self): """ Stops the block Cancel the token refresh job """ self._refresh_job.cancel() super().stop() def _refresh_auth(self): self.logger.info("Refeshing user token") self.user = self.auth.refresh(self.user['refreshToken']) def _create_firebase(self): firebase = pyrebase.initialize_app(self.config().get_auth_object()) self.auth = firebase.auth() self.user = self.auth.sign_in_with_email_and_password( self.userEmail(), self.userPassword()) self.db = firebase.database() def _get_collection(self, signal=None): collection = self.collection(signal) if not collection.startswith('/'): return "/{}".format(collection) else: return collection
class DeploymentManager(CoreComponent): """ Handle configuration updates """ class Status(Enum): started = 1 accepted = 2 in_progress = 3 success = 4 failure = 5 _name = "DeploymentManager" def __init__(self): super().__init__() self._rest_manager = None self._config_handler = None self._api_proxy = None self._configuration_manager = None self._config_api_url_prefix = None self._config_id = None self._config_version_id = None self._poll_job = None self._poll = None self._poll_interval = None self._poll_on_start = None self._start_stop_services = None self._delete_missing = None def configure(self, context): """ Configures component Establish dependency to RESTManager Fetches settings Args: context (CoreContext): component initialization context """ super().configure(context) # Register dependency to rest manager self._rest_manager = self.get_dependency('RESTManager') self._configuration_manager = \ self.get_dependency('ConfigurationManager') self._api_key_manager = self.get_dependency('APIKeyManager') # fetch component settings self._config_api_url_prefix = \ Settings.get("configuration", "config_api_url_prefix", fallback="https://api.n.io/v1") self._config_id = Persistence().load("configuration_id", default=Settings.get( "configuration", "config_id")) self._config_version_id = Persistence().load( "configuration_version_id", default=Settings.get("configuration", "config_version_id")) self._start_stop_services = Settings.getboolean("configuration", "start_stop_services", fallback=True) self._delete_missing = Settings.getboolean("configuration", "delete_missing", fallback=True) self._poll_interval = Settings.getint("configuration", "config_poll_interval", fallback=0) self._poll_on_start = Settings.getboolean("configuration", "config_poll_on_start", fallback=False) def start(self): """ Starts component Instantiates DeploymentHandler and DeploymentProxy Begins polling job if it is set """ super().start() self._api_proxy = DeploymentProxy(self._config_api_url_prefix, self) self._config_handler = DeploymentHandler(self) self._rest_manager.add_web_handler(self._config_handler) if self._poll_interval: self._poll_job = Job(self._run_config_update, timedelta(seconds=self._poll_interval), True) if self._poll_on_start: self._run_config_update() def stop(self): """ Stops component """ self._rest_manager.remove_web_handler(self._config_handler) if self._poll_job: self._poll_job.cancel() self._poll_job = None super().stop() @property def api_key(self): return self._api_key_manager.api_key @property def instance_id(self): return self._api_key_manager.instance_id def _run_config_update(self): """Callback function to run update at each polling interval """ self.logger.debug("Checking for latest configuration") # Poll the product api for config ids this instance # should be running ids = self._api_proxy.get_instance_config_ids() if ids is None: # It didn't report any IDs to update to, so ignore return self.logger.debug("Desired configuration: {}".format(ids)) config_id = ids.get("instance_configuration_id") config_version_id = ids.get("instance_configuration_version_id") if config_id == self.config_id and \ config_version_id == self.config_version_id: self.logger.debug( "No change detected from current version, skipping") return self.logger.info( "New configuration detected...updating to config ID {} " "version {}".format(config_id, config_version_id)) deployment_id = ids.get("deployment_id") result = self.update_configuration(config_id, config_version_id, deployment_id) self.logger.info("Configuration was updated: {}".format(result)) def update_configuration(self, config_id, config_version_id, deployment_id): """ Update this instance to a given config/version ID. Args: config_id: The ID of the instance configuration to use config_version_id: The version ID of the instance config deployment_id: The deployment ID to set the status for Returns: result (dict): The result of the instance update call """ # grab new configuration configuration = self._api_proxy.get_configuration( config_id, config_version_id) if configuration is None or "configuration_data" not in configuration: msg = "configuration_data entry missing in nio API return" self.logger.error(msg) raise RuntimeError(msg) configuration_data = json.loads(configuration["configuration_data"]) # notify configuration acceptance self._api_proxy.set_reported_configuration( config_id, config_version_id, deployment_id, self.Status.accepted.name, "Services and Blocks configuration was accepted, " "proceeding with update") # perform update result = self._configuration_manager.update(configuration_data, self._start_stop_services, self._delete_missing) # instance is now running this configuration so persist this fact self.config_id = config_id self.config_version_id = config_version_id error_messages = self._get_potential_errors_messages(result) if error_messages: # notify failure self._api_proxy.set_reported_configuration( config_id, config_version_id, deployment_id, self.Status.failure.name, "Failed to update, these errors were encountered: {}".format( error_messages)) else: # report success and new instance config ids self._api_proxy.set_reported_configuration( config_id, config_version_id, deployment_id, self.Status.success.name, "Successfully updated services and blocks") self.logger.info("Configuration was updated, {}".format(result)) return result def _get_potential_errors_messages(self, result): """Return any error messages contained in a result""" messages = [] for key in result.keys(): errors = result.get(key, {}).get("error", []) if errors: message = "Failed to install {}".format(key) for error in errors: if isinstance(error, dict): message += ", {}".format(json.dumps(error)) # providing API should send errors as dicts but just in # case it does not, at least attempt to convert to str else: message += ", " + str(error) messages.append(message) # do not let errors go unnoticed self.logger.error("{} error: {}".format(key, errors)) str_messages = ",".join(messages) if messages: self.logger.error( "{} errors were encountered during update: {}".format( len(messages), str_messages)) return str_messages @property def config_id(self): return self._config_id @config_id.setter def config_id(self, config_id): if self._config_id != config_id: self.logger.debug("Configuration ID set to: {}".format(config_id)) self._config_id = config_id # persist value so that it can be read eventually # when component starts again Persistence().save(self.config_id, "configuration_id") @property def config_version_id(self): return self._config_version_id @config_version_id.setter def config_version_id(self, config_version_id): if self._config_version_id != config_version_id: self.logger.debug("Configuration Version ID set to: {}".format( config_version_id)) self._config_version_id = config_version_id # persist value so that it can be read eventually # when component starts again Persistence().save(self.config_version_id, "configuration_version_id")
def start(self): super().start() self._refresh_job = Job(self.refresh_auth_token, timedelta(seconds=3000), True)
class EcobeeThermostat(Retry, EnrichSignals, Persistence, Block): app_key = StringProperty(title='App Key', default='[[ECOBEE_APP_KEY]]') refresh_token = StringProperty(title='Initial Refresh Token', default='[[ECOBEE_REFRESH_TOKEN]]') desired_temp = FloatProperty(title='Desired Temperature', default='{{ $temp }}') version = VersionProperty('0.0.2') def __init__(self): super().__init__() self._auth_token = None self._refresh_token = None self._refresh_job = None def configure(self, context): super().configure(context) # Try to use the persisted refresh token, otherwise use the block # configuration's initial refresh token if self._refresh_token is None: self._refresh_token = self.refresh_token() self.refresh_auth_token() def start(self): super().start() self._refresh_job = Job(self.refresh_auth_token, timedelta(seconds=3000), True) def stop(self): if self._refresh_job: self._refresh_job.cancel() super().stop() def persisted_values(self): return ["_refresh_token"] def refresh_auth_token(self): self.logger.info("Fetching access token...") self.logger.debug("Using refresh token {}".format(self._refresh_token)) token_body = requests.post( 'https://api.ecobee.com/token', params={ 'grant_type': 'refresh_token', 'code': self._refresh_token, 'client_id': self.app_key(), }, ).json() self._auth_token = token_body['access_token'] self._refresh_token = token_body['refresh_token'] self.logger.info("Fetched access token : {}".format(token_body)) def before_retry(self, *args, **kwargs): super().before_retry(*args, **kwargs) self.refresh_auth_token() def process_signals(self, signals, input_id='read'): out_signals = [] output_id = None for signal in signals: if input_id == 'read': output_id = 'temps' out_sig = self.get_output_signal( self.fetch_thermostats(signal), signal) elif input_id == 'set': output_id = 'set_status' desired_temp = self.desired_temp(signal) out_sig = self.get_output_signal(self.set_temp(desired_temp), signal) if out_sig: out_signals.append(out_sig) self.notify_signals(out_signals, output_id=output_id) def fetch_thermostats(self, signal=None): therms = self.execute_with_retry(self._make_ecobee_request, 'thermostat', method='get', body={ 'selection': { 'selectionType': 'registered', 'selectionMatch': '', 'includeRuntime': True, 'includeSettings': True, } }) return therms.json() def set_temp(self, temp): self.logger.info("Setting thermostat to {} degrees".format(temp)) result = self.execute_with_retry(self._make_ecobee_request, 'thermostat', method='post', body={ "selection": { "selectionType": "registered", "selectionMatch": "" }, "functions": [{ "type": "setHold", "params": { "holdType": "nextTransition", "heatHoldTemp": int(temp * 10), "coolHoldTemp": int(temp * 10) } }] }) return result.json() def _make_ecobee_request(self, endpoint, method='get', body={}): headers = { 'Authorization': 'Bearer {}'.format(self._auth_token), } params = { 'format': 'json', } if method == 'get': params['body'] = json.dumps(body) post_body = None else: post_body = body resp = getattr(requests, method)( 'https://api.ecobee.com/1/{}'.format(endpoint), headers=headers, params=params, json=post_body, ) resp.raise_for_status() return resp
class DeploymentManager(CoreComponent): """ Handle configuration updates """ class Status(Enum): started = 1 accepted = 2 in_progress = 3 success = 4 failure = 5 _name = "DeploymentManager" def __init__(self): super().__init__() self._rest_manager = None self._config_handler = None self._api_proxy = None self._configuration_manager = None self._config_api_url_prefix = None self._config_id = None self._config_version_id = None self._poll_job = None self._poll = None self._poll_interval = None self._configuration_manager = None self._start_stop_services = None self._delete_missing = None def configure(self, context): """ Configures component Establish dependency to RESTManager Fetches settings Args: context (CoreContext): component initialization context """ super().configure(context) # Register dependency to rest manager self._rest_manager = self.get_dependency('RESTManager') self._configuration_manager = \ self.get_dependency('ConfigurationManager') self._api_key_manager = self.get_dependency('APIKeyManager') # fetch component settings self._config_api_url_prefix = \ Settings.get("configuration", "config_api_url_prefix", fallback="https://api.n.io/v1") self._config_id = Persistence().load( "configuration_id", default=Settings.get("configuration", "config_id")) self._config_version_id = Persistence().load( "configuration_version_id", default=Settings.get("configuration", "config_version_id")) self._start_stop_services = Settings.getboolean( "configuration", "start_stop_services", fallback=True) self._delete_missing = Settings.getboolean( "configuration", "delete_missing", fallback=True) self._poll_interval = Settings.getint( "configuration", "config_poll_interval", fallback=0) def start(self): """ Starts component Instantiates DeploymentHandler and DeploymentProxy Begins polling job if it is set """ super().start() self._api_proxy = DeploymentProxy(self._config_api_url_prefix, self) self._config_handler = DeploymentHandler(self) self._rest_manager.add_web_handler(self._config_handler) if self._poll_interval: self._poll_job = Job(self._run_config_update, timedelta(seconds=self._poll_interval), True) def stop(self): """ Stops component """ self._rest_manager.remove_web_handler(self._config_handler) if self._poll_job: self._poll_job.cancel() self._poll_job = None super().stop() @property def api_key(self): return self._api_key_manager.api_key @property def instance_id(self): return self._api_key_manager.instance_id def _run_config_update(self): """Callback function to run update at each polling interval """ self.logger.debug("Checking for latest configuration") # Poll the product api for config ids this instance # should be running ids = self._api_proxy.get_instance_config_ids() if ids is None: # It didn't report any IDs to update to, so ignore return self.logger.debug("Desired configuration: {}".format(ids)) config_id = ids.get("instance_configuration_id") config_version_id = ids.get("instance_configuration_version_id") if config_id == self.config_id and \ config_version_id == self.config_version_id: self.logger.debug( "No change detected from current version, skipping") return self.logger.info( "New configuration detected...updating to config ID {} " "version {}".format(config_id, config_version_id)) deployment_id = ids.get("deployment_id") result = self.update_configuration( config_id, config_version_id, deployment_id) self.logger.info("Configuration was updated: {}".format(result)) def update_configuration( self, config_id, config_version_id, deployment_id): """ Update this instance to a given config/version ID. Args: config_id: The ID of the instance configuration to use config_version_id: The version ID of the instance config deployment_id: The deployment ID to set the status for Returns: result (dict): The result of the instance update call """ # grab new configuration configuration = self._api_proxy.get_configuration( config_id, config_version_id) if configuration is None or "configuration_data" not in configuration: msg = "configuration_data entry missing in nio API return" self.logger.error(msg) raise RuntimeError(msg) configuration_data = json.loads(configuration["configuration_data"]) # notify configuration acceptance self._api_proxy.set_reported_configuration( config_id, config_version_id, deployment_id, self.Status.accepted.name, "Services and Blocks configuration was accepted, " "proceeding with update") # perform update result = self._configuration_manager.update( configuration_data, self._start_stop_services, self._delete_missing) # instance is now running this configuration so persist this fact self.config_id = config_id self.config_version_id = config_version_id error_messages = self._get_potential_errors_messages(result) if error_messages: # notify failure self._api_proxy.set_reported_configuration( config_id, config_version_id, deployment_id, self.Status.failure.name, "Failed to update, these errors were encountered: {}".format( error_messages)) else: # report success and new instance config ids self._api_proxy.set_reported_configuration( config_id, config_version_id, deployment_id, self.Status.success.name, "Successfully updated services and blocks") self.logger.info("Configuration was updated, {}".format(result)) return result def _get_potential_errors_messages(self, result): """Return any error messages contained in a result""" messages = [] for key in result.keys(): errors = result.get(key, {}).get("error", []) if errors: message = "Failed to install {}".format(key) for error in errors: if isinstance(error, dict): message += ", {}".format(json.dumps(error)) # providing API should send errors as dicts but just in # case it does not, at least attempt to convert to str else: message += ", " + str(error) messages.append(message) # do not let errors go unnoticed self.logger.error("{} error: {}".format(key, errors)) str_messages = ",".join(messages) if messages: self.logger.error( "{} errors were encountered during update: {}".format( len(messages), str_messages)) return str_messages @property def config_id(self): return self._config_id @config_id.setter def config_id(self, config_id): if self._config_id != config_id: self.logger.debug("Configuration ID set to: {}".format(config_id)) self._config_id = config_id # persist value so that it can be read eventually # when component starts again Persistence().save(self.config_id, "configuration_id") @property def config_version_id(self): return self._config_version_id @config_version_id.setter def config_version_id(self, config_version_id): if self._config_version_id != config_version_id: self.logger.debug("Configuration Version ID set to: {}". format(config_version_id)) self._config_version_id = config_version_id # persist value so that it can be read eventually # when component starts again Persistence().save( self.config_version_id, "configuration_version_id")