# ============================================================ # 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"""
{val}
{lbl}
""", 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()