diff --git a/.gitignore b/.gitignore index eabe32d..5764bf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ __pycache__ python3 .env -.vscode -.settings \ No newline at end of file +.vscode/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ca43dbf..cf38bc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,11 +2,14 @@ FROM python:3.7.7-slim ENV PYTHONUNBUFFERED=1 -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt - COPY src/* /opt/microservices/ COPY requirements.txt /opt/microservices/ +RUN pip install --upgrade pip \ + && pip install --upgrade pipenv\ + && apt-get update \ + && apt install -y build-essential \ + && apt install -y libmariadb3 libmariadb-dev \ + && pip install --upgrade -r /opt/microservices/requirements.txt EXPOSE 8080 WORKDIR /opt/microservices/ diff --git a/chart/rulesdecision/templates/istio-gateway.yaml b/chart/rulesdecision/templates/istio-gateway.yaml index 45c1091..ff05e35 100644 --- a/chart/rulesdecision/templates/istio-gateway.yaml +++ b/chart/rulesdecision/templates/istio-gateway.yaml @@ -2,7 +2,7 @@ apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: - name: prometeo-rulesdecision + name: prometeo-gateway spec: selector: istio: ingressgateway # use istio default controller @@ -18,7 +18,7 @@ spec: apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: - name: prometeo + name: prometeo-rulesdecision spec: hosts: - "*" @@ -27,7 +27,7 @@ spec: http: - match: - uri: - exact: /decision + exact: /rulesdecision rewrite: uri: / route: @@ -38,6 +38,8 @@ spec: - match: - uri: prefix: /coreDecision + - uri: + prefix: /get_status - uri: prefix: /swaggerui - uri: diff --git a/chart/rulesdecision/templates/virutalservices.yaml b/chart/rulesdecision/templates/virutalservices.yaml deleted file mode 100644 index cb02af5..0000000 --- a/chart/rulesdecision/templates/virutalservices.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{ if .Values.istio.virtualservices.enabled }} -apiVersion: networking.istio.io/v1alpha3 -kind: VirtualService -metadata: - name: {{ .Chart.Name }} -spec: - hosts: - - {{ .Chart.Name }} - http: - - route: - - destination: - host: {{ .Chart.Name }} - subset: {{ .Values.rulesdecision.version }} -{{ end }} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cc6131c..7a65ca6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ pandas==1.1.1 sqlalchemy==1.3.19 pymysql==0.9.2 python-dotenv -APScheduler==3.6.3 \ No newline at end of file +APScheduler==3.6.3 +mariadb \ No newline at end of file diff --git a/src/coreDecision.py b/src/coreDecision.py deleted file mode 100644 index d95f13b..0000000 --- a/src/coreDecision.py +++ /dev/null @@ -1,38 +0,0 @@ -import json - -class coreDecision(object): - # the init method where we get the limit parameters from the parameters.json file - def __init__(self): - with open('parameters.json', 'r') as file: - self.parametros = json.load(file) - file.close() - - # Here are the rules based on the parameters - # Input: temperature, humidity and CO ppm - # Output: the status of the firefighter - - def get_status(self, temperature, humidity, co_ppm): - if co_ppm <= int(self.parametros["limits_co"]["verde"]): - status_co = 1 - elif co_ppm >= int(self.parametros["limits_co"]["rojo"]): - status_co = 3 - else: - status_co = 2 - - if temperature <= int(self.parametros["limits_temperature"]["verde"]): - status_temp = 1 - elif temperature >= int(self.parametros["limits_temperature"]["rojo"]): - status_temp = 3 - else: - status_temp = 2 - - # 1 - green, 2 - yellow, 3 - red - return max(status_co, status_temp) - - -# This code is to test the class -#prueba = coreDecision() -#print(prueba.parametros) -#print(prueba.parametros["limits_co"]) -#print(prueba.parametros["limits_co"]["rojo"]) -#print(prueba.get_status(41, 10, 20)) diff --git a/src/core_decision_flask_app.py b/src/core_decision_flask_app.py index a4ebd41..06a061c 100644 --- a/src/core_decision_flask_app.py +++ b/src/core_decision_flask_app.py @@ -1,5 +1,5 @@ import os -from flask import Flask, Response, jsonify +from flask import Flask, Response, jsonify, abort from flask_restplus import Api, Resource, fields, reqparse from flask_cors import CORS, cross_origin import pandas as pd @@ -9,6 +9,9 @@ import atexit from apscheduler.schedulers.background import BackgroundScheduler import logging +import sqlalchemy +import sys +from flask import request logger = logging.getLogger('core_decision_flask_app') logger.debug('creating an instance of devices') @@ -22,56 +25,94 @@ print('starting application') -api_prometeo_analytics = Api(app, version='1.0', title="Calculates Time-Weighted Average exposures and exposure-limit status 'gauges' for all firefighters for the last minute.", validate=False) -ns = api_prometeo_analytics.namespace('GasExposureAnalytics', 'Calculates core Prometeo analytics') - -# The API does not require any input data. Once called, it will retrieve the latest data from the database. -model_input = api_prometeo_analytics.model('Enter the data:', {'todo': fields.String(description='todo')}) - # On Bluemix, get the port number from the environment variable PORT # When running this app on the local machine, default to 8080 port = int(os.getenv('PORT', 8080)) +# DB Connections and identifier constants +SQLALCHEMY_DATABASE_URI = ("mysql+pymysql://"+os.getenv('MARIADB_USERNAME') + +":"+os.getenv("MARIADB_PASSWORD") + +"@"+os.getenv("MARIADB_HOST") + +":"+str(os.getenv("MARIADB_PORT")) + +"/prometeo") +DB_ENGINE = sqlalchemy.MetaData(SQLALCHEMY_DATABASE_URI).bind +ANALYTICS_TABLE = 'firefighter_status_analytics' +FIREFIGHTER_ID_COL = 'firefighter_id' +TIMESTAMP_COL = 'timestamp_mins' +STATUS_LED_COL = 'analytics_status_LED' + # We initialize the prometeo Analytics engine. perMinuteAnalytics = GasExposureAnalytics() + + +# Calculates Time-Weighted Average exposures and exposure-limit status 'gauges' for all firefighters for the last minute. def callGasExposureAnalytics(): - print(time.strftime("%A, %d. %B %Y %I:%M:%S %p")) + # print(time.strftime("%A, %d. %B %Y %I:%M:%S %p")) app.logger.info('info - running analytics') app.logger.debug('debug - running analytics') - # call the method on the class - perMinuteAnalytics.run_analytics() + # Run all of the core analytics for Prometeo for a given minute. + status_updates_df = perMinuteAnalytics.run_analytics() + + # # TODO: Pass all status details and gauges on to the dashboard via an update API + # status_updates_json = None # Information available for the current minute (may be None) + # if status_updates_df is not None: + # status_updates_json = (status_updates_df.reset_index(TIMESTAMP_COL) # index json by firefighter only + # .to_json(orient='index', date_format='iso')) # send json to dashboard + # + # resp = requests.post(API_URL, json=status_updates_json) + # if resp.status_code != EXPECTED_RESPONSE_CODE: + # app.logger.debug(f'ERROR: dashboard update API error code [{resp.status_code}]') + # app.logger.debug(f'\t with JSON: {status_updates_json}') + + +# Start up a scheduled job to run once per minute +ANALYTICS_FREQUENCY_SECONDS = 60 scheduler = BackgroundScheduler() -scheduler.add_job(func=callGasExposureAnalytics, trigger="interval", seconds=3) +scheduler.add_job(func=callGasExposureAnalytics, trigger="interval", seconds=ANALYTICS_FREQUENCY_SECONDS) scheduler.start() - # Shut down the scheduler when exiting the app atexit.register(lambda: scheduler.shutdown()) -# The ENDPOINT -@ns.route('/prometeo_analytics') -class prometeo_analytics(Resource): - @api_prometeo_analytics.response(200, "Success", model_input) - @api_prometeo_analytics.expect(model_input) - def post(self): - # We prepare the arguments - parser = reqparse.RequestParser() - parser.add_argument('firefighter_ids', type=list) - args = parser.parse_args() +# The ENDPOINTS +@app.route('/get_status', methods=['GET']) +def getStatus(): + + try: + firefighter_id = request.args.get(FIREFIGHTER_ID_COL) + timestamp_mins = request.args.get(TIMESTAMP_COL) + + # Return 404 (Not Found) if the record IDs are invalid + if (firefighter_id is None) or (timestamp_mins is None): + app.logger.error('Missing parameters : '+FIREFIGHTER_ID_COL+' : '+str(firefighter_id) + +', '+TIMESTAMP_COL+' : '+str(timestamp_mins)) + abort(404) + + # Read the requested Firefighter status + sql = ('SELECT '+FIREFIGHTER_ID_COL+', '+TIMESTAMP_COL+', '+STATUS_LED_COL+' FROM '+ANALYTICS_TABLE+ + ' WHERE '+FIREFIGHTER_ID_COL+' = '+firefighter_id+' AND '+TIMESTAMP_COL+' = "'+timestamp_mins+'"') + firefighter_status_df = pd.read_sql_query(sql, DB_ENGINE) + + # Return 404 (Not Found) if no record is found + if (firefighter_status_df is None) or (firefighter_status_df.empty): + app.logger.error('No status found for : ' + FIREFIGHTER_ID_COL + ' : ' + str(firefighter_id) + + ', ' + TIMESTAMP_COL + ' : ' + str(timestamp_mins)) + abort(404) + else: + firefighter_status_json = (firefighter_status_df + .rename(columns={STATUS_LED_COL: "status"}) # name as expected by client + .iloc[0,:] # convert dataframe to series (should never be more than 1 record) + .to_json(date_format='iso')) + return firefighter_status_json - # Run all of the core analytics for Prometeo for a given minute. - limits_and_gauges_for_all_firefighters_df = perMinuteAnalytics.run_analytics() + except Exception as e: + # Return 500 (Internal Server Error) if there's any unexpected errors. + app.logger.error(f'Internal Server Error: {e}') + abort(500) - # Return the limits and status gauges as json - if limits_and_gauges_for_all_firefighters_df is None : - return None - - return limits_and_gauges_for_all_firefighters_df.to_json(orient='index') - - if __name__ == '__main__': - app.run(host='0.0.0.0', port=port, debug=False) # deploy with debug=False + app.run(host='0.0.0.0', port=8080, debug=False) # deploy with debug=False diff --git a/src/parameters.json b/src/parameters.json deleted file mode 100644 index d87a20f..0000000 --- a/src/parameters.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "limits_co":{ - "verde":20, - "rojo":26 - }, - - "limits_temperature":{ - "verde":35, - "rojo":41 - } - -} diff --git a/src/prometeo_config.json b/src/prometeo_config.json index 967fae4..e770dc6 100644 --- a/src/prometeo_config.json +++ b/src/prometeo_config.json @@ -1,11 +1,11 @@ { "windows_and_limits": [ - { "label": "10min", "mins": 10, "gas_limits": { "carbon_monoxide": 420, "nitrogen_dioxide": 5.0, "formaldehyde": 14, "acrolein": 0.44 }}, - { "label": "30min", "mins": 30, "gas_limits": { "carbon_monoxide": 150, "nitrogen_dioxide": 1.0, "formaldehyde": 14, "acrolein": 0.18 }}, - { "label": "60min", "mins": 60, "gas_limits": { "carbon_monoxide": 83, "nitrogen_dioxide": 1.0, "formaldehyde": 14, "acrolein": 0.1 }}, - { "label": "4hr", "mins": 240, "gas_limits": { "carbon_monoxide": 33, "nitrogen_dioxide": 0.5, "formaldehyde": 14, "acrolein": 0.1 }}, - { "label": "8hr", "mins": 480, "gas_limits": { "carbon_monoxide": 27, "nitrogen_dioxide": 0.5, "formaldehyde": 14, "acrolein": 0.1 }} + { "label": "10min", "mins": 10, "gas_limits": { "carbon_monoxide": 420, "nitrogen_dioxide": 8, "formaldehyde": 14, "acrolein": 0.44 }}, + { "label": "30min", "mins": 30, "gas_limits": { "carbon_monoxide": 150, "nitrogen_dioxide": 6, "formaldehyde": 14, "acrolein": 0.18 }}, + { "label": "60min", "mins": 60, "gas_limits": { "carbon_monoxide": 83, "nitrogen_dioxide": 4, "formaldehyde": 14, "acrolein": 0.1 }}, + { "label": "4hr", "mins": 240, "gas_limits": { "carbon_monoxide": 33, "nitrogen_dioxide": 2, "formaldehyde": 14, "acrolein": 0.1 }}, + { "label": "8hr", "mins": 480, "gas_limits": { "carbon_monoxide": 27, "nitrogen_dioxide": 1, "formaldehyde": 14, "acrolein": 0.1 }} ], "supported_gases" :["carbon_monoxide", "nitrogen_dioxide"], "yellow_warning_percent" : 80,