def __init__(self, value, unit, utc): """constructor. Parameters ---------- value: float (required) the measurement reading numeric value value: string (required) the measurement reading unit utc: string (required) the UTC timestamp """ if not is_number(value): raise IllegalArgumentException("value is invalid.") if is_blank(unit): raise IllegalArgumentException("unit is required.") if is_blank(utc): raise IllegalArgumentException("utc is required.") self.value = value self.unit = unit self.utc = utc return
def convert(from_value, from_unit, to_unit): """convert length units. Parameters ---------- from_value: str (required) numeric value from_unit: str (required) length unit to_unit: str (required) length unit Returns ------- float: the converted value """ if not is_number(from_value): raise IllegalArgumentException( ("Parameter from_value=[{0}] not valid. " "A numeric value must be provided.").format(from_value)) if is_blank(from_unit): raise IllegalArgumentException( ("Parameter from_unit=[{0}] not valid. A unit be provided.") .format(from_unit)) if is_blank(to_unit): raise IllegalArgumentException( ("Parameter to_unit=[{0}] not valid. A unit be provided.") .format(to_unit)) if from_unit == to_unit: raise IllegalArgumentException( ("from_unit=[{0}] and to_unit=[{1}] units cannot be equal") .format(from_unit, to_unit)) # pint temperature units need to be lower-cased or degC, degF, degK from_unit = from_unit.lower().strip() if from_unit == "degc": from_unit = "degC" elif from_unit == "degf": from_unit = "degF" elif from_unit == "degk": from_unit = "degK" # pint temperature units need to be lower-cased or degC, degF, degK to_unit = to_unit.lower().strip() if to_unit == "degc": to_unit = "degC" elif to_unit == "degf": to_unit = "degF" elif to_unit == "degk": to_unit = "degK" result = convert_unit(UNIT_TYPES_ENUM.temperature, from_unit, from_value, to_unit) return result
def send_text(phone_number, message): """Sends given SMS message using http://textbelt.com/ Parameters ---------- phone_number: str (required) recipient numeric string for a valid phone number. It cannot be greater than 20 digits message: str (optional) message to send. It cannot be greater than 40 characters. Returns ------- nothing. """ if is_blank(phone_number): raise IllegalArgumentException("phone_number is required") if not is_number(phone_number): raise IllegalArgumentException( "phone_number [{0}] needs to be a number." .format(phone_number)) if len(phone_number.strip()) > SMS.MAX_PHONE_LENGTH: raise IllegalArgumentException( "phone_number [{0}] cannot be greater than [{1}]." .format(phone_number, SMS.MAX_PHONE_LENGTH)) if is_blank(message) : raise IllegalArgumentException ("message cannot blank") if len(message.strip()) > SMS.MAX_MSG_LENGTH: raise IllegalArgumentException( "message [{0}] cannot be greater than [{1}]." .format(message, SMS.MAX_MSG_LENGTH)) url = 'http://textbelt.com/text' payload = {'number': phone_number, 'message': "iotgw.rubens.home: " + message.strip()} logging.debug("Sending SMS to [{0}] using url [{1}]" .format(phone_number, url)) r = requests.post(url, data=payload) if (r.status_code != 200): logging.debug("Failed SMS to [{0}] using url [{1}]: status [{2}]" .format(phone_number, url, r.status_code)) r.raise_for_status() return
def add_sensor(serial, geolocation, location, address, state, name, sensor_type, description): """ Adds given sensor data to sensor database Parameters ---------- serial: str (required) sensor unique serial number. geolocation: str (optional) GEO Location: LATITUDE, LONGITUDE location: str (optional) ENGINE, HOME, PATIO, ... address: str (optional) Address where sensor is located state: str (required) UP, DOWN, ... name: str (required) name to help identify this sensor sensor_type: str (required) HUMIDITY, PRESSURE, TEMPERATURE, VELOCITY, ... description: str(optional) helps describe this sensor Returns ------- nothing. """ if is_blank(serial): raise IllegalArgumentException("serial is required.") if is_blank(state): raise IllegalArgumentException("state is required.") if is_blank(name): raise IllegalArgumentException("name is required.") if is_blank(sensor_type): raise IllegalArgumentException("sensor_type is required.") test_sensor = SensorDAO.get_sensor(serial) if test_sensor is not None: raise IllegalArgumentException("sensor with serial [{0}] " "is already registered." .format(serial)) SensorDAO.SENSOR_DB.add_sensor(serial, geolocation, location, address, state, name, sensor_type, description) return
def handle_request_timeout(self, error): """ This method handles an exception error when the request times out. Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside handle_request_timeout") code = "" if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = "URL [{0}] request timed out".format(request.url) response = ErrorResponse.get_response(408, description, "application/json") response.headers["WWW-Authenticate"] = 'Basic realm = "IoT Gateway"' return response
def handle_internal_server_error(self, error): """ This method handles an internal server exception error. Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside handle_internal_server_error") code = 500 if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = "An internal server error occurred" if isinstance(error, Exception): logging.exception(error) response = ErrorResponse.get_response(code, description, "application/json") return response
def method_not_allowed(self, error): """ This method handles an exception error when the HTTP method sent is not supported. Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside method_not_allowed") code = "" if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) method = request.method # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = "The HTTP method [{0}] is not supported.".format(method) response = ErrorResponse.get_response(405, description, "application/json") return response
def handle_not_expected(self, error): """ This method handles an exception error for an HTTP status code that is currently not supported, and it was not expected. Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside handle_not_expected") code = "" if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = ("URL [{0}] request generate a not expected status" .format(request.url)) response = ErrorResponse.get_response(401, description, "application/json") response.headers["WWW-Authenticate"] = 'Basic realm = "IoT Gateway"' return response
def bad_request(self, error): """ This method handles an exception error when the HTTP request could not be served due to a malformed request (e.g., some parameters are not valid). Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside bad_request") code = "" if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = "Bad URL [{0}] request".format(request.url) response = ErrorResponse.get_response(400, description, "application/json") return response
def convert(from_value, from_unit, to_unit): """convert length units. Parameters ---------- from_value: float (required) numeric value from_unit: str (required) length unit to_unit: str (required) length unit Returns ------- float: the converted value """ if not is_number(from_value): raise IllegalArgumentException( ("Parameter from_value=[{0}] not valid. " "A numeric value must be provided.").format(from_value)) if is_blank(from_unit): raise IllegalArgumentException( ("Parameter from_unit=[{0}] not valid. A unit be provided.") .format(from_unit)) if is_blank(to_unit): raise IllegalArgumentException( ("Parameter to_unit=[{0}] not valid. A unit be provided.") .format(to_unit)) if from_unit == to_unit: raise IllegalArgumentException( ("from_unit=[{0}] and to_unit=[{1}] units cannot be equal") .format(from_unit, to_unit)) # pint unit registry only accepts lower case letters for length units from_unit = from_unit.lower().strip() to_unit = to_unit.lower().strip() result = convert_unit(UNIT_TYPES_ENUM.length, from_unit, from_value, to_unit) return result
def add_reading(unit, value, utc, serial): """ Adds given measurement data to sensor database Parameters ---------- unit: str (required) degF, degC, degF. value: float (required) a float number utc: str (required) UTC timestamp of the reading serial: str (required) sensor unique serial number Returns ------- nothing. """ if is_blank(unit): raise IllegalArgumentException("unit is required.") if not value: raise IllegalArgumentException("utc is required.") if not is_number(value): raise IllegalArgumentException("value is not a numeric value.") if is_blank(utc): raise IllegalArgumentException("utc is required.") if is_blank(serial): raise IllegalArgumentException("serial is required.") test_sensor = SensorDAO.get_sensor(serial) if test_sensor is None: raise IllegalArgumentException("sensor with serial [{0}] " "is not registered in the system." .format(serial)) SensorDAO.SENSOR_DB.add_reading(unit, value, utc, serial) return
def __init__(self, serial): """constructor. Parameters ---------- serial: str (required) sensor serial or identification string """ if is_blank(serial): raise IllegalArgumentException("serial is required.") self.serial = serial
def get_response(code, description, mime_type, title=None): """Returns an HTTP flask response Parameters: ---------- code: int (required) The HTTP status code between 400 and 500 (inclusive) description: string (required) The text description related to the status mime_type: string (optional) The text corresponding to the mime-type title: string (optional) The text of a title to be rendered at an HTML page Returns: ------- A Flask HTTP Response object Raises: ------ Raises TypeError or ValueError if argument is not valid. """ if not code or not isinstance(code, (int)): raise TypeError("code [{0}] is not valid".format(code)) elif (code < 400) or (code > 500): raise ValueError("code [{0}] is not valid. " "It must be between 400 and 500" .format(code)) elif is_blank(description): logging.warn("error description is blank") description = "no error description found" if re.search("html", mime_type, flags=re.IGNORECASE): body = ErrorResponse.get_html_response_body( code, description, "HTTP Status Code:{0}".format(code)) response = Response(body, status=code, mimetype="text/html") elif re.search("xml", mime_type, flags=re.IGNORECASE): body = ErrorResponse.get_xml_response_body(code, description) response = Response(body, status=code, mimetype="application/xml") elif re.search("csv", mime_type, flags=re.IGNORECASE): body = ErrorResponse.get_csv_response_body(code, description) response = Response(body, status=code, mimetype="text/csv") elif re.search("json", mime_type, flags=re.IGNORECASE): response = ErrorResponse.get_json_response(code, description) else: # default to JSON response = ErrorResponse.get_json_response(code, description) return response
def database(name): """ Returns a valid and connected Mongo Database object with the given name. Parameters: ---------- name: str (required) the name of the MongoDB database Returns ------- A Mongo Database instance. Raises ------ Raises MongoDB exception if MongoDB is not up and running, and the client could NOT connect to the database """ if is_blank(name): raise IllegalArgumentException("name is required.") client = MongoDB._client() db_found = False dbs = client.database_names() for db_name in dbs: if (db_name == name): db_found = True break if not db_found: client.close() raise IllegalArgumentException("MongoDB DB name [{0}] not found!" .format(name)) db = client['{0}'.format(name)] logging.debug("MongoDB with database name [{0}] found." .format(name)) return db
def del_readings(serial): """ Deletes all measurement data for the given serial Parameters ---------- serial: str (required) sensor unique serial number Returns ------- nothing. """ if is_blank(serial): raise IllegalArgumentException("serial is required.") SensorDAO.SENSOR_DB.del_readings(serial) return
def handle_not_acceptable(self, error): """ This method handles an exception error when none of the media types defined in the incoming HTTP Accept header is supported by this REST API. Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside handle_not_acceptable") code = "" if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) mimeType = request.accept_mimetypes.best logging.debug("Handling a 406 error [{0}]. " "The best mime type is [{1}]" .format(error, mimeType)) accept = request.headers['Accept'] # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = ("Requested MIME types [{0}] not supported." .format(accept)) response = ErrorResponse.get_response(406, description, mimeType) return response
def handle_not_found(self, error): """ This method handles an exception error when the requested URL resource was not found on the server. Parameters ---------- error: Exception object (required) the exception error object. Returns ------- an HTTP response object with appropriate error message. """ logging.debug("inside handle_not_found") code = "" if hasattr(error, 'code'): code = getattr(error, 'code') logging.debug("Handling a status_code [{0}] error [{1}]" .format(code, error)) url_not_found = request.url logging.debug("Handling a URL [{0}] not found error [{1}]" .format(url_not_found, error)) # if a description was found on the error, use that one instead description = get_error_description(error) if is_blank(description): description = ("Requested resource identified by [{0}] not found." .format(url_not_found)) response = ErrorResponse.get_response(404, description, "application/json") return response
def get_sensor(serial): """ Returns a tuble corresponding to the sensor table in the database for the given sensor serial. Parameters ---------- serial: str (required) sensor unique serial number Returns ------- dict: A sensor dictionary containing column names as keys, and corresponding values. """ if is_blank(serial): raise IllegalArgumentException("serial is required.") data = SensorDAO.SENSOR_DB.get_sensor(serial) return data
def initialize_environment(ini_file_path, log_file_path=None): """ Loads the configuration file and initializes logging. This method should only be called once at the start up of the application to read in the configuration file, and set up logging. Parameters: ---------- ini_file_path: str (required) the full path to the application INI configuration file log_file_path: str (optional) aternate path to the application logging file. The default log file path is defined in the INI configuration file """ if is_blank(ini_file_path): raise IllegalArgumentException("ini_file_path is required.") ini_config.read(ini_file_path) # ensure working dir folder is available working_dir = ini_config.get("Logging", "WORKING_DIR") if not os.path.exists(working_dir): os.makedirs(working_dir, 0o775) # log file fullname including its path if log_file_path is None: log_file_path = ini_config.get("Logging", "LOG_FILE") # ensure log file is writeable log_file = open(log_file_path.encode("unicode-escape"), "w") log_file.close() max_bytes = ini_config.getint("Logging", "LOG_FILE_MAX_BYTES") backup_count = ini_config.getint("Logging", "LOG_BACKUP_COUNT") log_level = ini_config.get("Logging", "LOG_LEVEL") # define the log level level = logging.INFO if log_level.upper().strip() == "CRITICAL": level = logging.CRITICAL elif log_level.upper().strip() == "ERROR": level = logging.ERROR elif log_level.upper().strip() == "WARNING": level = logging.WARNING elif log_level.upper().strip() == "INFO": level = logging.INFO elif log_level.upper().strip() == "DEBUG": level = logging.DEBUG else: raise ConfigurationException("LOG_LEVEL [{0}] not valid in [{1}]." .format(log_level, log_file_path)) # create logging file handler log_file_handler = RotatingFileHandler( log_file_path, maxBytes=max_bytes, backupCount=backup_count ) log_file_handler.setLevel(level) log_format = ("%(asctime)s - %(name)s - %(funcName)s:%(lineno)d " "- %(levelname)s - %(message)s") date_fmt = "%m/%d/%Y %I:%M:%S %p" # create logging formatter handler and add it to log handler formatter = logging.Formatter(log_format, date_fmt) log_file_handler.setFormatter(formatter) logging.getLogger().addHandler(log_file_handler) logging.getLogger().setLevel(level) return
def publish_temperature(serial): """ Publishes the given temperature sensor data to MQTT message broker. Parameters ---------- serial: str (required) sensor serial number Returns ------- """ # test serial if is_blank(serial): raise IllegalArgumentException("serial is required") mqtt_id = ini_config.get("MQTT", "MQTT_CLIENT_ID") user = ini_config.get("MQTT", "MQTT_USERNAME") pw = ini_config.get("MQTT", "MQTT_PASSWORD") host = ini_config.get("MQTT", "MQTT_HOST") port = ini_config.getint("MQTT", "MQTT_PORT") topic = ini_config.get("MQTT", "MQTT_TOPIC") mqttc = mqtt.Client(client_id=mqtt_id, clean_session=True, protocol=mqtt.MQTTv31) mqttc.username_pw_set(user, password=pw) sensor_temperature = DS18B20Sensor(serial) readings = sensor_temperature.get_measurement() message = OrderedDict() message["value"] = readings.get_value() message["unit"] = readings.get_unit() message["utc"] = readings.get_utc() json_message = json.dumps(message, indent=2, sort_keys=True) auth = OrderedDict() auth["username"] = user auth["password"] = pw logging.debug("Publishing to MQTT Broker: " "host [{0}], port [{1}], client id [{2}], " "user [{3}], sensor serial [{4}]" .format(host, port, mqtt_id, user, serial)) publish.single(topic, payload=json_message, qos=0, retain=False, hostname=host, port=port, client_id=mqtt_id, keepalive=20, auth=auth) logging.debug("Message [{0}] was published correctly: " "host [{1}], port [{2}], client id [{3}], " "user [{4}], sensor serial [{5}]" .format(message, host, port, mqtt_id, user, serial)) return
def send_email(recipient, subject, message): """Sends given message using Gmail credentials defined in the configuration file. Parameters ---------- recipient: str (required) recipient email address subject: str (optional) email subject message: str (optional) message to send Returns ------- nothing. Raises ------ SMTPHeloError: The server didn't reply properly to the helo greeting. SMTPAuthenticationError: The server didn't accept the username/password combination. SMTPException: No suitable authentication method was found. SMTPRecipientsRefused: The server rejected ALL recipients (no mail was sent). SMTPSenderRefused: The server didn't accept the from_addr. SMTPDataError: The server replied with an unexpected error code (other than a refusal of a recipient). """ if is_blank (recipient): raise IllegalArgumentException("recipient is required.") # ensure valid GMail Account gmail_account = ini_config.get("Email", "GMAIL_ACCOUNT") is_valid = validate_email(gmail_account) if(not is_valid): raise IllegalArgumentException( "GMail Account [{0}] is not valid email address" .format(gmail_account)) gmail_user = ini_config.get("Email", "GMAIL_ACCOUNT") gmail_password = ini_config.get("Email", "GMAIL_PASSWORD") logging.debug("Sending email using Gmail account [{0}] " "to recipient [{1}]" .format(gmail_user, recipient)) is_valid = validate_email(recipient) if(not is_valid): raise IllegalArgumentException( "Recipient [{0}] is not valid email address" .format(recipient)) msg = MIMEMultipart() msg['From'] = gmail_user msg['To'] = recipient msg['Subject'] = subject msg.attach(MIMEText(message)) logging.debug("Sending email to [{0}] using Gmail account [{1}]" .format(recipient, gmail_user)) mail_server = smtplib.SMTP_SSL('smtp.gmail.com', 465) mail_server.login(gmail_user, gmail_password) failed_recipients = mail_server.sendmail( gmail_user, recipient, msg.as_string()) if failed_recipients: logging.warn("Email failed to following recipients [{0}]" .format(failed_recipients)) mail_server.close() return
def get_readings(serial, duration): """ Returns ..... Parameters ---------- serial: str (required) sensor unique serial number duration: str (required) a valid DURATION_ENUM Returns ------- dict: A sensor dictionary containing column names as keys, and corresponding values. """ if is_blank(serial): raise IllegalArgumentException("serial is required.") if is_blank(duration): raise IllegalArgumentException("duration is required.") logging.debug("Retrieving readings from sensor with serial [{0}] using " "duration [{1}] from database." .format(serial, duration)) arrow_utcnow = arrow.utcnow() logging.debug("current UTC now [{0}]".format(str(arrow_utcnow))) arrow_utcpast = None # I had to define the following variables to fix PyDev Error: # Undefined variable from import: name. PyDev last5Years = DURATION_ENUM.last5Years last1Year = DURATION_ENUM.last1Year last6Months = DURATION_ENUM.last6Months last90Days = DURATION_ENUM.last90Days last60Days = DURATION_ENUM.last60Days last30Days = DURATION_ENUM.last30Days last21Days = DURATION_ENUM.last21Days last7Days = DURATION_ENUM.last7Days last3Days = DURATION_ENUM.last3Days lastDay = DURATION_ENUM.lastDay last24Hours = DURATION_ENUM.last24Hours last12Hours = DURATION_ENUM.last12Hours last6Hours = DURATION_ENUM.last6Hours lastHour = DURATION_ENUM.lastHour if duration.lower().strip() == last5Years.name.lower(): arrow_utcpast = arrow_utcnow.replace(years=-5) elif duration.lower().strip() == last1Year.name.lower(): arrow_utcpast = arrow_utcnow.replace(years=-1) elif duration.lower().strip() == last6Months.name.lower(): arrow_utcpast = arrow_utcnow.replace(months=-6) elif duration.lower().strip() == last90Days.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-90) elif duration.lower().strip() == last60Days.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-60) elif duration.lower().strip() == last30Days.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-30) elif duration.lower().strip() == last21Days.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-21) elif duration.lower().strip() == last7Days.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-7) elif duration.lower().strip() == last3Days.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-3) elif duration.lower().strip() == lastDay.name.lower(): arrow_utcpast = arrow_utcnow.replace(days=-1) elif duration.lower().strip() == last24Hours.name.lower(): arrow_utcpast = arrow_utcnow.replace(hours=-24) elif duration.lower().strip() == last12Hours.name.lower(): arrow_utcpast = arrow_utcnow.replace(hours=-12) elif duration.lower().strip() == last6Hours.name.lower(): arrow_utcpast = arrow_utcnow.replace(hours=-6) elif duration.lower().strip() == lastHour.name.lower(): arrow_utcpast = arrow_utcnow.replace(hours=-1) else: raise IllegalArgumentException("duration [{0}] is not valid" .format(duration)) logging.debug("current UTC [{0}], past UTC [{1}], duration[{2}]" .format(str(arrow_utcnow), str(arrow_utcpast), duration)) current_datetime = str(arrow_utcnow) past_datetime = str(arrow_utcpast) data = SensorDAO.SENSOR_DB.get_readings(serial, current_datetime, past_datetime) return data