From 4551480e41e81e0e6f59ad6e72b83997ad51af57 Mon Sep 17 00:00:00 2001 From: Radek Date: Thu, 22 May 2025 11:20:46 +0100 Subject: [PATCH] add rounding to nearest 5m n dash --- dash_power_room_n_customer.py | 163 ++++++++++++++++++++++++++++++++++ get_power_room_n_customer.py | 30 +++++-- 2 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 dash_power_room_n_customer.py diff --git a/dash_power_room_n_customer.py b/dash_power_room_n_customer.py new file mode 100644 index 0000000..63e04b0 --- /dev/null +++ b/dash_power_room_n_customer.py @@ -0,0 +1,163 @@ +import dash +from dash import dcc, html, Input, Output +import plotly.express as px +import pandas as pd +import sqlite3 +from datetime import datetime, timedelta + +# Initialize the Dash app +app = dash.Dash(__name__) + +# Connect to the SQLite database +def get_db_connection(): + conn = sqlite3.connect('power_data.db') + return conn + +# Function to fetch data from the database +def fetch_data(time_range): + conn = get_db_connection() + query = f""" + SELECT * FROM building_totals + WHERE timestamp >= datetime('now', '-{time_range} hours') + """ + df = pd.read_sql_query(query, conn) + conn.close() + return df + +# Function to calculate kWh and round timestamps for building totals +def calculate_building_kwh(df): + df['timestamp'] = pd.to_datetime(df['timestamp']) + df['timestamp'] = df['timestamp'].dt.round('5min') # Round to 5-minute intervals + df = df.set_index('timestamp') + df['kWh'] = df['total_power'] * (5 / 60) # Convert power to kWh for 5-minute intervals + return df + +# Function to calculate kWh and round timestamps for room and customer breakdowns +def calculate_breakdown_kwh(df): + df['timestamp'] = pd.to_datetime(df['timestamp']) + df['timestamp'] = df['timestamp'].dt.round('5min') # Round to 5-minute intervals + df = df.set_index('timestamp') + df['kWh'] = df['power'] * (5 / 60) # Convert power to kWh for 5-minute intervals + return df + +# Define the layout of the dashboard +app.layout = html.Div([ + html.H1("Power Usage Overview", style={'textAlign': 'center'}), + + dcc.Dropdown( + id='time-range', + options=[ + {'label': '6 Hours', 'value': '6'}, + {'label': '12 Hours', 'value': '12'}, + {'label': '24 Hours', 'value': '24'}, + {'label': '1 Week', 'value': '168'}, + {'label': '2 Weeks', 'value': '336'}, + {'label': '1 Month', 'value': '720'}, + {'label': '2 Months', 'value': '1440'}, + {'label': '1 Year', 'value': '8760'}, + ], + value='6', + style={'width': '200px', 'margin': '0 auto'} + ), + + dcc.Graph(id='building-graph', style={'width': '100%', 'height': '400px'}), + + dcc.Tabs(id='tabs', value='room-breakdown', children=[ + dcc.Tab(label='Room Breakdown', value='room-breakdown'), + dcc.Tab(label='Customer Breakdown', value='customer-breakdown'), + ]), + + html.Div([ + dcc.Dropdown(id='drill-down', multi=False, style={'width': '200px', 'margin': '0 auto'}), + dcc.Graph(id='breakdown-graph', style={'width': '100%', 'height': '400px'}), + ]) +]) + +# Callback to update the building total graph +@app.callback( + Output('building-graph', 'figure'), + [Input('time-range', 'value')] +) +def update_building_graph(time_range): + df = fetch_data(time_range) + df = calculate_building_kwh(df) + fig = px.line(df, x=df.index, y=['total_current', 'total_power', 'kWh'], + labels={'value': 'Value', 'variable': 'Metric'}, + title='Building Total Metrics') + fig.update_layout(legend_title_text='Metrics') + + # Update legend to show recent values + for trace in fig.data: + if trace.name == 'kWh': + trace.name = f"{trace.name}: {df['kWh'].sum():.2f}" + else: + trace.name = f"{trace.name}: {df[trace.name].iloc[-1]:.2f}" + + return fig + +# Callback to update the drill-down dropdown +@app.callback( + Output('drill-down', 'options'), + [Input('tabs', 'value')] +) +def update_drill_down_options(tab): + conn = get_db_connection() + if tab == 'room-breakdown': + query = "SELECT DISTINCT room_number FROM room_breakdown" + else: + query = "SELECT DISTINCT customer_name FROM customer_breakdown" + df = pd.read_sql_query(query, conn) + conn.close() + return [{'label': i, 'value': i} for i in df.iloc[:, 0]] + +# Callback to set the default value for the drill-down dropdown +@app.callback( + Output('drill-down', 'value'), + [Input('drill-down', 'options')] +) +def set_drill_down_value(options): + if options: + return options[0]['value'] + return None + +# Callback to update the breakdown graph +@app.callback( + Output('breakdown-graph', 'figure'), + [Input('tabs', 'value'), + Input('drill-down', 'value'), + Input('time-range', 'value')] +) +def update_breakdown_graph(tab, drill_down, time_range): + conn = get_db_connection() + if tab == 'room-breakdown': + query = f""" + SELECT * FROM room_breakdown + WHERE room_number = '{drill_down}' + AND timestamp >= datetime('now', '-{time_range} hours') + """ + else: + query = f""" + SELECT * FROM customer_breakdown + WHERE customer_name = '{drill_down}' + AND timestamp >= datetime('now', '-{time_range} hours') + """ + df = pd.read_sql_query(query, conn) + conn.close() + df = calculate_breakdown_kwh(df) + fig = px.line(df, x=df.index, y=['current', 'power', 'kWh'], + labels={'value': 'Value', 'variable': 'Metric'}, + title=f'{tab.replace("-", " ").title()} Metrics') + fig.update_layout(legend_title_text='Metrics') + + # Update legend to show recent values + for trace in fig.data: + if trace.name == 'kWh': + trace.name = f"{trace.name}: {df['kWh'].sum():.2f}" + else: + trace.name = f"{trace.name}: {df[trace.name].iloc[-1]:.2f}" + + return fig + +# Run the app +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8050, debug=True) diff --git a/get_power_room_n_customer.py b/get_power_room_n_customer.py index 841efdd..ddf2f4f 100644 --- a/get_power_room_n_customer.py +++ b/get_power_room_n_customer.py @@ -3,7 +3,7 @@ import requests from collections import defaultdict import argparse import sqlite3 -from datetime import datetime +from datetime import datetime, timedelta import time # Configuration @@ -55,14 +55,31 @@ def create_tables(conn): except sqlite3.Error as e: print(e) +def round_to_nearest_5_minutes(dt): + """Round a datetime object to the nearest 5-minute interval.""" + # Calculate the number of seconds since the last 5-minute interval + seconds_since_last_interval = dt.minute % 5 * 60 + dt.second + dt.microsecond / 1e6 + + # Round to the nearest 5-minute interval + if seconds_since_last_interval < 150: # 150 seconds = 2.5 minutes + rounded_dt = dt - timedelta(seconds=seconds_since_last_interval) + else: + rounded_dt = dt + timedelta(seconds=300 - seconds_since_last_interval) # 300 seconds = 5 minutes + + # Set seconds and microseconds to zero + rounded_dt = rounded_dt.replace(second=0, microsecond=0) + + return rounded_dt + def insert_building_total(conn, total_current, total_power): """Insert building total data into the database.""" try: cursor = conn.cursor() + rounded_timestamp = round_to_nearest_5_minutes(datetime.now()) cursor.execute(''' INSERT INTO building_totals (total_current, total_power, timestamp) VALUES (?, ?, ?) - ''', (round(total_current, 3), round(total_power, 3), datetime.now().isoformat())) + ''', (round(total_current, 3), round(total_power, 3), rounded_timestamp.isoformat())) conn.commit() except sqlite3.Error as e: print(e) @@ -71,10 +88,11 @@ def insert_room_breakdown(conn, room_number, current, power): """Insert room breakdown data into the database.""" try: cursor = conn.cursor() + rounded_timestamp = round_to_nearest_5_minutes(datetime.now()) cursor.execute(''' INSERT INTO room_breakdown (room_number, current, power, timestamp) VALUES (?, ?, ?, ?) - ''', (room_number, round(current, 3), round(power, 3), datetime.now().isoformat())) + ''', (room_number, round(current, 3), round(power, 3), rounded_timestamp.isoformat())) conn.commit() except sqlite3.Error as e: print(e) @@ -83,14 +101,16 @@ def insert_customer_breakdown(conn, customer_name, current, power): """Insert customer breakdown data into the database.""" try: cursor = conn.cursor() + rounded_timestamp = round_to_nearest_5_minutes(datetime.now()) cursor.execute(''' INSERT INTO customer_breakdown (customer_name, current, power, timestamp) VALUES (?, ?, ?, ?) - ''', (customer_name, round(current, 3), round(power, 3), datetime.now().isoformat())) + ''', (customer_name, round(current, 3), round(power, 3), rounded_timestamp.isoformat())) conn.commit() except sqlite3.Error as e: print(e) + def get_device_ids(debug=False): """Fetch all device IDs from the LibreNMS API.""" response = requests.get(f'http://{LIBRENMS_IP}/api/v0/devices', headers=HEADERS, verify=False) @@ -286,5 +306,5 @@ if __name__ == '__main__': main(debug=args.debug, update_db=not args.no_db_update, concurrency_level=args.concurrency_level) runs += 1 if args.runs is None: - time.sleep(240) # Wait for 5 minutes before the next run + time.sleep(250) # Wait for 5 minutes before the next run