Example #1
0
 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)
Example #2
0
    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)
Example #3
0
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
Example #4
0
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)
Example #5
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)
        if self._poll_on_start:
            self._run_config_update()
Example #6
0
    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 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)
Example #8
0
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
Example #9
0
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")