From f46cf0ed3aee2e6959450edaadcf03b7d1b140ba Mon Sep 17 00:00:00 2001 From: Trevor Mack Date: Thu, 14 Oct 2021 19:32:29 -0400 Subject: [PATCH 1/3] introduce a webhook framework for processing and sending session data to internet or local network sources --- Dockerfile | 2 +- app/main/model.py | 16 +++ app/main/routes_frontend.py | 183 +++++++++++++++++++++++------ app/main/routes_iSpindel_api.py | 19 ++- app/main/routes_pico_api.py | 21 +++- app/main/routes_tilt_api.py | 23 +++- app/main/routes_zseries_api.py | 16 ++- app/main/routes_zymatic_api.py | 20 +++- app/main/session_parser.py | 2 +- app/main/still_polling.py | 19 ++- app/main/units.py | 10 ++ app/main/webhook.py | 42 +++++++ app/static/js/index.js | 103 +++++++++++++++- app/static/js/server_management.js | 8 +- app/templates/index.html | 25 ++++ app/templates/webhooks.html | 55 +++++++++ 16 files changed, 508 insertions(+), 56 deletions(-) create mode 100644 app/main/webhook.py create mode 100644 app/templates/webhooks.html diff --git a/Dockerfile b/Dockerfile index b43f09fa..314491d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.9 +FROM python:3.7 ENV HOST=0.0.0.0 ENV PORT=80 diff --git a/app/main/model.py b/app/main/model.py index b112e335..427e916d 100644 --- a/app/main/model.py +++ b/app/main/model.py @@ -61,6 +61,7 @@ def __init__(self, machineType=None): self.remaining_time = None self.is_pico = True if machineType in [MachineType.PICOBREW, MachineType.PICOBREW_C] else False self.data = [] + self.webhooks = [] def cleanup(self): if self.file and self.filepath: @@ -77,6 +78,7 @@ def cleanup(self): self.recovery = '' self.remaining_time = None self.data = [] + self.webhooks = [] class PicoStillSession: @@ -93,6 +95,7 @@ def __init__(self, uid=None): self.session = '' # session guid self.polling_thread = None self.data = [] + self.webhooks = [] def cleanup(self): if self.file and self.filepath: @@ -107,6 +110,7 @@ def cleanup(self): self.polling_thread = None self.session = '' self.data = [] + self.webhooks = [] def start_still_polling(self): connect_failure = False @@ -150,6 +154,7 @@ def __init__(self): self.voltage = '-' self.start_time = None self.data = [] + self.webhooks = [] def cleanup(self): if self.file and self.filepath: @@ -161,6 +166,7 @@ def cleanup(self): self.voltage = '-' self.start_time = None self.data = [] + self.webhooks = [] class iSpindelSession: @@ -173,6 +179,7 @@ def __init__(self): self.voltage = '-' self.start_time = None self.data = [] + self.webhooks = [] def cleanup(self): if self.file and self.filepath: @@ -185,6 +192,7 @@ def cleanup(self): self.voltage = '-' self.start_time = None self.data = [] + self.webhooks = [] class TiltSession: @@ -198,6 +206,7 @@ def __init__(self): self.rssi = None self.start_time = None self.data = [] + self.webhooks = [] def cleanup(self): if self.file and self.filepath: @@ -210,8 +219,15 @@ def cleanup(self): self.rssi = None self.start_time = None self.data = [] + self.webhooks = [] +class Webhook: + def __init__(self, url=None, enabled=None, status="disabled"): + self.url = url + self.enabled = enabled + self.status = status if not enabled and status == "disabled" else "enabled" + class SupportObject: def __init__(self): self.name = None diff --git a/app/main/routes_frontend.py b/app/main/routes_frontend.py index aa0a0d06..9e39d493 100644 --- a/app/main/routes_frontend.py +++ b/app/main/routes_frontend.py @@ -1,7 +1,9 @@ +import csv +import io import json import os import uuid -from datetime import timedelta +from datetime import datetime, timedelta from flask import current_app, escape, make_response, request, send_file, render_template from pathlib import Path from ruamel.yaml import YAML @@ -20,7 +22,8 @@ dirty_sessions_since_clean, last_session_metadata, BrewSessionType, get_brew_graph_data, get_ferm_graph_data, get_still_graph_data, get_iSpindel_graph_data, get_tilt_graph_data, active_brew_sessions, active_ferm_sessions, active_still_sessions, active_iSpindel_sessions, active_tilt_sessions, - add_invalid_session, get_invalid_sessions, load_brew_sessions) + add_invalid_session, get_invalid_sessions, load_brew_session, load_brew_sessions) +from .webhook import Webhook file_glob_pattern = "[!._]*.json" @@ -211,17 +214,7 @@ def update_zseries_recipe(): @main.route('/device//sessions/', methods=['PUT']) def update_device_session(uid, session_type): update = request.get_json() - valid_session = True - if session_type == 'ferm': - session = active_ferm_sessions[uid] - elif session_type == 'iSpindel': - session = active_iSpindel_sessions[uid] - elif session_type == 'tilt': - session = active_tilt_sessions[uid] - elif session_type == 'still': - session = active_still_sessions[uid] - else: - valid_session = False + session, valid_session = active_session(uid, session_type) if valid_session: if update['active'] == False: @@ -260,6 +253,27 @@ def allowed_extension(filename): filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS +@main.route('/device//sessions//webhooks', methods=['POST']) +def update_session_webhooks(uid, session_type): + body = request.get_json() + + # current_app.logger.error(f'request_body : {webhooks}') + session, valid_session = active_session(uid, session_type) + + if valid_session: + # save/update webhook definition(s) + webhooks = [] + for webhook in body['webhooks']: + webhooks.append(Webhook(webhook['url'], webhook['enabled'])) + + session.webhooks = webhooks + current_app.logger.debug(f'configured {len(session.webhooks)} webhook to the active {session_type} session {uid}') + return 'configuration of webhooks successful', 200 + else: + current_app.logger.error(f'unable to locate valid active session : {session_type} and {uid}') + return f'Invalid session type or device id provided {session_type}, {uid}', 418 + + def recipe_dirpath(machine_type): dirpath = None if machine_type == "picobrew" or machine_type == "pico": @@ -323,8 +337,8 @@ def download_recipe(machine_type, id, name): return f'Download Recipe: Failed to find recipe id "{id}" with name "{name}"', 418 -@main.route('/sessions//', methods=['GET']) -def download_session(session_type, filename): +@main.route('/sessions//.', methods=['GET']) +def download_session(session_type, filename, extension): session_dirpath = "" if session_type == "brew": session_dirpath = brew_archive_sessions_path() @@ -341,11 +355,90 @@ def download_session(session_type, filename): filepath = session_dirpath.joinpath(filename) for f in files: - if f.name == filename: - response = make_response(send_file(filepath)) - # custom content-type will force a download vs rendering with window.location - response.headers['Content-Type'] = 'application/octet-stream' - return response + if f.stem == filename: + if extension == "json": + response = make_response(send_file(f'{filepath}.json')) + # custom content-type will force a download vs rendering with window.location + response.headers['Content-Type'] = 'application/octet-stream' + return response + elif extension == "csv": + session = load_brew_session(f) + + output = io.StringIO() + writer = csv.writer(output) + + data_map = dict() + if session['is_pico']: # pico + data_map = { + "#PicoSessionID": "SessionID", + "PicoSessionLogID": "SessionLogID", # not in JSON data + "LogDate": "timeStr", + "WorkTemp": "wort", + "ThermoBlockTemp": "therm", + "Event": "event", + "ErrorCode": "errorCode", + "ShuttleScaler": None, + } + elif len(session['data']) >= 2 and 'board' in session['data'][1]: # zymatic + data_map = { + "#SessionID": "SessionID", + "SessionLogID": "SessionLogID", # not in JSON data + "Date": "timeStr", + "Event": "event", + "Heat": "heat1", + "Wort": "wort", + "Board": "board", + "Heat2": "heat2", + } + elif len(session['data']) >= 1 and 'ambient' in session['data'][0]: # zseries + data_map = { + "#ZSessionID": "SessionID", + "ID": "SessionLogID", # not in JSON data + "LogDate": "timeStr", + "StepName": "step", + "TargetTemp": "target", + "WortTemp": "wort", + "ThermoBlockTemp": "therm", + "AmbientTemp": "ambient", + "DrainTemp": "drain", + "ValvePosition": "position", + "KegPumpOn": False, + "DrainPumpOn": False, + "ErrorCode": 0, + "PauseReason": 0, + "rssi": 0, + "netSend": 0, + "netWait": 0, + "netRecv": 0 + } + + writer.writerow(data_map.keys()) + + for index, log_data in enumerate(session['data']): + data = [] + for key, data_key in data_map.items(): + if data_key == "SessionID": # not in log data + data.append(session['session']) + elif data_key == "SessionLogID": # not in log data + data.append(index) + elif data_key == "timeStr": + log_time = datetime.strptime(log_data[data_key], '%Y-%m-%dT%H:%M:%S.%f') + data.append(log_time.strftime("%m/%d/%Y %H:%M:%S %p")) + elif type(data_key) is not str: + data.append(data_key) + elif type(data_key) is str: + data.append(log_data[data_key]) + + # add data to csv + writer.writerow(data) + + response = make_response(output.getvalue()) + response.headers["Content-Disposition"] = f'attachment; filename={filename}.{extension}' + response.headers["Content-type"] = "text/html" + return response + else: + return f'Download Session: Failed due to unsupported file extension "{extension}"', 418 + return f'Download Session: Failed to find session with filename "{filename}"', 418 @@ -561,7 +654,8 @@ def load_active_brew_sessions(): 'graph': get_brew_graph_data(uid, active_brew_sessions[uid].name, active_brew_sessions[uid].step, active_brew_sessions[uid].data, - active_brew_sessions[uid].is_pico)} + active_brew_sessions[uid].is_pico), + 'webhooks': active_brew_sessions[uid].webhooks} if len(session_data) > 0: if 'timeLeft' in session_data[-1]: @@ -599,7 +693,8 @@ def load_active_ferm_sessions(): 'active': active_ferm_sessions[uid].active, 'date': active_ferm_sessions[uid].start_time or None, 'graph': get_ferm_graph_data(uid, active_ferm_sessions[uid].voltage, - active_ferm_sessions[uid].data)}) + active_ferm_sessions[uid].data), + 'webhooks': active_ferm_sessions[uid].webhooks}) return ferm_sessions @@ -624,11 +719,13 @@ def load_active_still_sessions(): still_sessions = [] for uid in active_still_sessions: still_sessions.append({'alias': active_still_sessions[uid].alias, - 'uid': uid, - 'ip_address': active_still_sessions[uid].ip_address, - 'active': active_still_sessions[uid].active, - 'date': active_still_sessions[uid].created_at or None, - 'graph': get_still_graph_data(uid, active_still_sessions[uid].name, active_still_sessions[uid].data)}) + 'uid': uid, + 'ip_address': active_still_sessions[uid].ip_address, + 'active': active_still_sessions[uid].active, + 'date': active_still_sessions[uid].created_at or None, + 'graph': get_still_graph_data(uid, active_still_sessions[uid].name, + active_still_sessions[uid].data), + 'webhooks': active_still_sessions[uid].webhooks}) return still_sessions @@ -657,7 +754,8 @@ def load_active_iSpindel_sessions(): 'active': active_iSpindel_sessions[uid].active, 'date': active_iSpindel_sessions[uid].start_time or None, 'graph': get_iSpindel_graph_data(uid, active_iSpindel_sessions[uid].voltage, - active_iSpindel_sessions[uid].data)}) + active_iSpindel_sessions[uid].data), + 'webhooks': active_iSpindel_sessions[uid].webhooks}) return iSpindel_sessions @@ -681,12 +779,13 @@ def load_active_tilt_sessions(): tilt_sessions = [] for uid in active_tilt_sessions: tilt_sessions.append({'alias': active_tilt_sessions[uid].alias, - 'uid': uid, - 'color': active_tilt_sessions[uid].color, - 'active': active_tilt_sessions[uid].active, - 'date': active_tilt_sessions[uid].start_time or None, - 'graph': get_tilt_graph_data(uid, active_tilt_sessions[uid].rssi, - active_tilt_sessions[uid].data)}) + 'uid': uid, + 'color': active_tilt_sessions[uid].color, + 'active': active_tilt_sessions[uid].active, + 'date': active_tilt_sessions[uid].start_time or None, + 'graph': get_tilt_graph_data(uid, active_tilt_sessions[uid].rssi, + active_tilt_sessions[uid].data), + 'webhooks': active_tilt_sessions[uid].webhooks}) return tilt_sessions @@ -739,3 +838,19 @@ def increment_zseries_recipe_id(): recipe_id += 1 return recipe_id + + +def active_session(uid, session_type): + session = None + if session_type == 'brew': + session = active_brew_sessions[uid] + elif session_type == 'ferm': + session = active_ferm_sessions[uid] + elif session_type == 'iSpindel': + session = active_iSpindel_sessions[uid] + elif session_type == 'tilt': + session = active_tilt_sessions[uid] + elif session_type == 'still': + session = active_still_sessions[uid] + + return session, session != None diff --git a/app/main/routes_iSpindel_api.py b/app/main/routes_iSpindel_api.py index fc8294f6..769cc465 100644 --- a/app/main/routes_iSpindel_api.py +++ b/app/main/routes_iSpindel_api.py @@ -10,7 +10,8 @@ from .config import iSpindel_active_sessions_path from .model import iSpindelSession from .session_parser import active_iSpindel_sessions -from .units import convert_temp +from .units import convert_temp, epoch_time, excel_date +from .webhook import send_webhook arg_parser = FlaskParser() @@ -44,11 +45,11 @@ def process_iSpindel_data(data): if active_iSpindel_sessions[uid].uninit: create_new_session(uid) - time = ((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() * 1000) + time = datetime.utcnow() session_data = [] log_data = '' point = { - 'time': time, + 'time': epoch_time(time), 'temp': data['temperature'] if data['temp_units'] == 'F' else convert_temp(data['temperature'], 'F'), 'gravity': data['gravity'], } @@ -56,6 +57,18 @@ def process_iSpindel_data(data): session_data.append(point) log_data += '\n\t{},'.format(json.dumps(point)) + # send data to configured webhooks (logging error and tracking individual status) + for webhook in active_iSpindel_sessions[uid].webhooks: + # translate point into Tilt-like webhook data payload + webhook_data = { + 'Timepoint': excel_date(time), + 'Temp': point['temp'], + 'SG': point['gravity'], + 'color': data['name'] + } + # send and update status of webhook + send_webhook(webhook, webhook_data) + active_iSpindel_sessions[uid].data.extend(session_data) active_iSpindel_sessions[uid].voltage = str(data['battery']) + 'V' diff --git a/app/main/routes_pico_api.py b/app/main/routes_pico_api.py index 2959bbe1..f611bf84 100644 --- a/app/main/routes_pico_api.py +++ b/app/main/routes_pico_api.py @@ -12,6 +12,8 @@ from .model import PicoBrewSession, PICO_SESSION from .routes_frontend import get_pico_recipes from .session_parser import active_brew_sessions, dirty_sessions_since_clean +from .units import epoch_time +from .webhook import send_webhook arg_parser = FlaskParser() @@ -202,16 +204,33 @@ def process_log(args): uid = args['uid'] if uid not in active_brew_sessions or active_brew_sessions[uid].name == 'Waiting To Brew': create_new_session(uid, args['sesId'], args['sesType']) - session_data = {'time': ((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() * 1000), + + log_date = datetime.utcnow() + session_data = {'time': epoch_time(log_date), 'timeLeft': args['timeLeft'], 'step': args['step'], 'wort': args['wort'], 'therm': args['therm'], } + event = None if 'event' in args: event = args['event'] session_data.update({'event': event}) + + # send data to configured webhooks (logging error and tracking individual status) + for webhook in active_brew_sessions[uid].webhooks: + webhook_data = { + # 'ErrorCode': session_data['board'], + 'Event': event, + # 'ShuttleScaler': session_data['heat1'], + 'ThermoBlockTemp': session_data['therm'], + 'WortTemp': session_data['wort'], + } + + # send and update status of webhook + send_webhook(webhook, webhook_data) + active_brew_sessions[uid].step = args['step'] active_brew_sessions[uid].data.append(session_data) graph_update = json.dumps({'time': session_data['time'], diff --git a/app/main/routes_tilt_api.py b/app/main/routes_tilt_api.py index cd92d304..8dc5f5db 100644 --- a/app/main/routes_tilt_api.py +++ b/app/main/routes_tilt_api.py @@ -11,6 +11,8 @@ from .config import tilt_active_sessions_path from .model import TiltSession from .session_parser import active_tilt_sessions +from .units import epoch_time, excel_date +from .webhook import send_webhook _lock = threading.Lock() arg_parser = FlaskParser() @@ -49,15 +51,14 @@ def process_tilt_data(data): if uid not in active_tilt_sessions: active_tilt_sessions[uid] = TiltSession() - if data['color']: - active_tilt_sessions[uid].color = data['color'] + active_tilt_sessions[uid].color = data.get('color') if active_tilt_sessions[uid].active: # initialize session and session files if active_tilt_sessions[uid].uninit: create_new_session(uid) - time = (datetime.fromisoformat(data['timestamp']) - datetime(1970, 1, 1)).total_seconds() * 1000 + time = datetime.fromisoformat(data.get('timestamp')) session_data = [] log_data = '' @@ -82,9 +83,21 @@ def process_tilt_data(data): session_data.append(point) log_data += '\n\t{},'.format(json.dumps(point)) + # send data to configured webhooks (logging error and tracking individual status) + for webhook in active_tilt_sessions[uid].webhooks: + # translate point into Tilt webhook data payload + webhook_data = { + 'Timepoint': excel_date(time), + 'Temp': point['temp'], + 'SG': point['gravity'], + 'color': data.get('color') + } + # send and update status of + send_webhook(webhook, webhook_data) + active_tilt_sessions[uid].data.extend(session_data) - active_tilt_sessions[uid].rssi = str(data['rssi']) - graph_update = json.dumps({'rssi': data['rssi'], 'data': session_data}) + active_tilt_sessions[uid].rssi = str(data.get('rssi')) + graph_update = json.dumps({'rssi': data.get('rssi'), 'data': session_data}) socketio.emit('tilt_session_update|{}'.format(uid), graph_update) # end fermentation only when user specifies fermentation is complete diff --git a/app/main/routes_zseries_api.py b/app/main/routes_zseries_api.py index 0d961d27..c5736096 100644 --- a/app/main/routes_zseries_api.py +++ b/app/main/routes_zseries_api.py @@ -15,8 +15,8 @@ from .routes_frontend import get_zseries_recipes from .session_parser import (active_brew_sessions, dirty_sessions_since_clean, get_machine_by_session, increment_session_id, last_session_type, ZSessionType) -from .units import convert_temp - +from .units import convert_temp, epoch_time +from .webhook import send_webhook arg_parser = FlaskParser() seed(1) @@ -430,7 +430,7 @@ def update_session_log(token, body): active_session.step = body['StepName'] log_time = datetime.utcnow() session_data = { - 'time': ((log_time - datetime(1970, 1, 1)).total_seconds() * 1000), + 'time': epoch_time(log_time), 'timeStr': log_time.isoformat(), 'timeLeft': body['SecondsRemaining'], 'step': body['StepName'], @@ -444,6 +444,16 @@ def update_session_log(token, body): 'position': body['ValvePosition'] } + # send data to configured webhooks (logging error and tracking individual status) + for webhook in active_session.webhooks: + webhook_data = dict(body) # copy original payload + webhook_data['DrainPumpOn'] = body['DrainPumpOn'] == 1 + webhook_data['KegPumpOn'] = body['KegPumpOn'] == 1 + del webhook_data['ZSessionID'] + + # send and update status of webhook + send_webhook(webhook, webhook_data) + event = None if active_session in events and len(events[active_session]) > 0: if len(events[active_session]) > 1: diff --git a/app/main/routes_zymatic_api.py b/app/main/routes_zymatic_api.py index 7c6045c4..877dc4c6 100644 --- a/app/main/routes_zymatic_api.py +++ b/app/main/routes_zymatic_api.py @@ -11,6 +11,8 @@ from .model import MachineType, PicoBrewSession from .routes_frontend import get_zymatic_recipes from .session_parser import active_brew_sessions +from .units import epoch_time +from .webhook import send_webhook arg_parser = FlaskParser() @@ -194,8 +196,9 @@ def process_log_session(args): elif args['code'] == 2: session = args['session'] uid = get_machine_by_session(session) + log_time = datetime.utcnow() temps = [int(temp[2:]) for temp in args['data'].split('|')] - session_data = {'time': ((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() * 1000), + session_data = {'time': epoch_time(log_time), 'wort': temps[0], 'heat1': temps[1], 'board': temps[2], @@ -204,12 +207,27 @@ def process_log_session(args): 'recovery': args['step'], 'state': args['state'], } + event = None if session in events and len(events[session]) > 0: if len(events[session]) > 1: print('DEBUG: Zymatic events > 1 - size = {}'.format(len(events[session]))) event = events[session].pop(0) session_data.update({'event': event}) + + # send data to configured webhooks (logging error and tracking individual status) + for webhook in active_brew_sessions[uid].webhooks: + webhook_data = { + 'Board': session_data['board'], + 'Event': event, + 'Heat': session_data['heat1'], + 'Heat2': session_data['heat2'], + 'Wort': session_data['wort'], + } + + # send and update status of webhook + send_webhook(webhook, webhook_data) + active_brew_sessions[uid].data.append(session_data) active_brew_sessions[uid].recovery = args['step'] graph_update = json.dumps({'time': session_data['time'], diff --git a/app/main/session_parser.py b/app/main/session_parser.py index bfb02c22..362a7dc3 100644 --- a/app/main/session_parser.py +++ b/app/main/session_parser.py @@ -445,7 +445,7 @@ def restore_active_brew_sessions(): session.file.flush() session.filepath = file session.created_at = brew_session['date'] - current_app.logger.debug(f'current created_at : {session.created_at}') + # current_app.logger.debug(f'current created_at : {session.created_at}') session.name = brew_session['name'] session.type = brew_session['type'] session.session = brew_session['session'] # session guid diff --git a/app/main/still_polling.py b/app/main/still_polling.py index 74d49a31..0386ea63 100644 --- a/app/main/still_polling.py +++ b/app/main/still_polling.py @@ -6,6 +6,8 @@ from .config import still_active_sessions_path, server_config from .model import PicoStillSession from .session_parser import active_still_sessions +from .units import epoch_time +from .webhook import send_webhook from time import sleep from datetime import datetime @@ -42,10 +44,10 @@ def poll_still(still_ip, uid): datastring = datastring[1:-1] t1, t2, t3, t4, pres, ok, d1, d2, errmsg = datastring.split(',') - time = (datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() * 1000 + log_time = datetime.utcnow() session_data = [] log_data = '' - point = {'time': time, + point = {'time': epoch_time(log_time), 't1': t1, 't2': t2, 't3': t3, @@ -55,6 +57,19 @@ def poll_still(still_ip, uid): session_data.append(point) log_data += '\t{},\n'.format(json.dumps(point)) + # send data to configured webhooks (logging error and tracking individual status) + for webhook in active_still_sessions[uid].webhooks: + webhook_data = { + 'T1': point['t1'], + 'T2': point['t2'], + 'T3': point['t3'], + 'T4': point['t4'], + 'Pressure': point['pres'] + } + + # send and update status of webhook + send_webhook(webhook, webhook_data) + active_still_sessions[uid].data.extend(session_data) graph_update = json.dumps({'data': session_data}) socketio.emit('still_session_update|{}'.format(uid), graph_update) diff --git a/app/main/units.py b/app/main/units.py index 0b232fef..eb2d7a14 100644 --- a/app/main/units.py +++ b/app/main/units.py @@ -1,3 +1,5 @@ +import datetime + # convert temperature between F and C def convert_temp(temp: float, units: str): converted_temp = temp @@ -7,3 +9,11 @@ def convert_temp(temp: float, units: str): converted_temp = (temp - 32) * 5 / 9 # convert fahrenheit to celcius return round(converted_temp, 2) + +def epoch_time(date1): + return (date1 - datetime.datetime(1970, 1, 1)).total_seconds() * 1000 + +def excel_date(date1): + temp = datetime.datetime(1899, 12, 30) # Note, not 31st Dec but 30th! + delta = date1 - temp + return float(delta.days) + (float(delta.seconds) / 86400) \ No newline at end of file diff --git a/app/main/webhook.py b/app/main/webhook.py new file mode 100644 index 00000000..c93100c5 --- /dev/null +++ b/app/main/webhook.py @@ -0,0 +1,42 @@ +from flask import current_app +import requests +import shutil +import json + +from .config import (MachineType, brew_archive_sessions_path, ferm_archive_sessions_path, + still_archive_sessions_path, iSpindel_archive_sessions_path, tilt_archive_sessions_path) + + +def send_webhook(webhook, message): + if not webhook.enabled: + return + + try: + response = requests.post(webhook.url, + data=json.dumps( + message, sort_keys=True, default=str), + headers={'Content-Type': 'application/json'}, + timeout=1.0) + response.raise_for_status() + except requests.exceptions.HTTPError as err: + current_app.logger.error(f'error received while processing webhook {webhook.url} : {err}') + webhook.status = "error" + pass + except requests.exceptions.ConnectionError as err: + current_app.logger.error(f'webhook destination failed to establish a connection {webhook.url} : {err}') + webhook.status = "error" + pass + except requests.exceptions.Timeout as err: + current_app.logger.error(f'timeout occured while processing webhook {webhook.url} : {err}') + webhook.status = "error" + pass + except requests.exceptions.RequestException as err: + current_app.logger.error(f'unknown error occured while processing webhook {webhook.url} : {err}') + webhook.status = "error" + pass + except: + webhook.status = "error" + pass + else: + webhook.status = "success" + return response.status_code \ No newline at end of file diff --git a/app/static/js/index.js b/app/static/js/index.js index 906a7f44..2397f7c3 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -52,4 +52,105 @@ function stop_monitoring(session_type, uid) { //setTimeout(function () { window.location.href = "pico_recipes";}, 2000); }, }); -} \ No newline at end of file +} + +function add_integration(id) { + var parentId = `f_${id}`; + var new_chq_no = parseInt($(`#${parentId}> .total_integrations`).val()) + 1; + var new_input = ` +
+
+ + +
+
+
+ + +
+
+
+ + disabled +
+
+
+ +
+
+ `; + + // var new_input = ""; + + $(`#${parentId}> .new_integrations`).append(new_input); + $(`#${parentId}> .total_integrations`).val(new_chq_no); + + display_unsaved_state(id); +} + +function delete_integration(parentId, id) { + var last_chq_no = $(`#${parentId}> .total_integrations`).val(); + + $(`#${id}`).remove(); + $(`#${parentId}> .total_integrations`).val(last_chq_no - 1); + + display_unsaved_state(id); +} + +function display_unsaved_state(chart_id) { + $(`#bsave_${chart_id}`).show(); +} + +$(document).ready(function () { + $('.save_integrations').click(function (event) { + var chart_id = event.currentTarget.id.replace("bsave_", ""); + + var integrations = {} + integrations.chart_id = chart_id; + integrations.device_id = document.getElementById(`${chart_id}_device_uid`).value; + integrations.session_type = document.getElementById(`${chart_id}_session_type`).value; + integrations.total_integrations = document.getElementById(`${chart_id}_total_integrations`).value; + integrations.webhooks = [] + + var $webhook_rows = $("#f_" + chart_id).find('.integration') + for (var i=0; i<$webhook_rows.length; i++) { + const $row = $webhook_rows[i]; + const change_num = $row.id.substring($row.id.lastIndexOf("_")+1) // id="integration_${parentId}_${new_chq_no}" + + var webhook = {} + webhook.url = document.getElementById(`url_${chart_id}_${change_num}`).value; + webhook.enabled = document.getElementById(`enabled_${chart_id}_${change_num}`).checked; + integrations.webhooks.push(webhook); + } + + $.ajax({ + url: `/device/${integrations.device_id}/sessions/${integrations.session_type}/webhooks`, + type: 'POST', + data: JSON.stringify(integrations), + processData: false, + contentType: "application/json; charset=UTF-8", + success: function (data) { + showAlert("Success!", "success") + }, + error: function (request, status, error) { + showAlert("Error: " + request.responseText, "danger") + window.scrollTo({top: 0, behavior: 'smooth'}); + }, + }); + }); + + // webhook url or enabled changes toggle unsaved state + $('.new_integrations').find('input[type=text]').bind('input propertychange', function(event) { + var chart_id = $(event.currentTarget).data('chartId') + display_unsaved_state(chart_id) + }); + $('.new_integrations').find('input[type=checkbox').change(function() { + var chart_id = $(this).data('chartId') + display_unsaved_state(chart_id) + }) +}); \ No newline at end of file diff --git a/app/static/js/server_management.js b/app/static/js/server_management.js index cf097469..3cce2eb3 100644 --- a/app/static/js/server_management.js +++ b/app/static/js/server_management.js @@ -1,11 +1,11 @@ -function delete_server_file(filename, type, redirectHref){ +function delete_server_file(filename, mtype, redirectHref){ if (confirm("Are you sure?")){ $.ajax({ url: 'delete_file', type: 'POST', data: JSON.stringify({ filename: filename, - type: type + type: mtype }), dataType: "json", processData: false, @@ -22,8 +22,8 @@ function delete_server_file(filename, type, redirectHref){ } }; -function download_server_session(filename, type){ - window.location = '/sessions/' + type + '/' + escape(filename); +function download_server_session(filename, mtype, filetype="json"){ + window.location = `/sessions/${mtype}/${escape(filename)}.${filetype}`; }; function upload_recipe_file(machineType, file, redirectUrl) { diff --git a/app/templates/index.html b/app/templates/index.html index 8398c89c..30e61117 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -87,6 +87,11 @@
{% endif %}
 
+ {% with session_type='brew' %} + {% set session=brew_session %} + {% include "webhooks.html" %} + {% endwith %} +
 
@@ -134,6 +139,11 @@
 
+ {% with session_type='ferm' %} + {% set session=ferm_session %} + {% include "webhooks.html" %} + {% endwith %} +
 
@@ -185,6 +195,11 @@
 
+ {% with session_type='still' %} + {% set session=still_session %} + {% include "webhooks.html" %} + {% endwith %} +
 
@@ -254,6 +269,11 @@
 
+ {% with session_type='iSpindel' %} + {% set session=iSpindel_session %} + {% include "webhooks.html" %} + {% endwith %} +
 
@@ -310,6 +330,11 @@
 
+ {% with session_type='tilt' %} + {% set session=tilt_session %} + {% include "webhooks.html" %} + {% endwith %} +
 
diff --git a/app/templates/webhooks.html b/app/templates/webhooks.html new file mode 100644 index 00000000..3046e0d9 --- /dev/null +++ b/app/templates/webhooks.html @@ -0,0 +1,55 @@ +
+
+
Webhooks / Integrations show/hide
+
+
+
+
+
+ + + + +
+ {% for webhook in session.webhooks %} +
+
+ + +
+
+
+ {% set webhook_enabled = 'checked' if webhook.enabled else '' %} + + +
+
+
+ + {% set badge_status_map = {"disabled": "secondary", "enabled": "info", "success": "success", "error": "danger"} %} + {% set badge_type = badge_status_map[webhook.status] %} + {{webhook.status}} +
+
+
+ +
+
+ {% endfor %} +
+
+ + +
+
+
+
\ No newline at end of file From e99a114dc2c033c4d07808ea67b6be1ecb3e4e60 Mon Sep 17 00:00:00 2001 From: Trevor Mack Date: Fri, 12 Nov 2021 13:36:59 -0500 Subject: [PATCH 2/3] stylistic changes --- app/main/model.py | 1 + app/main/routes_frontend.py | 10 +++++----- app/main/routes_iSpindel_api.py | 2 +- app/main/routes_pico_api.py | 2 +- app/main/routes_zymatic_api.py | 6 +++--- app/main/still_polling.py | 2 +- app/main/units.py | 4 ++++ 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/main/model.py b/app/main/model.py index 427e916d..dacb11fe 100644 --- a/app/main/model.py +++ b/app/main/model.py @@ -228,6 +228,7 @@ def __init__(self, url=None, enabled=None, status="disabled"): self.enabled = enabled self.status = status if not enabled and status == "disabled" else "enabled" + class SupportObject: def __init__(self): self.name = None diff --git a/app/main/routes_frontend.py b/app/main/routes_frontend.py index 9e39d493..541b0a8c 100644 --- a/app/main/routes_frontend.py +++ b/app/main/routes_frontend.py @@ -256,7 +256,7 @@ def allowed_extension(filename): @main.route('/device//sessions//webhooks', methods=['POST']) def update_session_webhooks(uid, session_type): body = request.get_json() - + # current_app.logger.error(f'request_body : {webhooks}') session, valid_session = active_session(uid, session_type) @@ -413,7 +413,7 @@ def download_session(session_type, filename, extension): } writer.writerow(data_map.keys()) - + for index, log_data in enumerate(session['data']): data = [] for key, data_key in data_map.items(): @@ -428,10 +428,10 @@ def download_session(session_type, filename, extension): data.append(data_key) elif type(data_key) is str: data.append(log_data[data_key]) - + # add data to csv writer.writerow(data) - + response = make_response(output.getvalue()) response.headers["Content-Disposition"] = f'attachment; filename={filename}.{extension}' response.headers["Content-type"] = "text/html" @@ -852,5 +852,5 @@ def active_session(uid, session_type): session = active_tilt_sessions[uid] elif session_type == 'still': session = active_still_sessions[uid] - + return session, session != None diff --git a/app/main/routes_iSpindel_api.py b/app/main/routes_iSpindel_api.py index 769cc465..48882d6e 100644 --- a/app/main/routes_iSpindel_api.py +++ b/app/main/routes_iSpindel_api.py @@ -68,7 +68,7 @@ def process_iSpindel_data(data): } # send and update status of webhook send_webhook(webhook, webhook_data) - + active_iSpindel_sessions[uid].data.extend(session_data) active_iSpindel_sessions[uid].voltage = str(data['battery']) + 'V' diff --git a/app/main/routes_pico_api.py b/app/main/routes_pico_api.py index f611bf84..34098fc7 100644 --- a/app/main/routes_pico_api.py +++ b/app/main/routes_pico_api.py @@ -227,7 +227,7 @@ def process_log(args): 'ThermoBlockTemp': session_data['therm'], 'WortTemp': session_data['wort'], } - + # send and update status of webhook send_webhook(webhook, webhook_data) diff --git a/app/main/routes_zymatic_api.py b/app/main/routes_zymatic_api.py index 877dc4c6..329523f7 100644 --- a/app/main/routes_zymatic_api.py +++ b/app/main/routes_zymatic_api.py @@ -214,7 +214,7 @@ def process_log_session(args): print('DEBUG: Zymatic events > 1 - size = {}'.format(len(events[session]))) event = events[session].pop(0) session_data.update({'event': event}) - + # send data to configured webhooks (logging error and tracking individual status) for webhook in active_brew_sessions[uid].webhooks: webhook_data = { @@ -224,10 +224,10 @@ def process_log_session(args): 'Heat2': session_data['heat2'], 'Wort': session_data['wort'], } - + # send and update status of webhook send_webhook(webhook, webhook_data) - + active_brew_sessions[uid].data.append(session_data) active_brew_sessions[uid].recovery = args['step'] graph_update = json.dumps({'time': session_data['time'], diff --git a/app/main/still_polling.py b/app/main/still_polling.py index 0386ea63..9b19189c 100644 --- a/app/main/still_polling.py +++ b/app/main/still_polling.py @@ -66,7 +66,7 @@ def poll_still(still_ip, uid): 'T4': point['t4'], 'Pressure': point['pres'] } - + # send and update status of webhook send_webhook(webhook, webhook_data) diff --git a/app/main/units.py b/app/main/units.py index eb2d7a14..392d95cb 100644 --- a/app/main/units.py +++ b/app/main/units.py @@ -1,6 +1,8 @@ import datetime # convert temperature between F and C + + def convert_temp(temp: float, units: str): converted_temp = temp if units.upper() == 'F': @@ -10,9 +12,11 @@ def convert_temp(temp: float, units: str): return round(converted_temp, 2) + def epoch_time(date1): return (date1 - datetime.datetime(1970, 1, 1)).total_seconds() * 1000 + def excel_date(date1): temp = datetime.datetime(1899, 12, 30) # Note, not 31st Dec but 30th! delta = date1 - temp From d5db5fba81f78a93793e92d03548b792311a9a6f Mon Sep 17 00:00:00 2001 From: Trevor Mack Date: Fri, 12 Nov 2021 13:41:07 -0500 Subject: [PATCH 3/3] fix important mistake from rebase --- app/main/routes_frontend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/main/routes_frontend.py b/app/main/routes_frontend.py index 541b0a8c..85655778 100644 --- a/app/main/routes_frontend.py +++ b/app/main/routes_frontend.py @@ -15,6 +15,7 @@ zymatic_recipe_path, zseries_recipe_path, pico_recipe_path, brew_archive_sessions_path, ferm_archive_sessions_path, still_archive_sessions_path, iSpindel_archive_sessions_path, tilt_archive_sessions_path) from .frontend_common import render_template_with_defaults +from .model import Webhook from .recipe_import import import_recipes from .recipe_parser import PicoBrewRecipe, ZymaticRecipe, ZSeriesRecipe from .session_parser import (_paginate_sessions, list_session_files, @@ -23,8 +24,6 @@ get_brew_graph_data, get_ferm_graph_data, get_still_graph_data, get_iSpindel_graph_data, get_tilt_graph_data, active_brew_sessions, active_ferm_sessions, active_still_sessions, active_iSpindel_sessions, active_tilt_sessions, add_invalid_session, get_invalid_sessions, load_brew_session, load_brew_sessions) -from .webhook import Webhook - file_glob_pattern = "[!._]*.json" yaml = YAML()