""" Copyright 2015, 2016 Jordan Husney Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import requests from base64 import b64encode import re try: from urllib import urlencode except ImportError: from urllib.parse import urlencode try: from urlparse import urlparse, parse_qsl, urlunparse except ImportError: from urllib.parse import urlparse, parse_qsl, urlunparse __version__ = "0.3.1" class TokenProvider(object): __key = None __secret = None __token = None def __init__(self, key, secret): """Handles token authentication for Neurio Client. Args: key (string): your Neurio API key secret (string): your Neurio API secret """ self.__key = key self.__secret = secret if self.__key is None or self.__secret is None: raise ValueError("Key and secret must be set.") def get_token(self): """Performs Neurio API token authentication using provided key and secret. Note: This method is generally not called by hand; rather it is usually called as-needed by a Neurio Client object. Returns: string: the access token """ if self.__token is not None: return self.__token url = "https://api.neur.io/v1/oauth2/token" creds = b64encode(":".join([self.__key,self.__secret]).encode()).decode() headers = { "Authorization": " ".join(["Basic", creds]), } payload = { "grant_type": "client_credentials" } r = requests.post(url, data=payload, headers=headers) self.__token = r.json()["access_token"] return self.__token class Client(object): __token = None def __init__(self, token_provider): """The Neurio API client. Args: token_provider (TokenProvider): object providing authentication services """ if token_provider is None: raise ValueError("token_provider is required") if not isinstance(token_provider, TokenProvider): raise ValueError("token_provider must be instance of TokenProvider") self.__token = token_provider.get_token() def __gen_headers(self): """Utility method adding authentication token to requests.""" headers = { "Authorization": " ".join(["Bearer", self.__token]) } return headers def __append_url_params(self, url, params): """Utility method formatting url request parameters.""" url_parts = list(urlparse(url)) query = dict(parse_qsl(url_parts[4])) query.update(params) url_parts[4] = urlencode(query) return urlunparse(url_parts) def get_appliance(self, appliance_id): """Get the information for a specified appliance Args: appliance_id (string): identifiying string of appliance Returns: list: dictionary object containing information about the specified appliance """ url = "https://api.neur.io/v1/appliances/%s"%(appliance_id) headers = self.__gen_headers() headers["Content-Type"] = "application/json" r = requests.get(url, headers=headers) return r.json() def get_appliances(self, location_id): """Get the appliances added for a specified location. Args: location_id (string): identifiying string of appliance Returns: list: dictionary objects containing appliances data """ url = "https://api.neur.io/v1/appliances" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "locationId": location_id, } url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_appliance_event_by_location(self, location_id, start, end, per_page=None, page=None, min_power=None): """Get appliance events by location Id. Args: location_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` start (string): ISO 8601 start time for getting the events of appliances. end (string): ISO 8601 stop time for getting the events of appliances. Cannot be larger than 1 day from start time min_power (string): The minimum average power (in watts) for filtering. Only events with an average power above this value will be returned. (default: 400) per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) Returns: list: dictionary objects containing appliance events meeting specified criteria """ url = "https://api.neur.io/v1/appliances/events" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "locationId": location_id, "start": start, "end": end } if min_power: params["minPower"] = min_power if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_appliance_event_after_time(self, location_id, since, per_page=None, page=None, min_power=None): """Get appliance events by location Id after defined time. Args: location_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` since (string): ISO 8601 start time for getting the events that are created or updated after it. Maxiumim value allowed is 1 day from the current time. min_power (string): The minimum average power (in watts) for filtering. Only events with an average power above this value will be returned. (default: 400) per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) Returns: list: dictionary objects containing appliance events meeting specified criteria """ url = "https://api.neur.io/v1/appliances/events" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "locationId": location_id, "since": since } if min_power: params["minPower"] = min_power if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_appliance_event_by_appliance(self, appliance_id, start, end, per_page=None, page=None, min_power=None): """Get appliance events by appliance Id. Args: appliance_id (string): hexadecimal id of the appliance to query, e.g. ``0x0013A20040B65FAD`` start (string): ISO 8601 start time for getting the events of appliances. end (string): ISO 8601 stop time for getting the events of appliances. Cannot be larger than 1 day from start time min_power (string): The minimum average power (in watts) for filtering. Only events with an average power above this value will be returned. (default: 400) per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) Returns: list: dictionary objects containing appliance events meeting specified criteria """ url = "https://api.neur.io/v1/appliances/events" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "applianceId": appliance_id, "start": start, "end": end } if min_power: params["minPower"] = min_power if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_appliance_stats_by_appliance(self, appliance_id, start, end, granularity=None, per_page=None, page=None, min_power=None): """Get appliance usage data for a single appliance within a given time range. Stats are generated by fetching appliance events that match the supplied criteria and then aggregating them together based on the granularity specified with the request. Note: This endpoint uses the location's time zone when generating time intervals for the stats, which is relevant if that time zone uses daylight saving time (some days will be 23 or 25 hours long). Args: appliance_id (string): hexadecimal id of the appliance to query, e.g. ``0x0013A20040B65FAD`` start (string): ISO 8601 start time for getting the events of appliances. end (string): ISO 8601 stop time for getting the events of appliances. Cannot be larger than 1 month from start time granularity (string): granularity of stats. If the granularity is 'unknown', the stats for the appliances between the start and end time is returned.; must be one of "minutes", "hours", "days", "weeks", "months", or "unknown" (default: days) min_power (string): The minimum average power (in watts) for filtering. Only events with an average power above this value will be returned. (default: 400) per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) Returns: list: dictionary objects containing appliance events meeting specified criteria """ url = "https://api.neur.io/v1/appliances/stats" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "applianceId": appliance_id, "start": start, "end": end } if granularity: params["granularity"] = granularity if min_power: params["minPower"] = min_power if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_appliance_stats_by_location(self, location_id, start, end, granularity=None, per_page=None, page=None, min_power=None): """Get appliance usage data for a given location within a given time range. Stats are generated by fetching appliance events that match the supplied criteria and then aggregating them together based on the granularity specified with the request. Note: This endpoint uses the location's time zone when generating time intervals for the stats, which is relevant if that time zone uses daylight saving time (some days will be 23 or 25 hours long). Args: location_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` start (string): ISO 8601 start time for getting the events of appliances. end (string): ISO 8601 stop time for getting the events of appliances. Cannot be larger than 1 month from start time granularity (string): granularity of stats. If the granularity is 'unknown', the stats for the appliances between the start and end time is returned.; must be one of "minutes", "hours", "days", "weeks", "months", or "unknown" (default: days) min_power (string): The minimum average power (in watts) for filtering. Only events with an average power above this value will be returned. (default: 400) per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) Returns: list: dictionary objects containing appliance events meeting specified criteria """ url = "https://api.neur.io/v1/appliances/stats" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "locationId": location_id, "start": start, "end": end } if granularity: params["granularity"] = granularity if min_power: params["minPower"] = min_power if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() @staticmethod def get_local_current_sample(ip): """Gets current sample from *local* Neurio device IP address. This is a static method. It doesn't require a token to authenticate. Note, call get_user_information to determine local Neurio IP addresses. Args: ip (string): address of local Neurio device Returns: dictionary object containing current sample information """ valid_ip_pat = re.compile( "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" ) if not valid_ip_pat.match(ip): raise ValueError("ip address invalid") url = "http://%s/current-sample" % (ip) headers = { "Content-Type": "application/json" } r = requests.get(url, headers=headers) return r.json() def get_samples_live(self, sensor_id, last=None): """Get recent samples, one sample per second for up to the last 2 minutes. Args: sensor_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` last (string): starting range, as ISO8601 timestamp Returns: list: dictionary objects containing sample data """ url = "https://api.neur.io/v1/samples/live" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "sensorId": sensor_id } if last: params["last"] = last url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_samples_live_last(self, sensor_id): """Get the last sample recorded by the sensor. Args: sensor_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` Returns: list: dictionary objects containing sample data """ url = "https://api.neur.io/v1/samples/live/last" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "sensorId": sensor_id } url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_samples(self, sensor_id, start, granularity, end=None, frequency=None, per_page=None, page=None, full=False): """Get a sensor's samples for a specified time interval. Args: sensor_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` start (string): ISO 8601 start time of sampling; depends on the ``granularity`` parameter value, the maximum supported time ranges are: 1 day for minutes or hours granularities, 1 month for days, 6 months for weeks, 1 year for months granularity, and 10 years for years granularity granularity (string): granularity of the sampled data; must be one of "minutes", "hours", "days", "weeks", "months", or "years" end (string, optional): ISO 8601 stop time for sampling; should be later than start time (default: the current time) frequency (string, optional): frequency of the sampled data (e.g. with granularity set to days, a value of 3 will result in a sample for every third day, should be a multiple of 5 when using minutes granularity) (default: 1) (example: "1, 5") per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) full (bool, optional): include additional information per sample (default: False) Returns: list: dictionary objects containing sample data """ url = "https://api.neur.io/v1/samples" if full: url = "https://api.neur.io/v1/samples/full" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "sensorId": sensor_id, "start": start, "granularity": granularity } if end: params["end"] = end if frequency: params["frequency"] = frequency if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_samples_stats(self, sensor_id, start, granularity, end=None, frequency=None, per_page=None, page=None): """Get brief stats for energy consumed in a given time interval. Note: Note: this endpoint uses the sensor location's time zone when generating time intervals for the stats, which is relevant if that time zone uses daylight saving time (some days will be 23 or 25 hours long). Args: sensor_id (string): hexadecimal id of the sensor to query, e.g. ``0x0013A20040B65FAD`` start (string): ISO 8601 start time of sampling; depends on the ``granularity`` parameter value, the maximum supported time ranges are: 1 day for minutes or hours granularities, 1 month for days, 6 months for weeks, 1 year for months granularity, and 10 years for years granularity granularity (string): granularity of the sampled data; must be one of "minutes", "hours", "days", "weeks", "months", or "years" end (string, optional): ISO 8601 stop time for sampling; should be later than start time (default: the current time) frequency (string, optional): frequency of the sampled data (e.g. with granularity set to days, a value of 3 will result in a sample for every third day, should be a multiple of 5 when using minutes granularity) (default: 1) (example: "1, 5") per_page (string, optional): the number of returned results per page (min 1, max 500) (default: 10) page (string, optional): the page number to return (min 1, max 100000) (default: 1) Returns: list: dictionary objects containing sample statistics data """ url = "https://api.neur.io/v1/samples/stats" headers = self.__gen_headers() headers["Content-Type"] = "application/json" params = { "sensorId": sensor_id, "start": start, "granularity": granularity } if end: params["end"] = end if frequency: params["frequency"] = frequency if per_page: params["perPage"] = per_page if page: params["page"] = page url = self.__append_url_params(url, params) r = requests.get(url, headers=headers) return r.json() def get_user_information(self): """Gets the current user information, including sensor ID Args: None Returns: dictionary object containing information about the current user """ url = "https://api.neur.io/v1/users/current" headers = self.__gen_headers() headers["Content-Type"] = "application/json" r = requests.get(url, headers=headers) return r.json()