# ============================================================
# ENERGYGURU – POWER CALCULUS
# Streamlit Dashboard | dashboard.py
# Run: streamlit run dashboard.py
# ============================================================
import streamlit as st
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
import serial
import serial.tools.list_ports
import time
import math
import random
from datetime import datetime, timedelta
# ── Page config (MUST be first Streamlit call) ───────────────
st.set_page_config(
page_title="EnergyGuru – Power Calculus",
page_icon="⚡",
layout="wide",
initial_sidebar_state="expanded",
)
# ── Custom CSS ───────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ── Constants ────────────────────────────────────────────────
CITY_LOCATIONS = {
"Rawalpindi City Model": {"lat": 33.6007, "lon": 73.0679, "desc": "Punjab, Pakistan"},
"Islamabad Lab": {"lat": 33.7215, "lon": 73.0433, "desc": "Federal Capital, Pakistan"},
"Lahore Smart Lab": {"lat": 31.5204, "lon": 74.3587, "desc": "Punjab, Pakistan"},
"Karachi IoT Hub": {"lat": 24.8607, "lon": 67.0011, "desc": "Sindh, Pakistan"},
"Custom Location": {"lat": 33.6007, "lon": 73.0679, "desc": "User-defined"},
}
ACCENT_COLORS = {
"voltage": "#00c8ff",
"current": "#ff9500",
"power": "#ff4466",
"energy": "#00ff99",
"bill": "#ffd700",
"carbon": "#88ff00",
}
# ── Session State Init ───────────────────────────────────────
COLS = ['timestamp', 'voltage', 'current', 'power',
'energy_kwh', 'bill_pkr', 'carbon_kg', 'runtime_hrs']
def _init_state():
defaults = {
'data_log': pd.DataFrame(columns=COLS),
'connected': False,
'serial_conn': None,
'demo_mode': True,
'demo_v_base': 220.0,
'demo_i_base': 1.8,
'demo_energy': 0.0,
'demo_tick': 0,
'last_sample_ts': 0.0,
'sample_interval_s': 1.0,
'latest': {k: 0.0 for k in
['voltage','current','power','energy_kwh','bill_pkr','carbon_kg','runtime_hrs']},
}
for k, v in defaults.items():
if k not in st.session_state:
st.session_state[k] = v
_init_state()
# ── Sidebar ──────────────────────────────────────────────────
with st.sidebar:
st.markdown("""
⚡ ENERGYGURU
POWER CALCULUS v1.0
""", unsafe_allow_html=True)
st.markdown('Connection
', unsafe_allow_html=True)
demo_mode = st.toggle("🎮 Demo Mode (No Hardware)", value=st.session_state.demo_mode)
st.session_state.demo_mode = demo_mode
if not demo_mode:
ports = [p.device for p in serial.tools.list_ports.comports()]
sel_port = st.selectbox("Serial Port", ports if ports else ["No ports found"])
baud = st.selectbox("Baud Rate", [9600, 115200], index=0)
c1, c2 = st.columns(2)
with c1:
if st.button("▶ Connect", use_container_width=True):
try:
st.session_state.serial_conn = serial.Serial(sel_port, baud, timeout=1)
st.session_state.connected = True
st.success("Connected!")
except Exception as e:
st.error(str(e))
with c2:
if st.button("■ Disconnect", use_container_width=True):
if st.session_state.serial_conn:
try: st.session_state.serial_conn.close()
except: pass
st.session_state.connected = False
st.session_state.serial_conn = None
# Status indicator
if demo_mode:
st.markdown('◉ DEMO MODE ACTIVE
', unsafe_allow_html=True)
elif st.session_state.connected:
st.markdown('◉ ARDUINO CONNECTED
', unsafe_allow_html=True)
else:
st.markdown('◉ DISCONNECTED
', unsafe_allow_html=True)
if demo_mode:
st.markdown('Demo Controls
', unsafe_allow_html=True)
st.session_state.demo_v_base = st.slider(
"Demo Voltage (V)",
min_value=180.0,
max_value=260.0,
value=float(st.session_state.demo_v_base),
step=0.5,
)
st.session_state.demo_i_base = st.slider(
"Demo Current (A)",
min_value=0.1,
max_value=5.0,
value=float(st.session_state.demo_i_base),
step=0.05,
)
if st.button("↺ Reset Demo Profile", use_container_width=True):
# Revert to the original synthetic profile baseline.
st.session_state.demo_v_base = 220.0
st.session_state.demo_i_base = 1.8
st.success("Demo profile reset to default dummy data behavior.")
st.markdown('Settings
', unsafe_allow_html=True)
rate = st.number_input("💰 Tariff (PKR / kWh)", 1.0, 500.0, 50.0, 1.0)
carbon = st.number_input("🌱 Carbon Factor (kg CO₂ / kWh)", 0.1, 3.0, 0.82, 0.01)
st.markdown('Location
', unsafe_allow_html=True)
city_choice = st.selectbox("City / Lab", list(CITY_LOCATIONS.keys()))
if city_choice == "Custom Location":
CITY_LOCATIONS["Custom Location"]["lat"] = st.number_input("Latitude", value=33.6007)
CITY_LOCATIONS["Custom Location"]["lon"] = st.number_input("Longitude", value=73.0679)
CITY_LOCATIONS["Custom Location"]["desc"] = st.text_input("Description", "Custom Site")
loc = CITY_LOCATIONS[city_choice]
st.markdown('Controls
', unsafe_allow_html=True)
if st.button("🗑 Clear Data Log", use_container_width=True):
st.session_state.data_log = pd.DataFrame(columns=COLS)
st.session_state.demo_energy = 0.0
st.session_state.demo_tick = 0
st.session_state.last_sample_ts = 0.0
st.success("Cleared!")
# Buffer info
n = len(st.session_state.data_log)
st.markdown(f'Buffer: {n}/500 readings
',
unsafe_allow_html=True)
# ── Data Functions ───────────────────────────────────────────
def _demo_reading():
"""Simulate a realistic city-model reading."""
t = st.session_state.demo_tick
st.session_state.demo_tick += 1
v_base = float(st.session_state.demo_v_base)
i_base = float(st.session_state.demo_i_base)
# AC voltage ~220 V with ±6 V fluctuation
v = v_base + 5 * math.sin(t * 0.07) + random.uniform(-2, 2)
# Load current varies 0.8–2.8 A (city model lamps + motors)
i = i_base + 0.6 * math.sin(t * 0.04) + 0.2 * math.sin(t * 0.13) + random.uniform(-0.05, 0.05)
i = max(0.1, i)
p = v * i
dt_h = 1 / 3600
st.session_state.demo_energy += (p / 1000) * dt_h
e = st.session_state.demo_energy
b = e * rate
co2 = e * carbon
rth = t / 3600
return dict(voltage=round(v,2), current=round(i,3),
power=round(p,2), energy_kwh=round(e,6),
bill_pkr=round(b,4), carbon_kg=round(co2,6),
runtime_hrs=round(rth,5))
def _arduino_reading():
"""Read one CSV line from Arduino serial."""
conn = st.session_state.serial_conn
if not conn or not st.session_state.connected:
return None
try:
raw = conn.readline().decode('utf-8', errors='ignore').strip()
if ',' in raw:
p = raw.split(',')
if len(p) == 7:
return dict(voltage=float(p[0]), current=float(p[1]),
power=float(p[2]), energy_kwh=float(p[3]),
bill_pkr=float(p[4]), carbon_kg=float(p[5]),
runtime_hrs=float(p[6]))
except Exception:
pass
return None
def _log(data):
"""Append to session log; keep last 500 rows."""
row = pd.DataFrame([{"timestamp": datetime.now(), **data}])
st.session_state.data_log = pd.concat(
[st.session_state.data_log, row], ignore_index=True
).tail(500)
st.session_state.latest = data
# ── Fetch current reading ────────────────────────────────────
now_ts = time.time()
can_sample = (now_ts - float(st.session_state.last_sample_ts)) >= float(st.session_state.sample_interval_s)
if can_sample:
if st.session_state.demo_mode:
_log(_demo_reading())
st.session_state.last_sample_ts = now_ts
elif st.session_state.connected:
d = _arduino_reading()
if d:
_log(d)
st.session_state.last_sample_ts = now_ts
latest = st.session_state.latest
df = st.session_state.data_log.copy()
def render_footer():
st.markdown(
'',
unsafe_allow_html=True
)
def render_device_alerts(latest_row):
if st.session_state.demo_mode:
st.info("🧪 Demo mode is active. Values are simulated unless connected to Arduino.")
return
if not st.session_state.connected:
st.error("🔴 Device status: Arduino is disconnected.")
return
v = float(latest_row.get("voltage", 0.0))
i = float(latest_row.get("current", 0.0))
p = float(latest_row.get("power", 0.0))
if v < 195 or v > 255:
st.error("🔴 Critical voltage detected. Check supply and wiring.")
elif v < 210 or v > 240:
st.warning("🟠 Voltage is borderline. Monitor stability.")
else:
st.success("🟢 Device status: connected and electrical values are stable.")
if i > 4.2:
st.warning("🟠 High current draw detected.")
if p > 1000:
st.warning("🟠 Power near upper expected limit.")
# ── Header ───────────────────────────────────────────────────
st.markdown("""
⚡ ENERGYGURU
POWER CALCULUS — AI-Assisted City Energy Monitor
""", unsafe_allow_html=True)
# ── Tabs ─────────────────────────────────────────────────────
tab1, tab2, tab3, tab4 = st.tabs([
"⚡ Live Dashboard",
"🗺️ City Map",
"📈 Analytics",
"📄 Report",
])
# ════════════════════════════════════════════════════════════
# TAB 1 – LIVE DASHBOARD
# ════════════════════════════════════════════════════════════
with tab1:
render_device_alerts(latest)
# ── 6 metric cards ──────────────────────────────────────
cards = [
("⚡ VOLTAGE", f"{latest['voltage']:.1f} V", "#00c8ff"),
("🔌 CURRENT", f"{latest['current']:.3f} A", "#ff9500"),
("💡 POWER", f"{latest['power']:.1f} W", "#ff4466"),
("🔋 ENERGY", f"{latest['energy_kwh']:.5f} kWh", "#00ff99"),
("💰 BILL", f"₨ {latest['bill_pkr']:.3f}", "#ffd700"),
("🌱 CO₂", f"{latest['carbon_kg']:.5f} kg", "#88ff00"),
]
cards_per_row = 6
for i in range(0, len(cards), cards_per_row):
row_cards = cards[i:i + cards_per_row]
cols = st.columns(len(row_cards))
for col, (lbl, val, clr) in zip(cols, row_cards):
with col:
st.markdown(f"""
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
# ── 3 gauges ────────────────────────────────────────────
def gauge(value, title, max_v, color, unit, threshold=0.85):
fig = go.Figure(go.Indicator(
mode="gauge+number",
value=value,
title={'text': title, 'font': {'color': '#8a9ab0', 'size': 12,
'family': 'Share Tech Mono'}},
number={'suffix': f' {unit}', 'font': {'color': color, 'size': 20,
'family': 'Share Tech Mono'}},
gauge={
'axis': {'range': [0, max_v], 'tickcolor': '#2a3a50',
'tickfont': {'size': 9, 'color': '#4a6080'}},
'bar': {'color': color, 'thickness': 0.25},
'bgcolor': '#0d1422',
'bordercolor': '#1a2840', 'borderwidth': 1,
'steps': [
{'range': [0, max_v * 0.5], 'color': '#0d1422'},
{'range': [max_v * 0.5, max_v * threshold], 'color': '#111d2e'},
{'range': [max_v * threshold, max_v], 'color': '#1a1020'},
],
'threshold': {'line': {'color': '#ff4466', 'width': 2},
'thickness': 0.75, 'value': max_v * threshold}
}
))
fig.update_layout(paper_bgcolor='#080c14', font_color='white',
height=200, margin=dict(l=15,r=15,t=40,b=5))
return fig
gauge_cols = st.columns(3)
gauge_specs = [
(latest['voltage'], "VOLTAGE (V)", 260, "#00c8ff", "V"),
(latest['current'], "CURRENT (A)", 5, "#ff9500", "A"),
(latest['power'], "POWER (W)", 1100, "#ff4466", "W"),
]
for col, spec in zip(gauge_cols, gauge_specs):
with col:
st.plotly_chart(gauge(*spec), use_container_width=True)
# ── Real-time charts ────────────────────────────────────
if len(df) > 1:
rc1, rc2 = st.columns(2)
with rc1:
st.markdown('Voltage & Current — Live
', unsafe_allow_html=True)
fig_vc = go.Figure()
fig_vc.add_trace(go.Scatter(
x=df['timestamp'], y=df['voltage'],
name='Voltage (V)', line=dict(color='#00c8ff', width=1.8), yaxis='y1'
))
fig_vc.add_trace(go.Scatter(
x=df['timestamp'], y=df['current'],
name='Current (A)', line=dict(color='#ff9500', width=1.8), yaxis='y2'
))
fig_vc.update_layout(
paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
font_color='white', height=260,
yaxis=dict(title='V', color='#00c8ff', gridcolor='#0d1e2e'),
yaxis2=dict(title='A', overlaying='y', side='right',
color='#ff9500', gridcolor='#0d1e2e'),
legend=dict(bgcolor='#0d1422', font=dict(size=10)),
margin=dict(l=8,r=8,t=8,b=8),
)
st.plotly_chart(fig_vc, use_container_width=True)
with rc2:
st.markdown('Power — Live
', unsafe_allow_html=True)
fig_pw = go.Figure()
fig_pw.add_trace(go.Scatter(
x=df['timestamp'], y=df['power'],
fill='tozeroy', name='Power (W)',
line=dict(color='#ff4466', width=1.8),
fillcolor='rgba(255,68,102,0.15)'
))
fig_pw.update_layout(
paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
font_color='white', height=260,
yaxis=dict(title='Watts', gridcolor='#0d1e2e'),
margin=dict(l=8,r=8,t=8,b=8),
)
st.plotly_chart(fig_pw, use_container_width=True)
else:
st.info("📡 Collecting readings… Charts will appear after a few seconds.")
# ════════════════════════════════════════════════════════════
# TAB 2 – CITY MAP
# ════════════════════════════════════════════════════════════
with tab2:
map_col, info_col = st.columns([3, 1])
with map_col:
st.markdown('City Energy Monitor — Location
',
unsafe_allow_html=True)
# Build coverage ring
ring_lats = [loc['lat'] + 0.012 * math.cos(math.radians(i)) for i in range(361)]
ring_lons = [loc['lon'] + 0.018 * math.sin(math.radians(i)) for i in range(361)]
fig_map = go.Figure()
# Coverage ring
fig_map.add_trace(go.Scattermapbox(
lat=ring_lats, lon=ring_lons,
mode='lines',
line=dict(color='rgba(0,200,255,0.4)', width=2),
name='Monitor Zone', showlegend=False,
))
# Main marker
fig_map.add_trace(go.Scattermapbox(
lat=[loc['lat']], lon=[loc['lon']],
mode='markers+text',
marker=dict(size=18, color='#00c8ff',
symbol='circle', opacity=0.9),
text=[f"⚡ {city_choice}"],
textposition='top right',
textfont=dict(color='white', size=13, family='Share Tech Mono'),
name=city_choice,
))
fig_map.update_layout(
mapbox=dict(
style='open-street-map',
center=dict(lat=loc['lat'], lon=loc['lon']),
zoom=13,
),
paper_bgcolor='#080c14',
font_color='white',
height=480,
margin=dict(l=0, r=0, t=0, b=0),
showlegend=False,
)
st.plotly_chart(fig_map, use_container_width=True)
with info_col:
st.markdown('Location
', unsafe_allow_html=True)
st.markdown(f"""
{city_choice}
{loc['desc']}
LAT: {loc['lat']:.4f}°
LON: {loc['lon']:.4f}°
""", unsafe_allow_html=True)
st.markdown('Live Readings
', unsafe_allow_html=True)
st.metric("Voltage", f"{latest['voltage']:.1f} V")
st.metric("Current", f"{latest['current']:.3f} A")
st.metric("Power", f"{latest['power']:.1f} W")
st.markdown('Totals
', unsafe_allow_html=True)
st.metric("Energy", f"{latest['energy_kwh']:.5f} kWh")
st.metric("Bill", f"₨ {latest['bill_pkr']:.3f}")
st.metric("CO₂", f"{latest['carbon_kg']:.5f} kg")
# Voltage health check
st.markdown('Power Quality
', unsafe_allow_html=True)
v = latest['voltage']
if 210 <= v <= 240:
st.success("✅ Voltage Normal")
elif 195 <= v < 210 or 240 < v <= 255:
st.warning("⚠️ Voltage Borderline")
else:
st.error("❌ Voltage Abnormal")
# ════════════════════════════════════════════════════════════
# TAB 3 – ANALYTICS
# ════════════════════════════════════════════════════════════
with tab3:
if len(df) < 5:
st.info("📡 Need at least 5 readings — collecting data…")
else:
# Summary row
st.markdown('Summary Statistics
', unsafe_allow_html=True)
s1,s2,s3,s4,s5 = st.columns(5)
s1.metric("Avg Voltage", f"{df['voltage'].mean():.2f} V", f"σ={df['voltage'].std():.2f}")
s2.metric("Avg Current", f"{df['current'].mean():.3f} A", f"σ={df['current'].std():.3f}")
s3.metric("Avg Power", f"{df['power'].mean():.1f} W")
s4.metric("Peak Power", f"{df['power'].max():.1f} W")
s5.metric("Total Energy", f"{df['energy_kwh'].iloc[-1]:.5f} kWh")
st.markdown('Energy & Bill Trends
', unsafe_allow_html=True)
ac1, ac2 = st.columns(2)
with ac1:
fig_e = px.area(df, x='timestamp', y='energy_kwh',
color_discrete_sequence=['#00ff99'],
labels={'energy_kwh': 'kWh', 'timestamp': ''})
fig_e.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
font_color='white', height=260,
margin=dict(l=8,r=8,t=8,b=8))
st.plotly_chart(fig_e, use_container_width=True)
with ac2:
fig_bc = go.Figure()
fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['bill_pkr'],
name='Bill (PKR)', yaxis='y1',
line=dict(color='#ffd700', width=1.8)))
fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['carbon_kg'],
name='CO₂ (kg)', yaxis='y2',
line=dict(color='#88ff00', width=1.8)))
fig_bc.update_layout(
paper_bgcolor='#080c14', plot_bgcolor='#0d1422', font_color='white',
height=260,
yaxis=dict(title='PKR', color='#ffd700', gridcolor='#0d1e2e'),
yaxis2=dict(title='kg CO₂', overlaying='y', side='right',
color='#88ff00'),
legend=dict(bgcolor='#0d1422', font=dict(size=10)),
margin=dict(l=8,r=8,t=8,b=8),
)
st.plotly_chart(fig_bc, use_container_width=True)
# Power histogram
st.markdown('Power Distribution
', unsafe_allow_html=True)
fig_h = px.histogram(df, x='power', nbins=30,
color_discrete_sequence=['#ff4466'],
labels={'power': 'Power (W)', 'count': 'Frequency'})
fig_h.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
font_color='white', height=240,
margin=dict(l=8,r=8,t=8,b=8))
st.plotly_chart(fig_h, use_container_width=True)
# AI Prediction (linear regression on energy)
reg_df = df.copy()
reg_df['energy_kwh'] = pd.to_numeric(reg_df['energy_kwh'], errors='coerce')
reg_df['timestamp'] = pd.to_datetime(reg_df['timestamp'], errors='coerce')
reg_df = reg_df.dropna(subset=['energy_kwh', 'timestamp']).reset_index(drop=True)
if len(reg_df) >= 15:
st.markdown('AI Energy Prediction (Linear Regression)
',
unsafe_allow_html=True)
x = np.arange(len(reg_df), dtype=float)
y = reg_df['energy_kwh'].to_numpy(dtype=float)
coeffs = np.polyfit(x, y, 1)
n_future = 60
fx = np.arange(len(reg_df), len(reg_df) + n_future, dtype=float)
fy = np.polyval(coeffs, fx)
ft = [reg_df['timestamp'].iloc[-1] + timedelta(seconds=i) for i in range(1, n_future+1)]
fig_pr = go.Figure()
fig_pr.add_trace(go.Scatter(x=reg_df['timestamp'], y=reg_df['energy_kwh'],
name='Actual', line=dict(color='#00ff99', width=2)))
fig_pr.add_trace(go.Scatter(x=ft, y=fy,
name='Predicted (60 s)', yaxis='y1',
line=dict(color='#ffd700', width=1.8, dash='dot')))
fig_pr.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
font_color='white', height=260,
yaxis=dict(title='kWh', gridcolor='#0d1e2e'),
legend=dict(bgcolor='#0d1422'),
margin=dict(l=8,r=8,t=8,b=8))
st.plotly_chart(fig_pr, use_container_width=True)
# Extrapolate
rate_kwh_per_s = coeffs[0]
daily = rate_kwh_per_s * 86400
monthly = daily * 30
p1,p2,p3,p4 = st.columns(4)
p1.metric("⏱ Rate", f"{rate_kwh_per_s*3600:.4f} kWh/hr")
p2.metric("📅 Daily Est.", f"{daily:.4f} kWh", f"₨ {daily*rate:.2f}")
p3.metric("📅 Monthly Est.", f"{monthly:.3f} kWh", f"₨ {monthly*rate:.2f}")
p4.metric("🌱 Monthly CO₂", f"{monthly*carbon:.3f} kg")
# Data table
st.markdown('Data Log (last 50 readings)
',
unsafe_allow_html=True)
disp = df.tail(50).copy()
disp['timestamp'] = pd.to_datetime(disp['timestamp'], errors='coerce')
disp['timestamp'] = disp['timestamp'].dt.strftime('%H:%M:%S').fillna('--:--:--')
st.dataframe(disp, use_container_width=True, height=260)
csv_bytes = df.to_csv(index=False).encode()
st.download_button("⬇️ Download CSV", csv_bytes,
f"energyguru_{datetime.now():%Y%m%d_%H%M%S}.csv",
"text/csv", use_container_width=True)
# ════════════════════════════════════════════════════════════
# TAB 4 – REPORT GENERATOR
# ════════════════════════════════════════════════════════════
with tab4:
st.markdown('Report Configuration
', unsafe_allow_html=True)
if "report_pdf_bytes" not in st.session_state:
st.session_state.report_pdf_bytes = None
if "report_pdf_filename" not in st.session_state:
st.session_state.report_pdf_filename = None
rc1, rc2 = st.columns(2)
with rc1:
rpt_title = st.text_input("Report Title", "EnergyGuru – Power Calculus Report")
institution = st.text_input("Institution", "Smart Energy Lab")
operator = st.text_input("Operator", "")
project_id = st.text_input("Project ID", "ENERGYGURU-2025-001")
with rc2:
notes = st.text_area("Notes / Remarks",
"Generated by EnergyGuru Power Calculus System.\n"
"Arduino-based IoT Energy Monitoring | City Model.")
generate_btn = st.button("📊 Generate PDF Report", type="primary",
use_container_width=True)
if generate_btn:
if len(df) < 2:
st.error("⚠️ Not enough data — collect at least 2 readings first.")
else:
try:
from fpdf import FPDF # pip install fpdf2
# ── Compute summary stats ───────────────────
avg_v = df['voltage'].mean()
avg_i = df['current'].mean()
avg_p = df['power'].mean()
max_p = df['power'].max()
min_p = df['power'].min()
tot_e = df['energy_kwh'].iloc[-1]
tot_b = df['bill_pkr'].iloc[-1]
tot_co2 = df['carbon_kg'].iloc[-1]
n_reads = len(df)
dur_s = n_reads # 1 reading per second
# Monthly projections from average power trend.
monthly_energy_est = (avg_p / 1000.0) * 24 * 30
monthly_cost_est = monthly_energy_est * rate
monthly_co2_est = monthly_energy_est * carbon
# ── AI Recommendations ──────────────────────
recs = []
if avg_p > 500:
recs.append("HIGH load detected - consider switching off idle appliances.")
if avg_v < 210 or avg_v > 235:
recs.append("Voltage outside safe range (210-235 V) - check power supply.")
if df['voltage'].std() > 8:
recs.append("High voltage fluctuation - consider a voltage stabiliser.")
recs.append("Use LED lighting to reduce city model consumption by ~70%.")
recs.append("Schedule high-load demos during off-peak hours (22:00-06:00).")
recs.append("Install capacitor banks to improve power factor.")
recs.append("Regular maintenance reduces standby losses significantly.")
def pdf_safe(text):
if text is None:
return ""
normalized = str(text).translate(str.maketrans({
"–": "-",
"—": "-",
"•": "|",
"°": " deg",
"₂": "2",
"₨": "Rs",
"✓": "[+]",
"⚠": "[!]",
}))
return normalized.encode("latin-1", "replace").decode("latin-1")
# ── Build PDF ───────────────────────────────
class EnergyPDF(FPDF):
def header(self):
# Dark top bar
self.set_fill_color(8, 12, 20)
self.rect(0, 0, 210, 297, 'F')
self.set_fill_color(0, 40, 60)
self.rect(0, 0, 210, 22, 'F')
self.set_fill_color(0, 200, 255)
self.rect(0, 0, 4, 22, 'F')
self.set_font('Helvetica', 'B', 14)
self.set_text_color(0, 200, 255)
self.set_xy(8, 4)
self.cell(100, 7, pdf_safe('ENERGYGURU - POWER CALCULUS'), ln=False)
self.set_font('Helvetica', '', 7)
self.set_text_color(80, 120, 160)
self.set_xy(8, 13)
self.cell(0, 5, pdf_safe(
f'AI-Assisted Energy Usage Analyzer | '
f'Generated: {datetime.now():%Y-%m-%d %H:%M:%S}'))
self.ln(14)
def footer(self):
self.set_y(-14)
self.set_font('Helvetica', 'I', 7)
self.set_text_color(50, 70, 100)
self.cell(0, 8,
pdf_safe(f'EnergyGuru Power Calculus | {institution} | Page {self.page_no()}'),
align='C')
def section_title(self, txt):
self.set_fill_color(0, 30, 50)
self.set_draw_color(0, 200, 255)
self.set_line_width(0.3)
self.rect(self.get_x(), self.get_y(), 185, 8, 'DF')
self.set_font('Helvetica', 'B', 9)
self.set_text_color(0, 200, 255)
self.cell(0, 8, pdf_safe(f' {txt}'), ln=True)
self.ln(2)
def kv_row(self, label, value, fill_idx):
if fill_idx % 2 == 0:
self.set_fill_color(13, 20, 34)
else:
self.set_fill_color(10, 16, 28)
self.set_text_color(100, 140, 180)
self.set_font('Helvetica', '', 9)
self.cell(90, 7, pdf_safe(f' {label}'), fill=True)
self.set_text_color(220, 230, 240)
self.set_font('Helvetica', 'B', 9)
self.cell(95, 7, pdf_safe(f' {value}'), fill=True, ln=True)
pdf = EnergyPDF()
pdf.set_auto_page_break(auto=True, margin=18)
pdf.add_page()
# Title block
pdf.set_font('Helvetica', 'B', 17)
pdf.set_text_color(0, 200, 255)
pdf.cell(0, 10, pdf_safe(rpt_title), ln=True, align='C')
pdf.ln(1)
pdf.set_font('Helvetica', '', 9)
pdf.set_text_color(80, 120, 160)
pdf.cell(0, 6, pdf_safe(f'Institution: {institution} | Project: {project_id}'), ln=True, align='C')
pdf.cell(0, 6, pdf_safe(f'Location: {city_choice} | Lat {loc["lat"]:.4f} deg Lon {loc["lon"]:.4f} deg'), ln=True, align='C')
if operator:
pdf.cell(0, 6, pdf_safe(f'Operator: {operator}'), ln=True, align='C')
pdf.cell(0, 6, pdf_safe(f'Date: {datetime.now():%B %d, %Y} Time: {datetime.now():%H:%M:%S}'), ln=True, align='C')
pdf.ln(4)
# Divider
pdf.set_draw_color(0, 60, 90)
pdf.set_line_width(0.4)
pdf.line(15, pdf.get_y(), 195, pdf.get_y())
pdf.ln(5)
# ── Section 1: Measurements ──────────────────
pdf.section_title('1. MEASUREMENT SUMMARY')
rows = [
("Average Voltage", f"{avg_v:.2f} V"),
("Average Current", f"{avg_i:.3f} A"),
("Average Power", f"{avg_p:.2f} W"),
("Peak Power", f"{max_p:.2f} W"),
("Minimum Power", f"{min_p:.2f} W"),
("Total Energy Consumed", f"{tot_e:.6f} kWh"),
("Electricity Bill", f"PKR {tot_b:.4f}"),
("Carbon Footprint", f"{tot_co2:.6f} kg CO2"),
("Monthly Energy (Est.)", f"{monthly_energy_est:.3f} kWh"),
("Monthly Cost (Est.)", f"PKR {monthly_cost_est:.2f}"),
("Monthly CO2 (Est.)", f"{monthly_co2_est:.3f} kg"),
("Tariff Rate", f"PKR {rate:.2f} / kWh"),
("Carbon Factor", f"{carbon:.2f} kg CO₂ / kWh"),
("Total Readings", f"{n_reads}"),
("Monitoring Duration", f"{dur_s} seconds ({dur_s/60:.1f} min)"),
]
for idx, (lbl, val) in enumerate(rows):
pdf.kv_row(lbl, val, idx)
pdf.ln(5)
# ── Section 2: Arduino Calculations ─────────
pdf.section_title('2. ARDUINO CALCULATIONS')
pdf.set_fill_color(8, 16, 26)
pdf.set_font('Courier', '', 8)
pdf.set_text_color(0, 220, 120)
calc_lines = [
'',
f' // Instantaneous Power',
f' power_W = voltage_V * current_A',
f' = {avg_v:.2f} V * {avg_i:.3f} A = {avg_p:.2f} W',
'',
f' // Energy accumulation (per interval)',
f' energy_kWh += (power_W / 1000.0) * dt_hours',
f' total_energy = {tot_e:.6f} kWh',
'',
f' // Electricity Bill',
f' bill_PKR = energy_kWh * tariff',
f' = {tot_e:.6f} * {rate:.2f} = PKR {tot_b:.4f}',
'',
f' // Carbon Footprint',
f' carbon_kg = energy_kWh * carbon_factor',
f' = {tot_e:.6f} * {carbon:.2f} = {tot_co2:.6f} kg CO2',
'',
f' // Apparent Power',
f' S (VA) = {avg_v:.2f} V * {avg_i:.3f} A = {avg_v*avg_i:.2f} VA',
'',
]
for ln_text in calc_lines:
pdf.cell(0, 6, ln_text, fill=True, ln=True)
pdf.ln(4)
# ── Section 3: Last 20 readings ──────────────
pdf.section_title('3. RECENT READINGS (last 20)')
hdrs = ['Time', 'V (V)', 'I (A)', 'P (W)',
'kWh', 'Bill Rs', 'CO2 kg']
c_widths = [26, 22, 22, 25, 32, 30, 28]
# Table header
pdf.set_fill_color(0, 40, 60)
pdf.set_text_color(0, 200, 255)
pdf.set_font('Helvetica', 'B', 8)
for h, w in zip(hdrs, c_widths):
pdf.cell(w, 7, pdf_safe(h), fill=True, align='C')
pdf.ln()
pdf.set_font('Helvetica', '', 8)
recent = df.tail(20)
for idx, (_, row) in enumerate(recent.iterrows()):
bg = (13, 20, 34) if idx % 2 == 0 else (10, 16, 28)
pdf.set_fill_color(*bg)
pdf.set_text_color(180, 200, 220)
ts = row['timestamp'].strftime('%H:%M:%S') if hasattr(row['timestamp'], 'strftime') else str(row['timestamp'])[:8]
vals = [ts,
f"{row['voltage']:.1f}",
f"{row['current']:.3f}",
f"{row['power']:.1f}",
f"{row['energy_kwh']:.6f}",
f"{row['bill_pkr']:.4f}",
f"{row['carbon_kg']:.6f}"]
for v, w in zip(vals, c_widths):
pdf.cell(w, 6, pdf_safe(v), fill=True, align='C')
pdf.ln()
pdf.ln(4)
# ── Section 4: AI Recommendations ───────────
pdf.section_title('4. AI ENERGY RECOMMENDATIONS')
pdf.set_font('Helvetica', '', 9)
for i, rec in enumerate(recs):
icon = '[!]' if rec.startswith('HIGH') or rec.startswith('Voltage') or rec.startswith('High') else '[+]'
clr = (255, 180, 60) if icon == '[!]' else (100, 220, 130)
pdf.set_text_color(*clr)
pdf.cell(0, 8, pdf_safe(f' {icon} {rec}'), ln=True)
pdf.ln(3)
# ── Notes ────────────────────────────────────
if notes.strip():
pdf.set_draw_color(0, 60, 90)
pdf.line(15, pdf.get_y(), 195, pdf.get_y())
pdf.ln(3)
pdf.set_font('Helvetica', 'B', 9)
pdf.set_text_color(60, 100, 140)
pdf.cell(0, 7, 'NOTES:', ln=True)
pdf.set_font('Helvetica', '', 8)
pdf.set_text_color(140, 160, 180)
for ln_text in notes.split('\n'):
pdf.cell(0, 6, pdf_safe(ln_text), ln=True)
# ── Output ───────────────────────────────────
pdf_bytes = bytes(pdf.output())
st.success("✅ Report generated successfully!")
filename = f"EnergyGuru_Report_{datetime.now():%Y%m%d_%H%M%S}.pdf"
st.session_state.report_pdf_bytes = pdf_bytes
st.session_state.report_pdf_filename = filename
# Quick preview
st.markdown('Report Preview
',
unsafe_allow_html=True)
pc = st.columns(4)
pc[0].metric("Total Energy", f"{tot_e:.6f} kWh")
pc[1].metric("Total Bill", f"₨ {tot_b:.4f}")
pc[2].metric("CO₂", f"{tot_co2:.6f} kg")
pc[3].metric("Peak Power", f"{max_p:.1f} W")
pm = st.columns(3)
pm[0].metric("Monthly Energy (Est.)", f"{monthly_energy_est:.3f} kWh")
pm[1].metric("Monthly Cost (Est.)", f"₨ {monthly_cost_est:.2f}")
pm[2].metric("Monthly CO₂ (Est.)", f"{monthly_co2_est:.3f} kg")
except ImportError:
st.error("⚠️ `fpdf2` is not installed. Run: **pip install fpdf2**")
except Exception as ex:
st.error(f"Error: {ex}")
st.exception(ex)
if st.session_state.report_pdf_bytes:
st.download_button(
"⬇️ Download PDF Report",
data=st.session_state.report_pdf_bytes,
file_name=st.session_state.report_pdf_filename,
mime="application/pdf",
type="primary",
use_container_width=True
)
render_footer()
# ── Auto refresh (live updates) ──────────────────────────────
if st.session_state.demo_mode or st.session_state.connected:
time.sleep(1)
st.rerun()