CoderHassan commited on
Commit
3beee0d
·
verified ·
1 Parent(s): 97674f5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +551 -319
app.py CHANGED
@@ -1,8 +1,7 @@
1
  # ============================================================
2
  # ENERGYGURU – POWER CALCULUS
3
- # Streamlit Dashboard | app.py
4
- # Hugging Face Spaces compatible
5
- # Run locally : streamlit run app.py
6
  # ============================================================
7
 
8
  import streamlit as st
@@ -10,20 +9,14 @@ import pandas as pd
10
  import numpy as np
11
  import plotly.graph_objects as go
12
  import plotly.express as px
 
 
13
  import time
14
  import math
15
  import random
16
  from datetime import datetime, timedelta
17
 
18
- # Optional serial (not available on HF Spaces cloud)
19
- try:
20
- import serial
21
- import serial.tools.list_ports
22
- SERIAL_AVAILABLE = True
23
- except ImportError:
24
- SERIAL_AVAILABLE = False
25
-
26
- # ── Page config ──────────────────────────────────────────────
27
  st.set_page_config(
28
  page_title="EnergyGuru – Power Calculus",
29
  page_icon="⚡",
@@ -33,71 +26,48 @@ st.set_page_config(
33
 
34
  # ── Custom CSS ───────────────────────────────────────────────
35
  st.markdown("""
36
- <style>
37
- @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow:wght@400;600;700&display=swap');
38
-
39
- html, body, [class*="css"] {
40
- font-family: 'Barlow', sans-serif;
41
- background-color: #080c14;
42
- }
43
-
44
- #MainMenu, footer, header { visibility: hidden; }
45
-
46
- .eg-card {
47
- background: linear-gradient(145deg, #0d1422, #111827);
48
- border: 1px solid #1e3a52;
49
- border-radius: 10px;
50
- padding: 16px 14px 12px 14px;
51
- text-align: center;
52
- position: relative;
53
- overflow: hidden;
54
- }
55
- .eg-card::before {
56
- content: '';
57
- position: absolute;
58
- top: 0; left: 0; right: 0;
59
- height: 2px;
60
- background: var(--accent);
61
- }
62
- .eg-value {
63
- font-family: 'Share Tech Mono', monospace;
64
- font-size: 1.55rem;
65
- font-weight: bold;
66
- color: var(--accent);
67
- letter-spacing: 1px;
68
- }
69
- .eg-label {
70
- font-size: 0.72rem;
71
- color: #6b7a90;
72
- margin-top: 3px;
73
- text-transform: uppercase;
74
- letter-spacing: 1.5px;
75
- }
76
- .eg-section {
77
- font-family: 'Share Tech Mono', monospace;
78
- color: #00c8ff;
79
- font-size: 0.78rem;
80
- letter-spacing: 3px;
81
- text-transform: uppercase;
82
- border-left: 3px solid #00c8ff;
83
- padding-left: 10px;
84
- margin: 18px 0 10px 0;
85
- }
86
- .status-live { color: #00ff99; font-size: 0.75rem; }
87
- .status-demo { color: #ffcc00; font-size: 0.75rem; }
88
- .status-off { color: #ff4466; font-size: 0.75rem; }
89
-
90
- section[data-testid="stSidebar"] {
91
- background: #080c14;
92
- border-right: 1px solid #1a2840;
93
- }
94
- </style>
95
  """, unsafe_allow_html=True)
96
 
97
- # ── Is this running on Hugging Face Spaces? ──────────────────
98
- import os
99
- ON_HF = os.environ.get("SPACE_ID") is not None
100
-
101
  # ── Constants ────────────────────────────────────────────────
102
  CITY_LOCATIONS = {
103
  "Rawalpindi City Model": {"lat": 33.6007, "lon": 73.0679, "desc": "Punjab, Pakistan"},
@@ -107,6 +77,15 @@ CITY_LOCATIONS = {
107
  "Custom Location": {"lat": 33.6007, "lon": 73.0679, "desc": "User-defined"},
108
  }
109
 
 
 
 
 
 
 
 
 
 
110
  # ── Session State Init ───────────────────────────────────────
111
  COLS = ['timestamp', 'voltage', 'current', 'power',
112
  'energy_kwh', 'bill_pkr', 'carbon_kg', 'runtime_hrs']
@@ -117,8 +96,12 @@ def _init_state():
117
  'connected': False,
118
  'serial_conn': None,
119
  'demo_mode': True,
 
 
120
  'demo_energy': 0.0,
121
  'demo_tick': 0,
 
 
122
  'latest': {k: 0.0 for k in
123
  ['voltage','current','power','energy_kwh','bill_pkr','carbon_kg','runtime_hrs']},
124
  }
@@ -141,39 +124,31 @@ with st.sidebar:
141
  """, unsafe_allow_html=True)
142
 
143
  st.markdown('<div class="eg-section">Connection</div>', unsafe_allow_html=True)
144
-
145
- if ON_HF:
146
- st.info("Running on Hugging Face Spaces — demo mode only. To use real Arduino hardware, run this app locally.", icon="ℹ️")
147
- st.session_state.demo_mode = True
148
- demo_mode = True
149
- else:
150
- demo_mode = st.toggle("🎮 Demo Mode (No Hardware)", value=st.session_state.demo_mode)
151
- st.session_state.demo_mode = demo_mode
152
-
153
- if not demo_mode and not ON_HF:
154
- if SERIAL_AVAILABLE:
155
- ports = [p.device for p in serial.tools.list_ports.comports()]
156
- sel_port = st.selectbox("Serial Port", ports if ports else ["No ports found"])
157
- baud = st.selectbox("Baud Rate", [9600, 115200], index=0)
158
- c1, c2 = st.columns(2)
159
- with c1:
160
- if st.button("▶ Connect", use_container_width=True):
161
- try:
162
- st.session_state.serial_conn = serial.Serial(sel_port, baud, timeout=1)
163
- st.session_state.connected = True
164
- st.success("Connected!")
165
- except Exception as e:
166
- st.error(str(e))
167
- with c2:
168
- if st.button("■ Disconnect", use_container_width=True):
169
- if st.session_state.serial_conn:
170
- try: st.session_state.serial_conn.close()
171
- except: pass
172
- st.session_state.connected = False
173
- st.session_state.serial_conn = None
174
- else:
175
- st.warning("pyserial not installed. Run: pip install pyserial")
176
-
177
  if demo_mode:
178
  st.markdown('<p class="status-demo">◉ DEMO MODE ACTIVE</p>', unsafe_allow_html=True)
179
  elif st.session_state.connected:
@@ -181,6 +156,28 @@ with st.sidebar:
181
  else:
182
  st.markdown('<p class="status-off">◉ DISCONNECTED</p>', unsafe_allow_html=True)
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  st.markdown('<div class="eg-section">Settings</div>', unsafe_allow_html=True)
185
  rate = st.number_input("💰 Tariff (PKR / kWh)", 1.0, 500.0, 50.0, 1.0)
186
  carbon = st.number_input("🌱 Carbon Factor (kg CO₂ / kWh)", 0.1, 3.0, 0.82, 0.01)
@@ -199,41 +196,45 @@ with st.sidebar:
199
  st.session_state.data_log = pd.DataFrame(columns=COLS)
200
  st.session_state.demo_energy = 0.0
201
  st.session_state.demo_tick = 0
 
202
  st.success("Cleared!")
203
 
 
204
  n = len(st.session_state.data_log)
205
  st.markdown(f'<div style="color:#4a6080;font-size:0.72rem;margin-top:8px;">Buffer: {n}/500 readings</div>',
206
  unsafe_allow_html=True)
207
 
208
- if ON_HF:
209
- st.markdown("""
210
- <div style="margin-top:16px;padding:8px;background:#0d1422;border-radius:6px;
211
- font-size:0.68rem;color:#4a6080;line-height:1.7;">
212
- <strong style="color:#00c8ff;">Local hardware setup</strong><br>
213
- Arduino Uno + ACS712 + voltage divider.<br>
214
- Clone repo and run locally to connect real sensors.
215
- </div>
216
- """, unsafe_allow_html=True)
217
-
218
  # ── Data Functions ───────────────────────────────────────────
219
  def _demo_reading():
220
- t = st.session_state.demo_tick
 
221
  st.session_state.demo_tick += 1
222
- v = 220 + 5 * math.sin(t * 0.07) + random.uniform(-2, 2)
223
- i = 1.8 + 0.6 * math.sin(t * 0.04) + 0.2 * math.sin(t * 0.13) + random.uniform(-0.05, 0.05)
 
 
 
 
 
 
224
  i = max(0.1, i)
225
  p = v * i
 
226
  dt_h = 1 / 3600
227
  st.session_state.demo_energy += (p / 1000) * dt_h
 
228
  e = st.session_state.demo_energy
229
  b = e * rate
230
  co2 = e * carbon
231
  rth = t / 3600
232
- return dict(voltage=round(v,2), current=round(i,3), power=round(p,2),
233
- energy_kwh=round(e,6), bill_pkr=round(b,4),
234
- carbon_kg=round(co2,6), runtime_hrs=round(rth,5))
 
 
235
 
236
  def _arduino_reading():
 
237
  conn = st.session_state.serial_conn
238
  if not conn or not st.session_state.connected:
239
  return None
@@ -242,31 +243,71 @@ def _arduino_reading():
242
  if ',' in raw:
243
  p = raw.split(',')
244
  if len(p) == 7:
245
- return dict(voltage=float(p[0]), current=float(p[1]),
246
- power=float(p[2]), energy_kwh=float(p[3]),
247
- bill_pkr=float(p[4]), carbon_kg=float(p[5]),
248
  runtime_hrs=float(p[6]))
249
  except Exception:
250
  pass
251
  return None
252
 
253
  def _log(data):
 
254
  row = pd.DataFrame([{"timestamp": datetime.now(), **data}])
255
  st.session_state.data_log = pd.concat(
256
  [st.session_state.data_log, row], ignore_index=True
257
  ).tail(500)
258
  st.session_state.latest = data
259
 
260
- if st.session_state.demo_mode:
261
- _log(_demo_reading())
262
- elif st.session_state.connected:
263
- d = _arduino_reading()
264
- if d:
265
- _log(d)
 
 
 
 
 
 
 
266
 
267
  latest = st.session_state.latest
268
  df = st.session_state.data_log.copy()
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  # ── Header ───────────────────────────────────────────────────
271
  st.markdown("""
272
  <div style="display:flex; align-items:baseline; gap:12px; padding:6px 0 4px 0;">
@@ -280,36 +321,48 @@ st.markdown("""
280
  <hr style="border-color:#1a2840; margin:6px 0 14px 0;">
281
  """, unsafe_allow_html=True)
282
 
 
283
  tab1, tab2, tab3, tab4 = st.tabs([
284
- "⚡ Live Dashboard", "🗺️ City Map", "📈 Analytics", "📄 Report",
 
 
 
285
  ])
286
 
287
  # ════════════════════════════════════════════════════════════
288
  # TAB 1 – LIVE DASHBOARD
289
  # ════════════════════════════════════════════════════════════
290
  with tab1:
291
- mc = st.columns(6)
 
 
292
  cards = [
293
- ("⚡ VOLTAGE", f"{latest['voltage']:.1f} V", "#00c8ff"),
294
- ("🔌 CURRENT", f"{latest['current']:.3f} A", "#ff9500"),
295
- ("💡 POWER", f"{latest['power']:.1f} W", "#ff4466"),
296
  ("🔋 ENERGY", f"{latest['energy_kwh']:.5f} kWh", "#00ff99"),
297
- ("💰 BILL", f"Rs {latest['bill_pkr']:.3f}", "#ffd700"),
298
- ("🌱 CO2", f"{latest['carbon_kg']:.5f} kg", "#88ff00"),
299
  ]
300
- for col, (lbl, val, clr) in zip(mc, cards):
301
- with col:
302
- st.markdown(f"""
303
- <div class="eg-card" style="--accent:{clr}">
304
- <div class="eg-value" style="color:{clr}">{val}</div>
305
- <div class="eg-label">{lbl}</div>
306
- </div>""", unsafe_allow_html=True)
 
 
 
 
307
 
308
  st.markdown("<br>", unsafe_allow_html=True)
309
 
 
310
  def gauge(value, title, max_v, color, unit, threshold=0.85):
311
  fig = go.Figure(go.Indicator(
312
- mode="gauge+number", value=value,
 
313
  title={'text': title, 'font': {'color': '#8a9ab0', 'size': 12,
314
  'family': 'Share Tech Mono'}},
315
  number={'suffix': f' {unit}', 'font': {'color': color, 'size': 20,
@@ -318,11 +371,12 @@ with tab1:
318
  'axis': {'range': [0, max_v], 'tickcolor': '#2a3a50',
319
  'tickfont': {'size': 9, 'color': '#4a6080'}},
320
  'bar': {'color': color, 'thickness': 0.25},
321
- 'bgcolor': '#0d1422', 'bordercolor': '#1a2840', 'borderwidth': 1,
 
322
  'steps': [
323
- {'range': [0, max_v*0.5], 'color': '#0d1422'},
324
- {'range': [max_v*0.5, max_v*threshold], 'color': '#111d2e'},
325
- {'range': [max_v*threshold, max_v], 'color': '#1a1020'},
326
  ],
327
  'threshold': {'line': {'color': '#ff4466', 'width': 2},
328
  'thickness': 0.75, 'value': max_v * threshold}
@@ -332,213 +386,291 @@ with tab1:
332
  height=200, margin=dict(l=15,r=15,t=40,b=5))
333
  return fig
334
 
335
- g1, g2, g3 = st.columns(3)
336
- with g1: st.plotly_chart(gauge(latest['voltage'], "VOLTAGE (V)", 260, "#00c8ff", "V"), use_container_width=True)
337
- with g2: st.plotly_chart(gauge(latest['current'], "CURRENT (A)", 5, "#ff9500", "A"), use_container_width=True)
338
- with g3: st.plotly_chart(gauge(latest['power'], "POWER (W)", 1100,"#ff4466", "W"), use_container_width=True)
 
 
 
 
 
339
 
 
340
  if len(df) > 1:
341
  rc1, rc2 = st.columns(2)
 
342
  with rc1:
343
  st.markdown('<div class="eg-section">Voltage & Current — Live</div>', unsafe_allow_html=True)
344
  fig_vc = go.Figure()
345
- fig_vc.add_trace(go.Scatter(x=df['timestamp'], y=df['voltage'],
346
- name='Voltage (V)', line=dict(color='#00c8ff', width=1.8), yaxis='y1'))
347
- fig_vc.add_trace(go.Scatter(x=df['timestamp'], y=df['current'],
348
- name='Current (A)', line=dict(color='#ff9500', width=1.8), yaxis='y2'))
 
 
 
 
349
  fig_vc.update_layout(
350
- paper_bgcolor='#080c14', plot_bgcolor='#0d1422', font_color='white', height=260,
 
351
  yaxis=dict(title='V', color='#00c8ff', gridcolor='#0d1e2e'),
352
- yaxis2=dict(title='A', overlaying='y', side='right', color='#ff9500'),
 
353
  legend=dict(bgcolor='#0d1422', font=dict(size=10)),
354
- margin=dict(l=8,r=8,t=8,b=8))
 
355
  st.plotly_chart(fig_vc, use_container_width=True)
 
356
  with rc2:
357
  st.markdown('<div class="eg-section">Power — Live</div>', unsafe_allow_html=True)
358
  fig_pw = go.Figure()
359
- fig_pw.add_trace(go.Scatter(x=df['timestamp'], y=df['power'],
 
360
  fill='tozeroy', name='Power (W)',
361
  line=dict(color='#ff4466', width=1.8),
362
- fillcolor='rgba(255,68,102,0.15)'))
 
363
  fig_pw.update_layout(
364
- paper_bgcolor='#080c14', plot_bgcolor='#0d1422', font_color='white', height=260,
 
365
  yaxis=dict(title='Watts', gridcolor='#0d1e2e'),
366
- margin=dict(l=8,r=8,t=8,b=8))
 
367
  st.plotly_chart(fig_pw, use_container_width=True)
368
  else:
369
- st.info("Collecting readings charts appear after a few seconds.")
 
370
 
371
  # ════════════════════════════════════════════════════════════
372
  # TAB 2 – CITY MAP
373
  # ════════════════════════════════════════════════════════════
374
  with tab2:
375
  map_col, info_col = st.columns([3, 1])
 
376
  with map_col:
377
- st.markdown('<div class="eg-section">City Energy Monitor — Location</div>', unsafe_allow_html=True)
378
- ring_lats = [loc['lat'] + 0.012*math.cos(math.radians(i)) for i in range(361)]
379
- ring_lons = [loc['lon'] + 0.018*math.sin(math.radians(i)) for i in range(361)]
 
 
 
 
380
  fig_map = go.Figure()
381
- fig_map.add_trace(go.Scattermapbox(lat=ring_lats, lon=ring_lons,
382
- mode='lines', line=dict(color='rgba(0,200,255,0.4)', width=2),
383
- name='Monitor Zone', showlegend=False))
384
- fig_map.add_trace(go.Scattermapbox(lat=[loc['lat']], lon=[loc['lon']],
385
- mode='markers+text', marker=dict(size=18, color='#00c8ff'),
386
- text=[f" {city_choice}"], textposition='top right',
 
 
 
 
 
 
 
 
 
 
 
387
  textfont=dict(color='white', size=13, family='Share Tech Mono'),
388
- name=city_choice))
 
 
389
  fig_map.update_layout(
390
- mapbox=dict(style='open-street-map',
391
- center=dict(lat=loc['lat'], lon=loc['lon']), zoom=13),
392
- paper_bgcolor='#080c14', font_color='white',
393
- height=480, margin=dict(l=0,r=0,t=0,b=0), showlegend=False)
 
 
 
 
 
 
 
394
  st.plotly_chart(fig_map, use_container_width=True)
395
 
396
  with info_col:
397
  st.markdown('<div class="eg-section">Location</div>', unsafe_allow_html=True)
398
  st.markdown(f"""
399
- <div style="font-family:'Share Tech Mono',monospace;font-size:0.78rem;color:#8a9ab0;line-height:1.9;">
400
- <div style="color:#00c8ff;font-size:0.95rem;margin-bottom:4px;">{city_choice}</div>
401
- {loc['desc']}<br>LAT: {loc['lat']:.4f}°<br>LON: {loc['lon']:.4f}°
402
- </div>""", unsafe_allow_html=True)
 
 
 
 
 
403
  st.markdown('<div class="eg-section">Live Readings</div>', unsafe_allow_html=True)
404
- st.metric("Voltage", f"{latest['voltage']:.1f} V")
405
- st.metric("Current", f"{latest['current']:.3f} A")
406
- st.metric("Power", f"{latest['power']:.1f} W")
 
407
  st.markdown('<div class="eg-section">Totals</div>', unsafe_allow_html=True)
408
- st.metric("Energy", f"{latest['energy_kwh']:.5f} kWh")
409
- st.metric("Bill", f"Rs {latest['bill_pkr']:.3f}")
410
- st.metric("CO2", f"{latest['carbon_kg']:.5f} kg")
 
 
411
  st.markdown('<div class="eg-section">Power Quality</div>', unsafe_allow_html=True)
412
  v = latest['voltage']
413
- if 210 <= v <= 240: st.success("Voltage Normal")
414
- elif 195 <= v < 210 or 240 < v <= 255: st.warning("Voltage Borderline")
415
- else: st.error("Voltage Abnormal")
 
 
 
 
416
 
417
  # ════════════════════════════════════════════════════════════
418
  # TAB 3 – ANALYTICS
419
  # ════════════════════════════════════════════════════════════
420
  with tab3:
421
  if len(df) < 5:
422
- st.info("Need at least 5 readings — collecting data...")
423
  else:
 
424
  st.markdown('<div class="eg-section">Summary Statistics</div>', unsafe_allow_html=True)
425
  s1,s2,s3,s4,s5 = st.columns(5)
426
- s1.metric("Avg Voltage", f"{df['voltage'].mean():.2f} V", f"s={df['voltage'].std():.2f}")
427
- s2.metric("Avg Current", f"{df['current'].mean():.3f} A", f"s={df['current'].std():.3f}")
428
  s3.metric("Avg Power", f"{df['power'].mean():.1f} W")
429
  s4.metric("Peak Power", f"{df['power'].max():.1f} W")
430
  s5.metric("Total Energy", f"{df['energy_kwh'].iloc[-1]:.5f} kWh")
431
 
432
  st.markdown('<div class="eg-section">Energy & Bill Trends</div>', unsafe_allow_html=True)
433
  ac1, ac2 = st.columns(2)
 
434
  with ac1:
435
  fig_e = px.area(df, x='timestamp', y='energy_kwh',
436
  color_discrete_sequence=['#00ff99'],
437
- labels={'energy_kwh':'kWh','timestamp':''})
438
  fig_e.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
439
- font_color='white', height=260, margin=dict(l=8,r=8,t=8,b=8))
 
440
  st.plotly_chart(fig_e, use_container_width=True)
 
441
  with ac2:
442
  fig_bc = go.Figure()
443
  fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['bill_pkr'],
444
- name='Bill (PKR)', yaxis='y1', line=dict(color='#ffd700', width=1.8)))
 
445
  fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['carbon_kg'],
446
- name='CO2 (kg)', yaxis='y2', line=dict(color='#88ff00', width=1.8)))
 
447
  fig_bc.update_layout(
448
- paper_bgcolor='#080c14', plot_bgcolor='#0d1422', font_color='white', height=260,
 
449
  yaxis=dict(title='PKR', color='#ffd700', gridcolor='#0d1e2e'),
450
- yaxis2=dict(title='kg CO2', overlaying='y', side='right', color='#88ff00'),
 
451
  legend=dict(bgcolor='#0d1422', font=dict(size=10)),
452
- margin=dict(l=8,r=8,t=8,b=8))
 
453
  st.plotly_chart(fig_bc, use_container_width=True)
454
 
 
455
  st.markdown('<div class="eg-section">Power Distribution</div>', unsafe_allow_html=True)
456
  fig_h = px.histogram(df, x='power', nbins=30,
457
  color_discrete_sequence=['#ff4466'],
458
- labels={'power':'Power (W)','count':'Frequency'})
459
  fig_h.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
460
- font_color='white', height=240, margin=dict(l=8,r=8,t=8,b=8))
 
461
  st.plotly_chart(fig_h, use_container_width=True)
462
 
 
463
  reg_df = df.copy()
464
  reg_df['energy_kwh'] = pd.to_numeric(reg_df['energy_kwh'], errors='coerce')
465
- reg_df['timestamp'] = pd.to_datetime(reg_df['timestamp'], errors='coerce')
466
  reg_df = reg_df.dropna(subset=['energy_kwh', 'timestamp']).reset_index(drop=True)
467
 
468
  if len(reg_df) >= 15:
469
  st.markdown('<div class="eg-section">AI Energy Prediction (Linear Regression)</div>',
470
  unsafe_allow_html=True)
471
- x = np.arange(len(reg_df), dtype=np.float64)
472
- y = pd.to_numeric(reg_df['energy_kwh'], errors='coerce').fillna(0).to_numpy(dtype=np.float64)
473
  coeffs = np.polyfit(x, y, 1)
474
- n_f = 60
475
- fx = np.arange(len(reg_df), len(reg_df)+n_f, dtype=float)
476
- fy = np.polyval(coeffs, fx)
477
- ft = [reg_df['timestamp'].iloc[-1]+timedelta(seconds=i) for i in range(1, n_f+1)]
 
 
478
  fig_pr = go.Figure()
479
  fig_pr.add_trace(go.Scatter(x=reg_df['timestamp'], y=reg_df['energy_kwh'],
480
- name='Actual', line=dict(color='#00ff99', width=2)))
481
  fig_pr.add_trace(go.Scatter(x=ft, y=fy,
482
- name='Predicted (60 s)', line=dict(color='#ffd700', width=1.8, dash='dot')))
 
483
  fig_pr.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
484
  font_color='white', height=260,
485
  yaxis=dict(title='kWh', gridcolor='#0d1e2e'),
486
  legend=dict(bgcolor='#0d1422'),
487
  margin=dict(l=8,r=8,t=8,b=8))
488
  st.plotly_chart(fig_pr, use_container_width=True)
489
- rate_s = coeffs[0]
490
- daily = rate_s * 86400
 
 
491
  monthly = daily * 30
492
- p1,p2,p3,p4 = st.columns(4)
493
- p1.metric("Rate", f"{rate_s*3600:.4f} kWh/hr")
494
- p2.metric("Daily Est.", f"{daily:.4f} kWh", f"Rs {daily*rate:.2f}")
495
- p3.metric("Monthly Est.", f"{monthly:.3f} kWh", f"Rs {monthly*rate:.2f}")
496
- p4.metric("Monthly CO2", f"{monthly*carbon:.3f} kg")
497
 
498
- st.markdown('<div class="eg-section">Data Log (last 50 readings)</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
499
  disp = df.tail(50).copy()
500
- disp['timestamp'] = pd.to_datetime(disp['timestamp'], errors='coerce').dt.strftime('%H:%M:%S').fillna('--:--:--')
 
501
  st.dataframe(disp, use_container_width=True, height=260)
 
502
  csv_bytes = df.to_csv(index=False).encode()
503
- st.download_button("Download CSV", csv_bytes,
504
  f"energyguru_{datetime.now():%Y%m%d_%H%M%S}.csv",
505
  "text/csv", use_container_width=True)
506
 
 
507
  # ════════════════════════════════════════════════════════════
508
- # TAB 4 – REPORT
509
  # ════════════════════════════════════════════════════════════
510
  with tab4:
511
  st.markdown('<div class="eg-section">Report Configuration</div>', unsafe_allow_html=True)
 
 
 
 
 
 
512
  rc1, rc2 = st.columns(2)
513
  with rc1:
514
- rpt_title = st.text_input("Report Title", "EnergyGuru – Power Calculus Report")
515
- institution = st.text_input("Institution", "Smart Energy Lab")
516
- operator = st.text_input("Operator", "")
517
- project_id = st.text_input("Project ID", "ENERGYGURU-2025-001")
518
  with rc2:
519
  notes = st.text_area("Notes / Remarks",
520
  "Generated by EnergyGuru Power Calculus System.\n"
521
  "Arduino-based IoT Energy Monitoring | City Model.")
522
 
523
- if st.button("Generate PDF Report", type="primary", use_container_width=True):
 
 
 
524
  if len(df) < 2:
525
- st.error("Not enough data — collect at least 2 readings first.")
526
  else:
527
  try:
528
- from fpdf import FPDF
529
-
530
- def pdf_safe(text):
531
- if text is None:
532
- return ""
533
- normalized = str(text).translate(str.maketrans({
534
- "\u2013": "-", "\u2014": "-",
535
- "\u2022": "|", "\u00b0": " deg",
536
- "\u2082": "2", "\u20a8": "Rs",
537
- "\u2713": "[+]", "\u26a0": "[!]",
538
- "\u2192": "->", "\u03c3": "s",
539
- }))
540
- return normalized.encode("latin-1", "replace").decode("latin-1")
541
 
 
542
  avg_v = df['voltage'].mean()
543
  avg_i = df['current'].mean()
544
  avg_p = df['power'].mean()
@@ -548,41 +680,74 @@ with tab4:
548
  tot_b = df['bill_pkr'].iloc[-1]
549
  tot_co2 = df['carbon_kg'].iloc[-1]
550
  n_reads = len(df)
 
 
 
 
 
551
 
 
552
  recs = []
553
  if avg_p > 500:
554
- recs.append("HIGH load detected consider switching off idle appliances.")
555
  if avg_v < 210 or avg_v > 235:
556
- recs.append("Voltage outside safe range (210-235V) check power supply.")
557
  if df['voltage'].std() > 8:
558
- recs.append("High voltage fluctuation consider a voltage stabiliser.")
559
  recs.append("Use LED lighting to reduce city model consumption by ~70%.")
560
  recs.append("Schedule high-load demos during off-peak hours (22:00-06:00).")
561
  recs.append("Install capacitor banks to improve power factor.")
562
  recs.append("Regular maintenance reduces standby losses significantly.")
563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  class EnergyPDF(FPDF):
565
  def header(self):
 
566
  self.set_fill_color(8, 12, 20)
567
  self.rect(0, 0, 210, 297, 'F')
 
568
  self.set_fill_color(0, 40, 60)
569
  self.rect(0, 0, 210, 22, 'F')
 
570
  self.set_fill_color(0, 200, 255)
571
  self.rect(0, 0, 4, 22, 'F')
 
572
  self.set_font('Helvetica', 'B', 14)
573
  self.set_text_color(0, 200, 255)
574
  self.set_xy(8, 4)
575
- self.cell(100, 7, pdf_safe('ENERGYGURU - POWER CALCULUS'))
 
576
  self.set_font('Helvetica', '', 7)
577
  self.set_text_color(80, 120, 160)
578
  self.set_xy(8, 13)
579
- self.cell(0, 5, pdf_safe(f'AI-Assisted Energy Usage Analyzer | Generated: {datetime.now():%Y-%m-%d %H:%M:%S}'))
 
 
580
  self.ln(14)
 
581
  def footer(self):
582
  self.set_y(-14)
583
  self.set_font('Helvetica', 'I', 7)
584
  self.set_text_color(50, 70, 100)
585
- self.cell(0, 8, pdf_safe(f'EnergyGuru Power Calculus | {institution} | Page {self.page_no()}'), align='C')
 
 
 
586
  def section_title(self, txt):
587
  self.set_fill_color(0, 30, 50)
588
  self.set_draw_color(0, 200, 255)
@@ -592,8 +757,12 @@ with tab4:
592
  self.set_text_color(0, 200, 255)
593
  self.cell(0, 8, pdf_safe(f' {txt}'), ln=True)
594
  self.ln(2)
 
595
  def kv_row(self, label, value, fill_idx):
596
- self.set_fill_color(13, 20, 34) if fill_idx%2==0 else self.set_fill_color(10, 16, 28)
 
 
 
597
  self.set_text_color(100, 140, 180)
598
  self.set_font('Helvetica', '', 9)
599
  self.cell(90, 7, pdf_safe(f' {label}'), fill=True)
@@ -605,113 +774,176 @@ with tab4:
605
  pdf.set_auto_page_break(auto=True, margin=18)
606
  pdf.add_page()
607
 
 
608
  pdf.set_font('Helvetica', 'B', 17)
609
  pdf.set_text_color(0, 200, 255)
610
  pdf.cell(0, 10, pdf_safe(rpt_title), ln=True, align='C')
611
  pdf.ln(1)
 
612
  pdf.set_font('Helvetica', '', 9)
613
  pdf.set_text_color(80, 120, 160)
614
- pdf.cell(0, 6, pdf_safe(f'Institution: {institution} | Project: {project_id}'), ln=True, align='C')
615
- pdf.cell(0, 6, pdf_safe(f'Location: {city_choice} - Lat {loc["lat"]:.4f} Lon {loc["lon"]:.4f}'), ln=True, align='C')
616
  if operator:
617
  pdf.cell(0, 6, pdf_safe(f'Operator: {operator}'), ln=True, align='C')
618
- pdf.cell(0, 6, pdf_safe(f'Date: {datetime.now():%B %d, %Y} Time: {datetime.now():%H:%M:%S}'), ln=True, align='C')
619
  pdf.ln(4)
 
 
620
  pdf.set_draw_color(0, 60, 90)
621
  pdf.set_line_width(0.4)
622
  pdf.line(15, pdf.get_y(), 195, pdf.get_y())
623
  pdf.ln(5)
624
 
 
625
  pdf.section_title('1. MEASUREMENT SUMMARY')
626
- for idx, (lbl, val) in enumerate([
627
- ("Average Voltage", f"{avg_v:.2f} V"),
628
- ("Average Current", f"{avg_i:.3f} A"),
629
- ("Average Power", f"{avg_p:.2f} W"),
630
- ("Peak Power", f"{max_p:.2f} W"),
631
- ("Minimum Power", f"{min_p:.2f} W"),
632
- ("Total Energy", f"{tot_e:.6f} kWh"),
633
- ("Electricity Bill", f"PKR {tot_b:.4f}"),
634
- ("Carbon Footprint", f"{tot_co2:.6f} kg CO2"),
635
- ("Tariff Rate", f"PKR {rate:.2f} / kWh"),
636
- ("Carbon Factor", f"{carbon:.2f} kg CO2 / kWh"),
637
- ("Total Readings", f"{n_reads}"),
638
- ]):
 
 
 
 
 
639
  pdf.kv_row(lbl, val, idx)
640
  pdf.ln(5)
641
 
 
642
  pdf.section_title('2. ARDUINO CALCULATIONS')
643
  pdf.set_fill_color(8, 16, 26)
644
  pdf.set_font('Courier', '', 8)
645
  pdf.set_text_color(0, 220, 120)
646
- for ln_text in [
 
 
 
 
 
 
 
 
 
 
 
 
647
  '',
648
- f' power_W = voltage_V * current_A = {avg_v:.2f} * {avg_i:.3f} = {avg_p:.2f} W',
649
- f' energy_kWh += (power_W / 1000.0) * dt_hours --> {tot_e:.6f} kWh',
650
- f' bill_PKR = energy_kWh * tariff = {tot_e:.6f} * {rate:.2f} = PKR {tot_b:.4f}',
651
- f' carbon_kg = energy_kWh * carbon_factor = {tot_e:.6f} * {carbon:.2f} = {tot_co2:.6f} kg',
652
- f' apparent_VA = {avg_v:.2f} * {avg_i:.3f} = {avg_v*avg_i:.2f} VA',
653
  '',
654
- ]:
655
- pdf.cell(0, 6, pdf_safe(ln_text), fill=True, ln=True)
 
 
 
 
656
  pdf.ln(4)
657
 
658
- pdf.section_title('3. RECENT READINGS (last 20)')
659
- hdrs = ['Time','V (V)','I (A)','P (W)','kWh','Bill Rs','CO2 kg']
660
- cw = [26,22,22,25,32,30,28]
661
- pdf.set_fill_color(0,40,60)
662
- pdf.set_text_color(0,200,255)
663
- pdf.set_font('Helvetica','B',8)
664
- for h,w in zip(hdrs,cw): pdf.cell(w,7,h,fill=True,align='C')
 
 
 
 
 
665
  pdf.ln()
666
- pdf.set_font('Helvetica','',8)
667
- for idx,(_, row) in enumerate(df.tail(20).iterrows()):
668
- pdf.set_fill_color(*(13,20,34) if idx%2==0 else (10,16,28))
669
- pdf.set_text_color(180,200,220)
670
- ts = row['timestamp'].strftime('%H:%M:%S') if hasattr(row['timestamp'],'strftime') else str(row['timestamp'])[:8]
671
- for v,w in zip([ts,f"{row['voltage']:.1f}",f"{row['current']:.3f}",
672
- f"{row['power']:.1f}",f"{row['energy_kwh']:.6f}",
673
- f"{row['bill_pkr']:.4f}",f"{row['carbon_kg']:.6f}"],cw):
674
- pdf.cell(w,6,pdf_safe(v),fill=True,align='C')
 
 
 
 
 
 
 
 
675
  pdf.ln()
676
  pdf.ln(4)
677
 
 
678
  pdf.section_title('4. AI ENERGY RECOMMENDATIONS')
679
- pdf.set_font('Helvetica','',9)
680
- for rec in recs:
681
- ok = not (rec.startswith('HIGH') or rec.startswith('Voltage') or rec.startswith('High'))
682
- pdf.set_text_color(*(100,220,130) if ok else (255,180,60))
683
- pdf.cell(0,8,pdf_safe(f' {"+" if ok else "!"} {rec}'),ln=True)
684
-
 
 
 
685
  if notes.strip():
686
- pdf.set_draw_color(0,60,90)
687
- pdf.line(15,pdf.get_y(),195,pdf.get_y())
688
  pdf.ln(3)
689
- pdf.set_font('Helvetica','B',9)
690
- pdf.set_text_color(60,100,140)
691
- pdf.cell(0,7,'NOTES:',ln=True)
692
- pdf.set_font('Helvetica','',8)
693
- pdf.set_text_color(140,160,180)
694
  for ln_text in notes.split('\n'):
695
- pdf.cell(0,6,pdf_safe(ln_text),ln=True)
696
 
 
697
  pdf_bytes = bytes(pdf.output())
698
- st.success("Report generated!")
699
- st.download_button("Download PDF Report", pdf_bytes,
700
- f"EnergyGuru_Report_{datetime.now():%Y%m%d_%H%M%S}.pdf",
701
- "application/pdf", type="primary", use_container_width=True)
702
-
 
 
 
703
  pc = st.columns(4)
704
- pc[0].metric("Total Energy", f"{tot_e:.6f} kWh")
705
- pc[1].metric("Total Bill", f"Rs {tot_b:.4f}")
706
- pc[2].metric("CO2", f"{tot_co2:.6f} kg")
707
- pc[3].metric("Peak Power", f"{max_p:.1f} W")
 
 
 
 
708
 
709
  except ImportError:
710
- st.error("fpdf2 not installed. Run: pip install fpdf2")
711
  except Exception as ex:
712
  st.error(f"Error: {ex}")
 
 
 
 
 
 
 
 
 
 
 
 
 
713
 
714
- # ── Auto refresh ─────────────────────────────────────────────
715
  if st.session_state.demo_mode or st.session_state.connected:
716
  time.sleep(1)
717
- st.rerun()
 
1
  # ============================================================
2
  # ENERGYGURU – POWER CALCULUS
3
+ # Streamlit Dashboard | dashboard.py
4
+ # Run: streamlit run dashboard.py
 
5
  # ============================================================
6
 
7
  import streamlit as st
 
9
  import numpy as np
10
  import plotly.graph_objects as go
11
  import plotly.express as px
12
+ import serial
13
+ import serial.tools.list_ports
14
  import time
15
  import math
16
  import random
17
  from datetime import datetime, timedelta
18
 
19
+ # ── Page config (MUST be first Streamlit call) ───────────────
 
 
 
 
 
 
 
 
20
  st.set_page_config(
21
  page_title="EnergyGuru – Power Calculus",
22
  page_icon="⚡",
 
26
 
27
  # ── Custom CSS ───────────────────────────────────────────────
28
  st.markdown("""
29
+ <style>
30
+ @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow:wght@400;600;700&display=swap');
31
+ html, body, [class*="css"] { font-family: 'Barlow', sans-serif; background-color: #080c14; color: #ffffff; }
32
+ #MainMenu, footer { visibility: hidden; }
33
+ .eg-card { background: linear-gradient(145deg, #0d1422, #111827); border: 1px solid #1e3a52; border-radius: 10px; padding: 16px 14px 12px 14px; text-align: center; position: relative; overflow: hidden; }
34
+ .eg-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--accent); }
35
+ .eg-value { font-family: 'Share Tech Mono', monospace; font-size: 1.55rem; font-weight: bold; color: var(--accent); letter-spacing: 1px; }
36
+ .eg-label { font-size: 0.72rem; color: #6b7a90; margin-top: 3px; text-transform: uppercase; letter-spacing: 1.5px; }
37
+ .eg-section { font-family: 'Share Tech Mono', monospace; color: #00c8ff; font-size: 0.78rem; letter-spacing: 3px; text-transform: uppercase; border-left: 3px solid #00c8ff; padding-left: 10px; margin: 18px 0 10px 0; }
38
+ .status-live { color: #00ff99; font-size: 0.75rem; }
39
+ .status-demo { color: #ffcc00; font-size: 0.75rem; }
40
+ .status-off { color: #ff4466; font-size: 0.75rem; }
41
+ section[data-testid="stSidebar"] { background: #080c14; border-right: 1px solid #1a2840; }
42
+ .eg-footer { margin-top: 18px; padding: 10px 0 2px 0; border-top: 1px solid #1a2840; text-align: center; }
43
+ .eg-footer-title {
44
+ font-family:'Share Tech Mono',monospace; font-size: 1.05rem; color: #00c8ff;
45
+ letter-spacing: 2px; text-transform: uppercase;
46
+ text-shadow: 0 0 6px rgba(0,200,255,0.75), 0 0 14px rgba(0,200,255,0.45);
47
+ animation: egGlowTitle 1.8s ease-in-out infinite alternate;
48
+ }
49
+ .eg-footer-text {
50
+ margin-top: 5px; font-family:'Share Tech Mono',monospace; font-size: 0.8rem;
51
+ color: #9ec3df; letter-spacing: 1px; text-transform: uppercase;
52
+ text-shadow: 0 0 6px rgba(120,200,255,0.65), 0 0 12px rgba(120,200,255,0.35);
53
+ animation: egGlowText 2.2s ease-in-out infinite alternate;
54
+ }
55
+ @keyframes egGlowTitle {
56
+ from { text-shadow: 0 0 4px rgba(0,200,255,0.55), 0 0 10px rgba(0,200,255,0.30); }
57
+ to { text-shadow: 0 0 9px rgba(0,200,255,0.95), 0 0 18px rgba(0,200,255,0.55); }
58
+ }
59
+ @keyframes egGlowText {
60
+ from { text-shadow: 0 0 4px rgba(120,200,255,0.45), 0 0 9px rgba(120,200,255,0.20); }
61
+ to { text-shadow: 0 0 8px rgba(120,200,255,0.85), 0 0 16px rgba(120,200,255,0.40); }
62
+ }
63
+ @media (max-width: 768px) {
64
+ .eg-value { font-size: 1.2rem; }
65
+ .eg-label { font-size: 0.66rem; letter-spacing: 1px; }
66
+ .eg-section { font-size: 0.7rem; letter-spacing: 2px; }
67
+ }
68
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  """, unsafe_allow_html=True)
70
 
 
 
 
 
71
  # ── Constants ────────────────────────────────────────────────
72
  CITY_LOCATIONS = {
73
  "Rawalpindi City Model": {"lat": 33.6007, "lon": 73.0679, "desc": "Punjab, Pakistan"},
 
77
  "Custom Location": {"lat": 33.6007, "lon": 73.0679, "desc": "User-defined"},
78
  }
79
 
80
+ ACCENT_COLORS = {
81
+ "voltage": "#00c8ff",
82
+ "current": "#ff9500",
83
+ "power": "#ff4466",
84
+ "energy": "#00ff99",
85
+ "bill": "#ffd700",
86
+ "carbon": "#88ff00",
87
+ }
88
+
89
  # ── Session State Init ───────────────────────────────────────
90
  COLS = ['timestamp', 'voltage', 'current', 'power',
91
  'energy_kwh', 'bill_pkr', 'carbon_kg', 'runtime_hrs']
 
96
  'connected': False,
97
  'serial_conn': None,
98
  'demo_mode': True,
99
+ 'demo_v_base': 220.0,
100
+ 'demo_i_base': 1.8,
101
  'demo_energy': 0.0,
102
  'demo_tick': 0,
103
+ 'last_sample_ts': 0.0,
104
+ 'sample_interval_s': 1.0,
105
  'latest': {k: 0.0 for k in
106
  ['voltage','current','power','energy_kwh','bill_pkr','carbon_kg','runtime_hrs']},
107
  }
 
124
  """, unsafe_allow_html=True)
125
 
126
  st.markdown('<div class="eg-section">Connection</div>', unsafe_allow_html=True)
127
+ demo_mode = st.toggle("🎮 Demo Mode (No Hardware)", value=st.session_state.demo_mode)
128
+ st.session_state.demo_mode = demo_mode
129
+
130
+ if not demo_mode:
131
+ ports = [p.device for p in serial.tools.list_ports.comports()]
132
+ sel_port = st.selectbox("Serial Port", ports if ports else ["No ports found"])
133
+ baud = st.selectbox("Baud Rate", [9600, 115200], index=0)
134
+ c1, c2 = st.columns(2)
135
+ with c1:
136
+ if st.button("▶ Connect", use_container_width=True):
137
+ try:
138
+ st.session_state.serial_conn = serial.Serial(sel_port, baud, timeout=1)
139
+ st.session_state.connected = True
140
+ st.success("Connected!")
141
+ except Exception as e:
142
+ st.error(str(e))
143
+ with c2:
144
+ if st.button("■ Disconnect", use_container_width=True):
145
+ if st.session_state.serial_conn:
146
+ try: st.session_state.serial_conn.close()
147
+ except: pass
148
+ st.session_state.connected = False
149
+ st.session_state.serial_conn = None
150
+
151
+ # Status indicator
 
 
 
 
 
 
 
 
152
  if demo_mode:
153
  st.markdown('<p class="status-demo">◉ DEMO MODE ACTIVE</p>', unsafe_allow_html=True)
154
  elif st.session_state.connected:
 
156
  else:
157
  st.markdown('<p class="status-off">◉ DISCONNECTED</p>', unsafe_allow_html=True)
158
 
159
+ if demo_mode:
160
+ st.markdown('<div class="eg-section">Demo Controls</div>', unsafe_allow_html=True)
161
+ st.session_state.demo_v_base = st.slider(
162
+ "Demo Voltage (V)",
163
+ min_value=180.0,
164
+ max_value=260.0,
165
+ value=float(st.session_state.demo_v_base),
166
+ step=0.5,
167
+ )
168
+ st.session_state.demo_i_base = st.slider(
169
+ "Demo Current (A)",
170
+ min_value=0.1,
171
+ max_value=5.0,
172
+ value=float(st.session_state.demo_i_base),
173
+ step=0.05,
174
+ )
175
+ if st.button("↺ Reset Demo Profile", use_container_width=True):
176
+ # Revert to the original synthetic profile baseline.
177
+ st.session_state.demo_v_base = 220.0
178
+ st.session_state.demo_i_base = 1.8
179
+ st.success("Demo profile reset to default dummy data behavior.")
180
+
181
  st.markdown('<div class="eg-section">Settings</div>', unsafe_allow_html=True)
182
  rate = st.number_input("💰 Tariff (PKR / kWh)", 1.0, 500.0, 50.0, 1.0)
183
  carbon = st.number_input("🌱 Carbon Factor (kg CO₂ / kWh)", 0.1, 3.0, 0.82, 0.01)
 
196
  st.session_state.data_log = pd.DataFrame(columns=COLS)
197
  st.session_state.demo_energy = 0.0
198
  st.session_state.demo_tick = 0
199
+ st.session_state.last_sample_ts = 0.0
200
  st.success("Cleared!")
201
 
202
+ # Buffer info
203
  n = len(st.session_state.data_log)
204
  st.markdown(f'<div style="color:#4a6080;font-size:0.72rem;margin-top:8px;">Buffer: {n}/500 readings</div>',
205
  unsafe_allow_html=True)
206
 
 
 
 
 
 
 
 
 
 
 
207
  # ── Data Functions ───────────────────────────────────────────
208
  def _demo_reading():
209
+ """Simulate a realistic city-model reading."""
210
+ t = st.session_state.demo_tick
211
  st.session_state.demo_tick += 1
212
+
213
+ v_base = float(st.session_state.demo_v_base)
214
+ i_base = float(st.session_state.demo_i_base)
215
+
216
+ # AC voltage ~220 V with ±6 V fluctuation
217
+ v = v_base + 5 * math.sin(t * 0.07) + random.uniform(-2, 2)
218
+ # Load current varies 0.8–2.8 A (city model lamps + motors)
219
+ i = i_base + 0.6 * math.sin(t * 0.04) + 0.2 * math.sin(t * 0.13) + random.uniform(-0.05, 0.05)
220
  i = max(0.1, i)
221
  p = v * i
222
+
223
  dt_h = 1 / 3600
224
  st.session_state.demo_energy += (p / 1000) * dt_h
225
+
226
  e = st.session_state.demo_energy
227
  b = e * rate
228
  co2 = e * carbon
229
  rth = t / 3600
230
+
231
+ return dict(voltage=round(v,2), current=round(i,3),
232
+ power=round(p,2), energy_kwh=round(e,6),
233
+ bill_pkr=round(b,4), carbon_kg=round(co2,6),
234
+ runtime_hrs=round(rth,5))
235
 
236
  def _arduino_reading():
237
+ """Read one CSV line from Arduino serial."""
238
  conn = st.session_state.serial_conn
239
  if not conn or not st.session_state.connected:
240
  return None
 
243
  if ',' in raw:
244
  p = raw.split(',')
245
  if len(p) == 7:
246
+ return dict(voltage=float(p[0]), current=float(p[1]),
247
+ power=float(p[2]), energy_kwh=float(p[3]),
248
+ bill_pkr=float(p[4]), carbon_kg=float(p[5]),
249
  runtime_hrs=float(p[6]))
250
  except Exception:
251
  pass
252
  return None
253
 
254
  def _log(data):
255
+ """Append to session log; keep last 500 rows."""
256
  row = pd.DataFrame([{"timestamp": datetime.now(), **data}])
257
  st.session_state.data_log = pd.concat(
258
  [st.session_state.data_log, row], ignore_index=True
259
  ).tail(500)
260
  st.session_state.latest = data
261
 
262
+ # ── Fetch current reading ────────────────────────────────────
263
+ now_ts = time.time()
264
+ can_sample = (now_ts - float(st.session_state.last_sample_ts)) >= float(st.session_state.sample_interval_s)
265
+
266
+ if can_sample:
267
+ if st.session_state.demo_mode:
268
+ _log(_demo_reading())
269
+ st.session_state.last_sample_ts = now_ts
270
+ elif st.session_state.connected:
271
+ d = _arduino_reading()
272
+ if d:
273
+ _log(d)
274
+ st.session_state.last_sample_ts = now_ts
275
 
276
  latest = st.session_state.latest
277
  df = st.session_state.data_log.copy()
278
 
279
+ def render_footer():
280
+ st.markdown(
281
+ '<div class="eg-footer"><div class="eg-footer-title">ENERGYGURU</div>'
282
+ '<div class="eg-footer-text">A PRODUCT OF 7PSOLUTIONS: 7PS CAAD LABS</div></div>',
283
+ unsafe_allow_html=True
284
+ )
285
+
286
+ def render_device_alerts(latest_row):
287
+ if st.session_state.demo_mode:
288
+ st.info("🧪 Demo mode is active. Values are simulated unless connected to Arduino.")
289
+ return
290
+
291
+ if not st.session_state.connected:
292
+ st.error("🔴 Device status: Arduino is disconnected.")
293
+ return
294
+
295
+ v = float(latest_row.get("voltage", 0.0))
296
+ i = float(latest_row.get("current", 0.0))
297
+ p = float(latest_row.get("power", 0.0))
298
+
299
+ if v < 195 or v > 255:
300
+ st.error("🔴 Critical voltage detected. Check supply and wiring.")
301
+ elif v < 210 or v > 240:
302
+ st.warning("🟠 Voltage is borderline. Monitor stability.")
303
+ else:
304
+ st.success("🟢 Device status: connected and electrical values are stable.")
305
+
306
+ if i > 4.2:
307
+ st.warning("🟠 High current draw detected.")
308
+ if p > 1000:
309
+ st.warning("🟠 Power near upper expected limit.")
310
+
311
  # ── Header ───────────────────────────────────────────────────
312
  st.markdown("""
313
  <div style="display:flex; align-items:baseline; gap:12px; padding:6px 0 4px 0;">
 
321
  <hr style="border-color:#1a2840; margin:6px 0 14px 0;">
322
  """, unsafe_allow_html=True)
323
 
324
+ # ── Tabs ─────────────────────────────────────────────────────
325
  tab1, tab2, tab3, tab4 = st.tabs([
326
+ "⚡ Live Dashboard",
327
+ "🗺️ City Map",
328
+ "📈 Analytics",
329
+ "📄 Report",
330
  ])
331
 
332
  # ════════════════════════════════════════════════════════════
333
  # TAB 1 – LIVE DASHBOARD
334
  # ════════════════════════════════════════════════════════════
335
  with tab1:
336
+ render_device_alerts(latest)
337
+
338
+ # ── 6 metric cards ──────────────────────────────────────
339
  cards = [
340
+ ("⚡ VOLTAGE", f"{latest['voltage']:.1f} V", "#00c8ff"),
341
+ ("🔌 CURRENT", f"{latest['current']:.3f} A", "#ff9500"),
342
+ ("💡 POWER", f"{latest['power']:.1f} W", "#ff4466"),
343
  ("🔋 ENERGY", f"{latest['energy_kwh']:.5f} kWh", "#00ff99"),
344
+ ("💰 BILL", f" {latest['bill_pkr']:.3f}", "#ffd700"),
345
+ ("🌱 CO₂", f"{latest['carbon_kg']:.5f} kg", "#88ff00"),
346
  ]
347
+ cards_per_row = 6
348
+ for i in range(0, len(cards), cards_per_row):
349
+ row_cards = cards[i:i + cards_per_row]
350
+ cols = st.columns(len(row_cards))
351
+ for col, (lbl, val, clr) in zip(cols, row_cards):
352
+ with col:
353
+ st.markdown(f"""
354
+ <div class="eg-card" style="--accent:{clr}">
355
+ <div class="eg-value" style="color:{clr}">{val}</div>
356
+ <div class="eg-label">{lbl}</div>
357
+ </div>""", unsafe_allow_html=True)
358
 
359
  st.markdown("<br>", unsafe_allow_html=True)
360
 
361
+ # ── 3 gauges ────────────────────────────────────────────
362
  def gauge(value, title, max_v, color, unit, threshold=0.85):
363
  fig = go.Figure(go.Indicator(
364
+ mode="gauge+number",
365
+ value=value,
366
  title={'text': title, 'font': {'color': '#8a9ab0', 'size': 12,
367
  'family': 'Share Tech Mono'}},
368
  number={'suffix': f' {unit}', 'font': {'color': color, 'size': 20,
 
371
  'axis': {'range': [0, max_v], 'tickcolor': '#2a3a50',
372
  'tickfont': {'size': 9, 'color': '#4a6080'}},
373
  'bar': {'color': color, 'thickness': 0.25},
374
+ 'bgcolor': '#0d1422',
375
+ 'bordercolor': '#1a2840', 'borderwidth': 1,
376
  'steps': [
377
+ {'range': [0, max_v * 0.5], 'color': '#0d1422'},
378
+ {'range': [max_v * 0.5, max_v * threshold], 'color': '#111d2e'},
379
+ {'range': [max_v * threshold, max_v], 'color': '#1a1020'},
380
  ],
381
  'threshold': {'line': {'color': '#ff4466', 'width': 2},
382
  'thickness': 0.75, 'value': max_v * threshold}
 
386
  height=200, margin=dict(l=15,r=15,t=40,b=5))
387
  return fig
388
 
389
+ gauge_cols = st.columns(3)
390
+ gauge_specs = [
391
+ (latest['voltage'], "VOLTAGE (V)", 260, "#00c8ff", "V"),
392
+ (latest['current'], "CURRENT (A)", 5, "#ff9500", "A"),
393
+ (latest['power'], "POWER (W)", 1100, "#ff4466", "W"),
394
+ ]
395
+ for col, spec in zip(gauge_cols, gauge_specs):
396
+ with col:
397
+ st.plotly_chart(gauge(*spec), use_container_width=True)
398
 
399
+ # ── Real-time charts ────────────────────────────────────
400
  if len(df) > 1:
401
  rc1, rc2 = st.columns(2)
402
+
403
  with rc1:
404
  st.markdown('<div class="eg-section">Voltage & Current — Live</div>', unsafe_allow_html=True)
405
  fig_vc = go.Figure()
406
+ fig_vc.add_trace(go.Scatter(
407
+ x=df['timestamp'], y=df['voltage'],
408
+ name='Voltage (V)', line=dict(color='#00c8ff', width=1.8), yaxis='y1'
409
+ ))
410
+ fig_vc.add_trace(go.Scatter(
411
+ x=df['timestamp'], y=df['current'],
412
+ name='Current (A)', line=dict(color='#ff9500', width=1.8), yaxis='y2'
413
+ ))
414
  fig_vc.update_layout(
415
+ paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
416
+ font_color='white', height=260,
417
  yaxis=dict(title='V', color='#00c8ff', gridcolor='#0d1e2e'),
418
+ yaxis2=dict(title='A', overlaying='y', side='right',
419
+ color='#ff9500', gridcolor='#0d1e2e'),
420
  legend=dict(bgcolor='#0d1422', font=dict(size=10)),
421
+ margin=dict(l=8,r=8,t=8,b=8),
422
+ )
423
  st.plotly_chart(fig_vc, use_container_width=True)
424
+
425
  with rc2:
426
  st.markdown('<div class="eg-section">Power — Live</div>', unsafe_allow_html=True)
427
  fig_pw = go.Figure()
428
+ fig_pw.add_trace(go.Scatter(
429
+ x=df['timestamp'], y=df['power'],
430
  fill='tozeroy', name='Power (W)',
431
  line=dict(color='#ff4466', width=1.8),
432
+ fillcolor='rgba(255,68,102,0.15)'
433
+ ))
434
  fig_pw.update_layout(
435
+ paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
436
+ font_color='white', height=260,
437
  yaxis=dict(title='Watts', gridcolor='#0d1e2e'),
438
+ margin=dict(l=8,r=8,t=8,b=8),
439
+ )
440
  st.plotly_chart(fig_pw, use_container_width=True)
441
  else:
442
+ st.info("📡 Collecting readings Charts will appear after a few seconds.")
443
+
444
 
445
  # ════════════════════════════════════════════════════════════
446
  # TAB 2 – CITY MAP
447
  # ════════════════════════════════════════════════════════════
448
  with tab2:
449
  map_col, info_col = st.columns([3, 1])
450
+
451
  with map_col:
452
+ st.markdown('<div class="eg-section">City Energy Monitor — Location</div>',
453
+ unsafe_allow_html=True)
454
+
455
+ # Build coverage ring
456
+ ring_lats = [loc['lat'] + 0.012 * math.cos(math.radians(i)) for i in range(361)]
457
+ ring_lons = [loc['lon'] + 0.018 * math.sin(math.radians(i)) for i in range(361)]
458
+
459
  fig_map = go.Figure()
460
+
461
+ # Coverage ring
462
+ fig_map.add_trace(go.Scattermapbox(
463
+ lat=ring_lats, lon=ring_lons,
464
+ mode='lines',
465
+ line=dict(color='rgba(0,200,255,0.4)', width=2),
466
+ name='Monitor Zone', showlegend=False,
467
+ ))
468
+
469
+ # Main marker
470
+ fig_map.add_trace(go.Scattermapbox(
471
+ lat=[loc['lat']], lon=[loc['lon']],
472
+ mode='markers+text',
473
+ marker=dict(size=18, color='#00c8ff',
474
+ symbol='circle', opacity=0.9),
475
+ text=[f"⚡ {city_choice}"],
476
+ textposition='top right',
477
  textfont=dict(color='white', size=13, family='Share Tech Mono'),
478
+ name=city_choice,
479
+ ))
480
+
481
  fig_map.update_layout(
482
+ mapbox=dict(
483
+ style='open-street-map',
484
+ center=dict(lat=loc['lat'], lon=loc['lon']),
485
+ zoom=13,
486
+ ),
487
+ paper_bgcolor='#080c14',
488
+ font_color='white',
489
+ height=480,
490
+ margin=dict(l=0, r=0, t=0, b=0),
491
+ showlegend=False,
492
+ )
493
  st.plotly_chart(fig_map, use_container_width=True)
494
 
495
  with info_col:
496
  st.markdown('<div class="eg-section">Location</div>', unsafe_allow_html=True)
497
  st.markdown(f"""
498
+ <div style="font-family:'Share Tech Mono',monospace; font-size:0.78rem;
499
+ color:#8a9ab0; line-height:1.9;">
500
+ <div style="color:#00c8ff; font-size:0.95rem; margin-bottom:4px;">{city_choice}</div>
501
+ {loc['desc']}<br>
502
+ LAT: {loc['lat']:.4f}°<br>
503
+ LON: {loc['lon']:.4f}°
504
+ </div>
505
+ """, unsafe_allow_html=True)
506
+
507
  st.markdown('<div class="eg-section">Live Readings</div>', unsafe_allow_html=True)
508
+ st.metric("Voltage", f"{latest['voltage']:.1f} V")
509
+ st.metric("Current", f"{latest['current']:.3f} A")
510
+ st.metric("Power", f"{latest['power']:.1f} W")
511
+
512
  st.markdown('<div class="eg-section">Totals</div>', unsafe_allow_html=True)
513
+ st.metric("Energy", f"{latest['energy_kwh']:.5f} kWh")
514
+ st.metric("Bill", f" {latest['bill_pkr']:.3f}")
515
+ st.metric("CO₂", f"{latest['carbon_kg']:.5f} kg")
516
+
517
+ # Voltage health check
518
  st.markdown('<div class="eg-section">Power Quality</div>', unsafe_allow_html=True)
519
  v = latest['voltage']
520
+ if 210 <= v <= 240:
521
+ st.success("Voltage Normal")
522
+ elif 195 <= v < 210 or 240 < v <= 255:
523
+ st.warning("⚠️ Voltage Borderline")
524
+ else:
525
+ st.error("❌ Voltage Abnormal")
526
+
527
 
528
  # ════════════════════════════════════════════════════════════
529
  # TAB 3 – ANALYTICS
530
  # ════════════════════════════════════════════════════════════
531
  with tab3:
532
  if len(df) < 5:
533
+ st.info("📡 Need at least 5 readings — collecting data")
534
  else:
535
+ # Summary row
536
  st.markdown('<div class="eg-section">Summary Statistics</div>', unsafe_allow_html=True)
537
  s1,s2,s3,s4,s5 = st.columns(5)
538
+ s1.metric("Avg Voltage", f"{df['voltage'].mean():.2f} V", f"σ={df['voltage'].std():.2f}")
539
+ s2.metric("Avg Current", f"{df['current'].mean():.3f} A", f"σ={df['current'].std():.3f}")
540
  s3.metric("Avg Power", f"{df['power'].mean():.1f} W")
541
  s4.metric("Peak Power", f"{df['power'].max():.1f} W")
542
  s5.metric("Total Energy", f"{df['energy_kwh'].iloc[-1]:.5f} kWh")
543
 
544
  st.markdown('<div class="eg-section">Energy & Bill Trends</div>', unsafe_allow_html=True)
545
  ac1, ac2 = st.columns(2)
546
+
547
  with ac1:
548
  fig_e = px.area(df, x='timestamp', y='energy_kwh',
549
  color_discrete_sequence=['#00ff99'],
550
+ labels={'energy_kwh': 'kWh', 'timestamp': ''})
551
  fig_e.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
552
+ font_color='white', height=260,
553
+ margin=dict(l=8,r=8,t=8,b=8))
554
  st.plotly_chart(fig_e, use_container_width=True)
555
+
556
  with ac2:
557
  fig_bc = go.Figure()
558
  fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['bill_pkr'],
559
+ name='Bill (PKR)', yaxis='y1',
560
+ line=dict(color='#ffd700', width=1.8)))
561
  fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['carbon_kg'],
562
+ name='CO₂ (kg)', yaxis='y2',
563
+ line=dict(color='#88ff00', width=1.8)))
564
  fig_bc.update_layout(
565
+ paper_bgcolor='#080c14', plot_bgcolor='#0d1422', font_color='white',
566
+ height=260,
567
  yaxis=dict(title='PKR', color='#ffd700', gridcolor='#0d1e2e'),
568
+ yaxis2=dict(title='kg CO₂', overlaying='y', side='right',
569
+ color='#88ff00'),
570
  legend=dict(bgcolor='#0d1422', font=dict(size=10)),
571
+ margin=dict(l=8,r=8,t=8,b=8),
572
+ )
573
  st.plotly_chart(fig_bc, use_container_width=True)
574
 
575
+ # Power histogram
576
  st.markdown('<div class="eg-section">Power Distribution</div>', unsafe_allow_html=True)
577
  fig_h = px.histogram(df, x='power', nbins=30,
578
  color_discrete_sequence=['#ff4466'],
579
+ labels={'power': 'Power (W)', 'count': 'Frequency'})
580
  fig_h.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
581
+ font_color='white', height=240,
582
+ margin=dict(l=8,r=8,t=8,b=8))
583
  st.plotly_chart(fig_h, use_container_width=True)
584
 
585
+ # AI Prediction (linear regression on energy)
586
  reg_df = df.copy()
587
  reg_df['energy_kwh'] = pd.to_numeric(reg_df['energy_kwh'], errors='coerce')
588
+ reg_df['timestamp'] = pd.to_datetime(reg_df['timestamp'], errors='coerce')
589
  reg_df = reg_df.dropna(subset=['energy_kwh', 'timestamp']).reset_index(drop=True)
590
 
591
  if len(reg_df) >= 15:
592
  st.markdown('<div class="eg-section">AI Energy Prediction (Linear Regression)</div>',
593
  unsafe_allow_html=True)
594
+ x = np.arange(len(reg_df), dtype=float)
595
+ y = reg_df['energy_kwh'].to_numpy(dtype=float)
596
  coeffs = np.polyfit(x, y, 1)
597
+
598
+ n_future = 60
599
+ fx = np.arange(len(reg_df), len(reg_df) + n_future, dtype=float)
600
+ fy = np.polyval(coeffs, fx)
601
+ ft = [reg_df['timestamp'].iloc[-1] + timedelta(seconds=i) for i in range(1, n_future+1)]
602
+
603
  fig_pr = go.Figure()
604
  fig_pr.add_trace(go.Scatter(x=reg_df['timestamp'], y=reg_df['energy_kwh'],
605
+ name='Actual', line=dict(color='#00ff99', width=2)))
606
  fig_pr.add_trace(go.Scatter(x=ft, y=fy,
607
+ name='Predicted (60 s)', yaxis='y1',
608
+ line=dict(color='#ffd700', width=1.8, dash='dot')))
609
  fig_pr.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
610
  font_color='white', height=260,
611
  yaxis=dict(title='kWh', gridcolor='#0d1e2e'),
612
  legend=dict(bgcolor='#0d1422'),
613
  margin=dict(l=8,r=8,t=8,b=8))
614
  st.plotly_chart(fig_pr, use_container_width=True)
615
+
616
+ # Extrapolate
617
+ rate_kwh_per_s = coeffs[0]
618
+ daily = rate_kwh_per_s * 86400
619
  monthly = daily * 30
 
 
 
 
 
620
 
621
+ p1,p2,p3,p4 = st.columns(4)
622
+ p1.metric("⏱ Rate", f"{rate_kwh_per_s*3600:.4f} kWh/hr")
623
+ p2.metric("📅 Daily Est.", f"{daily:.4f} kWh", f"₨ {daily*rate:.2f}")
624
+ p3.metric("📅 Monthly Est.", f"{monthly:.3f} kWh", f"₨ {monthly*rate:.2f}")
625
+ p4.metric("🌱 Monthly CO₂", f"{monthly*carbon:.3f} kg")
626
+
627
+ # Data table
628
+ st.markdown('<div class="eg-section">Data Log (last 50 readings)</div>',
629
+ unsafe_allow_html=True)
630
  disp = df.tail(50).copy()
631
+ disp['timestamp'] = pd.to_datetime(disp['timestamp'], errors='coerce')
632
+ disp['timestamp'] = disp['timestamp'].dt.strftime('%H:%M:%S').fillna('--:--:--')
633
  st.dataframe(disp, use_container_width=True, height=260)
634
+
635
  csv_bytes = df.to_csv(index=False).encode()
636
+ st.download_button("⬇️ Download CSV", csv_bytes,
637
  f"energyguru_{datetime.now():%Y%m%d_%H%M%S}.csv",
638
  "text/csv", use_container_width=True)
639
 
640
+
641
  # ════════════════════════════════════════════════════════════
642
+ # TAB 4 – REPORT GENERATOR
643
  # ════════════════════════════════════════════════════════════
644
  with tab4:
645
  st.markdown('<div class="eg-section">Report Configuration</div>', unsafe_allow_html=True)
646
+
647
+ if "report_pdf_bytes" not in st.session_state:
648
+ st.session_state.report_pdf_bytes = None
649
+ if "report_pdf_filename" not in st.session_state:
650
+ st.session_state.report_pdf_filename = None
651
+
652
  rc1, rc2 = st.columns(2)
653
  with rc1:
654
+ rpt_title = st.text_input("Report Title", "EnergyGuru – Power Calculus Report")
655
+ institution = st.text_input("Institution", "Smart Energy Lab")
656
+ operator = st.text_input("Operator", "")
657
+ project_id = st.text_input("Project ID", "ENERGYGURU-2025-001")
658
  with rc2:
659
  notes = st.text_area("Notes / Remarks",
660
  "Generated by EnergyGuru Power Calculus System.\n"
661
  "Arduino-based IoT Energy Monitoring | City Model.")
662
 
663
+ generate_btn = st.button("📊 Generate PDF Report", type="primary",
664
+ use_container_width=True)
665
+
666
+ if generate_btn:
667
  if len(df) < 2:
668
+ st.error("⚠️ Not enough data — collect at least 2 readings first.")
669
  else:
670
  try:
671
+ from fpdf import FPDF # pip install fpdf2
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
+ # ── Compute summary stats ───────────────────
674
  avg_v = df['voltage'].mean()
675
  avg_i = df['current'].mean()
676
  avg_p = df['power'].mean()
 
680
  tot_b = df['bill_pkr'].iloc[-1]
681
  tot_co2 = df['carbon_kg'].iloc[-1]
682
  n_reads = len(df)
683
+ dur_s = n_reads # 1 reading per second
684
+ # Monthly projections from average power trend.
685
+ monthly_energy_est = (avg_p / 1000.0) * 24 * 30
686
+ monthly_cost_est = monthly_energy_est * rate
687
+ monthly_co2_est = monthly_energy_est * carbon
688
 
689
+ # ── AI Recommendations ──────────────────────
690
  recs = []
691
  if avg_p > 500:
692
+ recs.append("HIGH load detected - consider switching off idle appliances.")
693
  if avg_v < 210 or avg_v > 235:
694
+ recs.append("Voltage outside safe range (210-235 V) - check power supply.")
695
  if df['voltage'].std() > 8:
696
+ recs.append("High voltage fluctuation - consider a voltage stabiliser.")
697
  recs.append("Use LED lighting to reduce city model consumption by ~70%.")
698
  recs.append("Schedule high-load demos during off-peak hours (22:00-06:00).")
699
  recs.append("Install capacitor banks to improve power factor.")
700
  recs.append("Regular maintenance reduces standby losses significantly.")
701
 
702
+ def pdf_safe(text):
703
+ if text is None:
704
+ return ""
705
+ normalized = str(text).translate(str.maketrans({
706
+ "–": "-",
707
+ "—": "-",
708
+ "•": "|",
709
+ "°": " deg",
710
+ "₂": "2",
711
+ "₨": "Rs",
712
+ "✓": "[+]",
713
+ "⚠": "[!]",
714
+ }))
715
+ return normalized.encode("latin-1", "replace").decode("latin-1")
716
+
717
+ # ── Build PDF ───────────────────────────────
718
  class EnergyPDF(FPDF):
719
  def header(self):
720
+ # Dark top bar
721
  self.set_fill_color(8, 12, 20)
722
  self.rect(0, 0, 210, 297, 'F')
723
+
724
  self.set_fill_color(0, 40, 60)
725
  self.rect(0, 0, 210, 22, 'F')
726
+
727
  self.set_fill_color(0, 200, 255)
728
  self.rect(0, 0, 4, 22, 'F')
729
+
730
  self.set_font('Helvetica', 'B', 14)
731
  self.set_text_color(0, 200, 255)
732
  self.set_xy(8, 4)
733
+ self.cell(100, 7, pdf_safe('ENERGYGURU - POWER CALCULUS'), ln=False)
734
+
735
  self.set_font('Helvetica', '', 7)
736
  self.set_text_color(80, 120, 160)
737
  self.set_xy(8, 13)
738
+ self.cell(0, 5, pdf_safe(
739
+ f'AI-Assisted Energy Usage Analyzer | '
740
+ f'Generated: {datetime.now():%Y-%m-%d %H:%M:%S}'))
741
  self.ln(14)
742
+
743
  def footer(self):
744
  self.set_y(-14)
745
  self.set_font('Helvetica', 'I', 7)
746
  self.set_text_color(50, 70, 100)
747
+ self.cell(0, 8,
748
+ pdf_safe(f'EnergyGuru Power Calculus | {institution} | Page {self.page_no()}'),
749
+ align='C')
750
+
751
  def section_title(self, txt):
752
  self.set_fill_color(0, 30, 50)
753
  self.set_draw_color(0, 200, 255)
 
757
  self.set_text_color(0, 200, 255)
758
  self.cell(0, 8, pdf_safe(f' {txt}'), ln=True)
759
  self.ln(2)
760
+
761
  def kv_row(self, label, value, fill_idx):
762
+ if fill_idx % 2 == 0:
763
+ self.set_fill_color(13, 20, 34)
764
+ else:
765
+ self.set_fill_color(10, 16, 28)
766
  self.set_text_color(100, 140, 180)
767
  self.set_font('Helvetica', '', 9)
768
  self.cell(90, 7, pdf_safe(f' {label}'), fill=True)
 
774
  pdf.set_auto_page_break(auto=True, margin=18)
775
  pdf.add_page()
776
 
777
+ # Title block
778
  pdf.set_font('Helvetica', 'B', 17)
779
  pdf.set_text_color(0, 200, 255)
780
  pdf.cell(0, 10, pdf_safe(rpt_title), ln=True, align='C')
781
  pdf.ln(1)
782
+
783
  pdf.set_font('Helvetica', '', 9)
784
  pdf.set_text_color(80, 120, 160)
785
+ pdf.cell(0, 6, pdf_safe(f'Institution: {institution} | Project: {project_id}'), ln=True, align='C')
786
+ pdf.cell(0, 6, pdf_safe(f'Location: {city_choice} | Lat {loc["lat"]:.4f} deg Lon {loc["lon"]:.4f} deg'), ln=True, align='C')
787
  if operator:
788
  pdf.cell(0, 6, pdf_safe(f'Operator: {operator}'), ln=True, align='C')
789
+ pdf.cell(0, 6, pdf_safe(f'Date: {datetime.now():%B %d, %Y} Time: {datetime.now():%H:%M:%S}'), ln=True, align='C')
790
  pdf.ln(4)
791
+
792
+ # Divider
793
  pdf.set_draw_color(0, 60, 90)
794
  pdf.set_line_width(0.4)
795
  pdf.line(15, pdf.get_y(), 195, pdf.get_y())
796
  pdf.ln(5)
797
 
798
+ # ── Section 1: Measurements ──────────────────
799
  pdf.section_title('1. MEASUREMENT SUMMARY')
800
+ rows = [
801
+ ("Average Voltage", f"{avg_v:.2f} V"),
802
+ ("Average Current", f"{avg_i:.3f} A"),
803
+ ("Average Power", f"{avg_p:.2f} W"),
804
+ ("Peak Power", f"{max_p:.2f} W"),
805
+ ("Minimum Power", f"{min_p:.2f} W"),
806
+ ("Total Energy Consumed", f"{tot_e:.6f} kWh"),
807
+ ("Electricity Bill", f"PKR {tot_b:.4f}"),
808
+ ("Carbon Footprint", f"{tot_co2:.6f} kg CO2"),
809
+ ("Monthly Energy (Est.)", f"{monthly_energy_est:.3f} kWh"),
810
+ ("Monthly Cost (Est.)", f"PKR {monthly_cost_est:.2f}"),
811
+ ("Monthly CO2 (Est.)", f"{monthly_co2_est:.3f} kg"),
812
+ ("Tariff Rate", f"PKR {rate:.2f} / kWh"),
813
+ ("Carbon Factor", f"{carbon:.2f} kg CO₂ / kWh"),
814
+ ("Total Readings", f"{n_reads}"),
815
+ ("Monitoring Duration", f"{dur_s} seconds ({dur_s/60:.1f} min)"),
816
+ ]
817
+ for idx, (lbl, val) in enumerate(rows):
818
  pdf.kv_row(lbl, val, idx)
819
  pdf.ln(5)
820
 
821
+ # ── Section 2: Arduino Calculations ─────────
822
  pdf.section_title('2. ARDUINO CALCULATIONS')
823
  pdf.set_fill_color(8, 16, 26)
824
  pdf.set_font('Courier', '', 8)
825
  pdf.set_text_color(0, 220, 120)
826
+ calc_lines = [
827
+ '',
828
+ f' // Instantaneous Power',
829
+ f' power_W = voltage_V * current_A',
830
+ f' = {avg_v:.2f} V * {avg_i:.3f} A = {avg_p:.2f} W',
831
+ '',
832
+ f' // Energy accumulation (per interval)',
833
+ f' energy_kWh += (power_W / 1000.0) * dt_hours',
834
+ f' total_energy = {tot_e:.6f} kWh',
835
+ '',
836
+ f' // Electricity Bill',
837
+ f' bill_PKR = energy_kWh * tariff',
838
+ f' = {tot_e:.6f} * {rate:.2f} = PKR {tot_b:.4f}',
839
  '',
840
+ f' // Carbon Footprint',
841
+ f' carbon_kg = energy_kWh * carbon_factor',
842
+ f' = {tot_e:.6f} * {carbon:.2f} = {tot_co2:.6f} kg CO2',
 
 
843
  '',
844
+ f' // Apparent Power',
845
+ f' S (VA) = {avg_v:.2f} V * {avg_i:.3f} A = {avg_v*avg_i:.2f} VA',
846
+ '',
847
+ ]
848
+ for ln_text in calc_lines:
849
+ pdf.cell(0, 6, ln_text, fill=True, ln=True)
850
  pdf.ln(4)
851
 
852
+ # ── Section 3: Last 20 readings ──────────────
853
+ pdf.section_title('3. RECENT READINGS (last 20)')
854
+ hdrs = ['Time', 'V (V)', 'I (A)', 'P (W)',
855
+ 'kWh', 'Bill Rs', 'CO2 kg']
856
+ c_widths = [26, 22, 22, 25, 32, 30, 28]
857
+
858
+ # Table header
859
+ pdf.set_fill_color(0, 40, 60)
860
+ pdf.set_text_color(0, 200, 255)
861
+ pdf.set_font('Helvetica', 'B', 8)
862
+ for h, w in zip(hdrs, c_widths):
863
+ pdf.cell(w, 7, pdf_safe(h), fill=True, align='C')
864
  pdf.ln()
865
+
866
+ pdf.set_font('Helvetica', '', 8)
867
+ recent = df.tail(20)
868
+ for idx, (_, row) in enumerate(recent.iterrows()):
869
+ bg = (13, 20, 34) if idx % 2 == 0 else (10, 16, 28)
870
+ pdf.set_fill_color(*bg)
871
+ pdf.set_text_color(180, 200, 220)
872
+ ts = row['timestamp'].strftime('%H:%M:%S') if hasattr(row['timestamp'], 'strftime') else str(row['timestamp'])[:8]
873
+ vals = [ts,
874
+ f"{row['voltage']:.1f}",
875
+ f"{row['current']:.3f}",
876
+ f"{row['power']:.1f}",
877
+ f"{row['energy_kwh']:.6f}",
878
+ f"{row['bill_pkr']:.4f}",
879
+ f"{row['carbon_kg']:.6f}"]
880
+ for v, w in zip(vals, c_widths):
881
+ pdf.cell(w, 6, pdf_safe(v), fill=True, align='C')
882
  pdf.ln()
883
  pdf.ln(4)
884
 
885
+ # ── Section 4: AI Recommendations ───────────
886
  pdf.section_title('4. AI ENERGY RECOMMENDATIONS')
887
+ pdf.set_font('Helvetica', '', 9)
888
+ for i, rec in enumerate(recs):
889
+ icon = '[!]' if rec.startswith('HIGH') or rec.startswith('Voltage') or rec.startswith('High') else '[+]'
890
+ clr = (255, 180, 60) if icon == '[!]' else (100, 220, 130)
891
+ pdf.set_text_color(*clr)
892
+ pdf.cell(0, 8, pdf_safe(f' {icon} {rec}'), ln=True)
893
+ pdf.ln(3)
894
+
895
+ # ── Notes ────────────────────────────────────
896
  if notes.strip():
897
+ pdf.set_draw_color(0, 60, 90)
898
+ pdf.line(15, pdf.get_y(), 195, pdf.get_y())
899
  pdf.ln(3)
900
+ pdf.set_font('Helvetica', 'B', 9)
901
+ pdf.set_text_color(60, 100, 140)
902
+ pdf.cell(0, 7, 'NOTES:', ln=True)
903
+ pdf.set_font('Helvetica', '', 8)
904
+ pdf.set_text_color(140, 160, 180)
905
  for ln_text in notes.split('\n'):
906
+ pdf.cell(0, 6, pdf_safe(ln_text), ln=True)
907
 
908
+ # ── Output ───────────────────────────────────
909
  pdf_bytes = bytes(pdf.output())
910
+ st.success("Report generated successfully!")
911
+ filename = f"EnergyGuru_Report_{datetime.now():%Y%m%d_%H%M%S}.pdf"
912
+ st.session_state.report_pdf_bytes = pdf_bytes
913
+ st.session_state.report_pdf_filename = filename
914
+
915
+ # Quick preview
916
+ st.markdown('<div class="eg-section">Report Preview</div>',
917
+ unsafe_allow_html=True)
918
  pc = st.columns(4)
919
+ pc[0].metric("Total Energy", f"{tot_e:.6f} kWh")
920
+ pc[1].metric("Total Bill", f" {tot_b:.4f}")
921
+ pc[2].metric("CO₂", f"{tot_co2:.6f} kg")
922
+ pc[3].metric("Peak Power", f"{max_p:.1f} W")
923
+ pm = st.columns(3)
924
+ pm[0].metric("Monthly Energy (Est.)", f"{monthly_energy_est:.3f} kWh")
925
+ pm[1].metric("Monthly Cost (Est.)", f"₨ {monthly_cost_est:.2f}")
926
+ pm[2].metric("Monthly CO₂ (Est.)", f"{monthly_co2_est:.3f} kg")
927
 
928
  except ImportError:
929
+ st.error("⚠️ `fpdf2` is not installed. Run: **pip install fpdf2**")
930
  except Exception as ex:
931
  st.error(f"Error: {ex}")
932
+ st.exception(ex)
933
+
934
+ if st.session_state.report_pdf_bytes:
935
+ st.download_button(
936
+ "⬇️ Download PDF Report",
937
+ data=st.session_state.report_pdf_bytes,
938
+ file_name=st.session_state.report_pdf_filename,
939
+ mime="application/pdf",
940
+ type="primary",
941
+ use_container_width=True
942
+ )
943
+
944
+ render_footer()
945
 
946
+ # ── Auto refresh (live updates) ──────────────────────────────
947
  if st.session_state.demo_mode or st.session_state.connected:
948
  time.sleep(1)
949
+ st.rerun()