From 3dbd3f5576964c4e810d24bb2eeb4bd538d5525b Mon Sep 17 00:00:00 2001 From: Radek Date: Wed, 21 May 2025 15:06:35 +0100 Subject: [PATCH] new n first --- .gitignore | 1 + dash-power.py | 146 ++++++++++++++++++++++++++++++++++ get_all_by_room.py | 194 +++++++++++++++++++++++++++++++++++++++++++++ power_data.db | Bin 0 -> 16384 bytes 4 files changed, 341 insertions(+) create mode 100644 .gitignore create mode 100644 dash-power.py create mode 100644 get_all_by_room.py create mode 100644 power_data.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/dash-power.py b/dash-power.py new file mode 100644 index 0000000..aa6a43a --- /dev/null +++ b/dash-power.py @@ -0,0 +1,146 @@ +import dash +from dash import dcc, html, Input, Output, State +import plotly.express as px +import sqlite3 +import pandas as pd +from datetime import datetime, timedelta + +# Create a Dash app +app = dash.Dash(__name__) + +# Function to fetch data from the SQLite database +def fetch_data(): + conn = sqlite3.connect('power_data.db') + building_totals = pd.read_sql_query('SELECT * FROM building_totals', conn) + room_breakdown = pd.read_sql_query('SELECT * FROM room_breakdown', conn) + conn.close() + return building_totals, room_breakdown + +# Function to calculate kWh usage +def calculate_kwh(data, power_column): + data = data.copy() # Create a copy of the DataFrame to avoid SettingWithCopyWarning + data.loc[:, 'timestamp'] = pd.to_datetime(data['timestamp'], format='ISO8601') + data = data.sort_values('timestamp') + data.loc[:, 'time_diff'] = data['timestamp'].diff().dt.total_seconds() / 3600 # Convert to hours + data.loc[:, 'kwh'] = data[power_column] * data['time_diff'] + data.loc[:, 'cumulative_kwh'] = data['kwh'].cumsum() + return data + +# Define the layout of the dashboard +app.layout = html.Div([ + html.H1("Power and Current Data Dashboard"), + dcc.Dropdown( + id='time-range-selector', + options=[ + {'label': 'Last 6 Hours', 'value': 6}, + {'label': 'Last 12 Hours', 'value': 12}, + {'label': 'Last 1 Day', 'value': 24}, + {'label': 'Last 2 Days', 'value': 48}, + {'label': 'Last 1 Week', 'value': 168}, + {'label': 'Last 1 Month', 'value': 720}, + {'label': 'Last 2 Months', 'value': 1440}, + {'label': 'Last 1 Year', 'value': 8760} + ], + value=6, + clearable=False + ), + dcc.Graph(id='building-totals-graph'), + dcc.Dropdown( + id='room-selector', + options=[{'label': room, 'value': room} for room in fetch_data()[1]['room_number'].unique()], + value=None, + placeholder="Select a room" + ), + dcc.Graph(id='room-graph') +]) + +# Define callbacks to update the graphs +@app.callback( + Output('building-totals-graph', 'figure'), + Input('time-range-selector', 'value'), + Input('building-totals-graph', 'relayoutData') +) +def update_building_totals_graph(time_range, relayoutData): + building_totals, _ = fetch_data() + building_totals = calculate_kwh(building_totals, 'total_power') + + # Handle time range selection + end_time = datetime.now() + start_time = end_time - timedelta(hours=time_range) + filtered_data = building_totals[(building_totals['timestamp'] >= start_time) & (building_totals['timestamp'] <= end_time)] + + # Handle zoom level + if relayoutData and 'xaxis.range[0]' in relayoutData: + zoom_start = pd.to_datetime(relayoutData['xaxis.range[0]']) + zoom_end = pd.to_datetime(relayoutData['xaxis.range[1]']) + filtered_data = filtered_data[(filtered_data['timestamp'] >= zoom_start) & (filtered_data['timestamp'] <= zoom_end)] + + latest_data = filtered_data.iloc[-1] if not filtered_data.empty else None + + fig = px.line(filtered_data, x='timestamp', y=['total_current', 'total_power', 'cumulative_kwh'], + title='Building Totals', labels={'value': 'Value', 'variable': 'Metric'}) + + if latest_data is not None: + fig.update_traces( + name=f"Total Current: {latest_data['total_current']} A", + selector=dict(name="total_current") + ) + fig.update_traces( + name=f"Total Power: {latest_data['total_power']} kW", + selector=dict(name="total_power") + ) + fig.update_traces( + name=f"Cumulative kWh: {round(latest_data['cumulative_kwh'], 3)} kWh", + selector=dict(name="cumulative_kwh") + ) + + return fig + +@app.callback( + Output('room-graph', 'figure'), + Input('time-range-selector', 'value'), + Input('room-selector', 'value'), + Input('room-graph', 'relayoutData') +) +def update_room_graph(time_range, selected_room, relayoutData): + if selected_room: + _, room_breakdown = fetch_data() + room_data = room_breakdown[room_breakdown['room_number'] == selected_room] + room_data = calculate_kwh(room_data, 'power') + + # Handle time range selection + end_time = datetime.now() + start_time = end_time - timedelta(hours=time_range) + filtered_data = room_data[(room_data['timestamp'] >= start_time) & (room_data['timestamp'] <= end_time)] + + # Handle zoom level + if relayoutData and 'xaxis.range[0]' in relayoutData: + zoom_start = pd.to_datetime(relayoutData['xaxis.range[0]']) + zoom_end = pd.to_datetime(relayoutData['xaxis.range[1]']) + filtered_data = filtered_data[(filtered_data['timestamp'] >= zoom_start) & (filtered_data['timestamp'] <= zoom_end)] + + latest_data = filtered_data.iloc[-1] if not filtered_data.empty else None + + fig = px.line(filtered_data, x='timestamp', y=['current', 'power', 'cumulative_kwh'], + title=f'Room {selected_room}', labels={'value': 'Value', 'variable': 'Metric'}) + + if latest_data is not None: + fig.update_traces( + name=f"Current: {latest_data['current']} A", + selector=dict(name="current") + ) + fig.update_traces( + name=f"Power: {latest_data['power']} kW", + selector=dict(name="power") + ) + fig.update_traces( + name=f"Cumulative kWh: {round(latest_data['cumulative_kwh'], 3)} kWh", + selector=dict(name="cumulative_kwh") + ) + + return fig + return px.line(title='Select a room to display its graph') + +# Run the app +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8050, debug=True) diff --git a/get_all_by_room.py b/get_all_by_room.py new file mode 100644 index 0000000..7e945f3 --- /dev/null +++ b/get_all_by_room.py @@ -0,0 +1,194 @@ +import requests +from collections import defaultdict +import argparse +import sqlite3 +from datetime import datetime +import time + +# Configuration +API_KEY = '{api-key}' +LIBRENMS_IP = '{librenms_ip}' +HEADERS = {'X-Auth-Token': API_KEY} + +def create_db_connection(db_file): + """Create a database connection to a SQLite database.""" + conn = None + try: + conn = sqlite3.connect(db_file) + return conn + except sqlite3.Error as e: + print(e) + return conn + +def create_tables(conn): + """Create tables for storing the data.""" + try: + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS building_totals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + total_current REAL, + total_power REAL, + timestamp TEXT + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS room_breakdown ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_number TEXT, + current REAL, + power REAL, + timestamp TEXT + ) + ''') + conn.commit() + except sqlite3.Error as e: + print(e) + +def insert_building_total(conn, total_current, total_power): + """Insert building total data into the database.""" + try: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO building_totals (total_current, total_power, timestamp) + VALUES (?, ?, ?) + ''', (round(total_current, 3), round(total_power, 3), datetime.now().isoformat())) + conn.commit() + except sqlite3.Error as e: + print(e) + +def insert_room_breakdown(conn, room_number, current, power): + """Insert room breakdown data into the database.""" + try: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO room_breakdown (room_number, current, power, timestamp) + VALUES (?, ?, ?, ?) + ''', (room_number, round(current, 3), round(power, 3), datetime.now().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) + if response.status_code == 200: + devices = response.json().get('devices', []) + if debug: + print(f"Devices: {devices}") # Debugging statement + power_devices = [device['device_id'] for device in devices if device.get('type') == 'power'] + return power_devices + else: + raise Exception(f"Failed to fetch devices: {response.status_code}") + +def get_device_location(device_id, debug=False): + """Fetch the location for a given device ID.""" + response = requests.get(f'http://{LIBRENMS_IP}/api/v0/devices/{device_id}', headers=HEADERS, verify=False) + if response.status_code == 200: + location = response.json().get('devices', [{}])[0].get('location', '') + if debug: + print(f"Location for device {device_id}: {location}") # Debugging statement + # Extract room number from location + room_number = location.split(',')[-1].strip() if ',' in location else 'Unknown' + return room_number + else: + raise Exception(f"Failed to fetch location for device {device_id}: {response.status_code}") + +def get_sensor_ids(device_id, sensor_group, debug=False): + """Fetch sensor IDs for a given device ID and sensor group.""" + response = requests.get(f'http://{LIBRENMS_IP}/api/v0/devices/{device_id}/health/{sensor_group}', headers=HEADERS, verify=False) + if response.status_code == 200: + graphs = response.json().get('graphs', []) + if debug: + print(f"Graphs for device {device_id}: {graphs}") # Debugging statement + # Filter sensors based on descriptions + if sensor_group == 'device_current': + relevant_sensors = [ + sensor['sensor_id'] for sensor in graphs + if sensor['desc'] in ["Input Phase 1.1", "Input Phase 1.2", "Input Phase 1.3", "Phase 1"] + ] + elif sensor_group == 'device_power': + relevant_sensors = [ + sensor['sensor_id'] for sensor in graphs + if sensor['desc'] in ["Active power #1", "Total power"] + ] + return relevant_sensors + else: + raise Exception(f"Failed to fetch sensors for device {device_id}: {response.status_code}") + +def get_sensor_value(device_id, sensor_id, sensor_group, debug=False): + """Fetch the current value for a given sensor ID and sensor group.""" + response = requests.get(f'http://{LIBRENMS_IP}/api/v0/devices/{device_id}/health/{sensor_group}/{sensor_id}', headers=HEADERS, verify=False) + if response.status_code == 200: + sensor_data = response.json() + if debug: + print(f"Sensor data for device {device_id}, sensor {sensor_id}: {sensor_data}") # Debugging statement + sensor_value = sensor_data['graphs'][0].get('sensor_current', 0) + sensor_desc = sensor_data['graphs'][0].get('sensor_descr', '') + + # Divide by 100 if the sensor is from an nLogic PDU and is a current sensor + if sensor_group == 'device_current' and sensor_desc in ["Input Phase 1.1", "Input Phase 1.2", "Input Phase 1.3"]: + sensor_value /= 100 + + return sensor_value + else: + raise Exception(f"Failed to fetch sensor value for sensor {sensor_id}: {response.status_code}") + +def main(debug=False): + try: + device_ids = get_device_ids(debug) + total_current = 0 + total_power_watts = 0 + room_current = defaultdict(float) + room_power = defaultdict(float) + + # Create a SQLite database connection + db_file = 'power_data.db' + conn = create_db_connection(db_file) + create_tables(conn) + + for device_id in device_ids: + room_number = get_device_location(device_id, debug) + + # Fetch and sum current values + current_sensor_ids = get_sensor_ids(device_id, 'device_current', debug) + for sensor_id in current_sensor_ids: + sensor_value = get_sensor_value(device_id, sensor_id, 'device_current', debug) + total_current += sensor_value + room_current[room_number] += sensor_value + + # Fetch and sum power values + power_sensor_ids = get_sensor_ids(device_id, 'device_power', debug) + for sensor_id in power_sensor_ids: + sensor_value = get_sensor_value(device_id, sensor_id, 'device_power', debug) + total_power_watts += sensor_value + room_power[room_number] += sensor_value + + total_power_kw = total_power_watts / 1000 # Convert watts to kilowatts + print(f"Total Current: {round(total_current, 3)} A") + print(f"Total Power: {round(total_power_kw, 3)} kW") + + # Insert building total data into the database + insert_building_total(conn, total_current, total_power_kw) + + print("\nBreakdown by Room:") + for room, current in room_current.items(): + power_kw = room_power[room] / 1000 # Convert watts to kilowatts + print(f"Room {room}: Current = {round(current, 3)} A, Power = {round(power_kw, 3)} kW") + + # Insert room breakdown data into the database + insert_room_breakdown(conn, room, current, power_kw) + + # Close the database connection + conn.close() + except Exception as e: + print(str(e)) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Fetch and display power and current data from LibreNMS.') + parser.add_argument('--debug', action='store_true', help='Enable debug output') + args = parser.parse_args() + + while True: + main(debug=args.debug) + time.sleep(300) # Wait for 5 minutes before the next run diff --git a/power_data.db b/power_data.db new file mode 100644 index 0000000000000000000000000000000000000000..4178238a789b9db580cffb5da7d2c137961d8b2c GIT binary patch literal 16384 zcmeI2ZEO_B8OQI%zVq3>I|6424S@?d#IEsrW_NaH_XzbE@LRxO8wUcV7#j|9V4sO? znkH1Vl+vUkfKXm&k~V!s0fJCcl@>)*O`(A*2_*qVq>Yk-XhYl3Mrlh)6jkaoJ2y7B zx1d(-hkn?VPWHY0_L=$rXP%jR<`#9#&kU!NYX%1Udxn#mZ;a3H_pxNs=kqOsM-e>a z6@Y)PeeRcU+~DsbpR##b6h07)?)SyRvBU7kyF3Lv1v~{j1v~{j1v~{j1v~{j1v~{j z1#YCkS0cgigz9Sl*0$lE-oEtUz(9X@?_j!T{px}Hv-!`8=5#hTbTuZs8fMRLOy<9r ztSU`9uFUFW>w>Pv=ElxsduMA~L+8@uyvC);hP%5Kwl08MwKXp2no&U6x6N+s?@bRT zyBhCt`NXP?gM;bpa1t6bzmVTBa6gdl?+s`A(?i2O{Tq^Vp&wnXZH;;1tJ?#i2{kqT zrM417_w~Uy*zTe9eH+u+RcXi5V4fC^H>A^DXf zbNBw~xzbr1e!w!T_Fi0c^|S-PDMS_Bu!v6TLOI~fk&gGbO<|c$a~|Ikx;-Bt&>l-M z4XT@ZU2zULUv=f!uH`KAaBth04-e!6nw7#xQ4zIFtF9;q{Kb}oWga_Nf~Wt{0ca^~ zD#Xx{X4M6Az^Pmxw(P!l@cG;F0hRIy8rC$`tPA9T_jrq{A;Q+N**!ZP08U{;F|kH5 zsq^Q63)?0i>A1);Pdxji|EC`<(AI>rz5M*Y z#dp*fe)e?)X+y~{M$wwG{ zJ^72L_g*ZdOvN%RgjCWbDIK%^tf(rrme{l#MKxZzB8`$Fl%<&6EtWWY^;f?h|BxG{ zBUM9$%pIXjmJH))(F^q^LqEg4!KvkS)|zmN_U&W=-o0igL2xr)$teM47{~SM znJoXAD#EHt>SZ`?*?!STde&Cv+D490Sl6jVJ|ih^StTz@CRob}s7VeVb*GT2BQi@; z+%g?1Z&m+biI>0o%1c|vx>Cr%L?d^OP$shu3#wxLLp6c&yWA;k05?-m!m@Y8psI|X z`EdUym)s}{W6?q+B`I#%AosbHwNHUituMM!28KbU5=*v_TlNAU^_{G#?=`qixltM= zP#C4A!!ll3e1F?@EQ129r9f?0Ba~1Xj$3vBRJ?MW(Otitxq8{Hj&L@GEn*Ra7?R?a zH9QH|Q>^8eVA#4Z6;ga|YKYf+C3UT3e2&!w+$hS^01W>T3M_j`%*_fh+uSHR&BP1tbL}aHuyUC?D%86wR+mj92Z+$;>6Tj`@w06f#VWD3aoq)rzjQ zyuqi-fGdTlfecbBDQ?*$kw6kBdG={>r-*KvWX1>uiLuxoimYNTB<@8Yxl>4k!B`_G zVHqFz9hX_9ogYgJC>X$&r9)mMpO%z8%fy*R`@~N!^=+T)P9e+0q*}I+TlP(nbDEYH zq;6Yz9?PVrg=p1vEGt~gNQxgqs0yg4tCZX!!@(@;St4@2`SIIRU;q8e{6=yLD<&~u zG?3|%;+C0W1#CH=bgqCR7FHm0W0jEGCB-dMgdmBZ@q?Qow^$mru%VL55ejA*ANO#4 zli&%iZ@U|Xk!r$xnInFqv?jA>|=ipf+-amzlJn`Kd#)Ga6j_5&s* zx5*ZA%bvDp3v24QP79GjkZwVOOa3R8iDMwkg4L5uk>R*yYwZ!wEPGC`a0rs4bWKHM zvZT0Wvqd^>IXCabvmZR;PGQxu$gPs%mf4f6nk9~|+x+p{(_ASNQlmvCjZiSl?9tK5 zD!#)SqNi4IWup{-LvKH`wkV(KutI_RV{;(xD3ZF z+aZ<+Yo~3vBb=a+r5ielNs3!mp0n(*OXd)(_M<|$HVn4+y_UzzW2sa8g+$PdUnV^JaKM|RJ+8^wbVK)kPU?$A6Qb}>kx%wjN!srCQqR)1;LJw zAQG10xMhpQF4z(|ZZw>vOvOYxGbvqX4QA)G;Zy5dsoXA5U literal 0 HcmV?d00001