CoderHassan commited on
Commit
97674f5
·
verified ·
1 Parent(s): 16cd13a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +254 -421
app.py CHANGED
@@ -1,7 +1,8 @@
1
  # ============================================================
2
  # ENERGYGURU – POWER CALCULUS
3
- # Streamlit Dashboard | dashboard.py
4
- # Run: streamlit run dashboard.py
 
5
  # ============================================================
6
 
7
  import streamlit as st
@@ -9,14 +10,20 @@ import pandas as pd
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="⚡",
@@ -34,10 +41,8 @@ html, body, [class*="css"] {
34
  background-color: #080c14;
35
  }
36
 
37
- /* Hide default Streamlit elements */
38
  #MainMenu, footer, header { visibility: hidden; }
39
 
40
- /* Metric cards */
41
  .eg-card {
42
  background: linear-gradient(145deg, #0d1422, #111827);
43
  border: 1px solid #1e3a52;
@@ -68,8 +73,6 @@ html, body, [class*="css"] {
68
  text-transform: uppercase;
69
  letter-spacing: 1.5px;
70
  }
71
-
72
- /* Section headers */
73
  .eg-section {
74
  font-family: 'Share Tech Mono', monospace;
75
  color: #00c8ff;
@@ -80,13 +83,10 @@ html, body, [class*="css"] {
80
  padding-left: 10px;
81
  margin: 18px 0 10px 0;
82
  }
83
-
84
- /* Status badge */
85
  .status-live { color: #00ff99; font-size: 0.75rem; }
86
  .status-demo { color: #ffcc00; font-size: 0.75rem; }
87
  .status-off { color: #ff4466; font-size: 0.75rem; }
88
 
89
- /* Sidebar styling */
90
  section[data-testid="stSidebar"] {
91
  background: #080c14;
92
  border-right: 1px solid #1a2840;
@@ -94,6 +94,10 @@ section[data-testid="stSidebar"] {
94
  </style>
95
  """, unsafe_allow_html=True)
96
 
 
 
 
 
97
  # ── Constants ────────────────────────────────────────────────
98
  CITY_LOCATIONS = {
99
  "Rawalpindi City Model": {"lat": 33.6007, "lon": 73.0679, "desc": "Punjab, Pakistan"},
@@ -103,15 +107,6 @@ CITY_LOCATIONS = {
103
  "Custom Location": {"lat": 33.6007, "lon": 73.0679, "desc": "User-defined"},
104
  }
105
 
106
- ACCENT_COLORS = {
107
- "voltage": "#00c8ff",
108
- "current": "#ff9500",
109
- "power": "#ff4466",
110
- "energy": "#00ff99",
111
- "bill": "#ffd700",
112
- "carbon": "#88ff00",
113
- }
114
-
115
  # ── Session State Init ───────────────────────────────────────
116
  COLS = ['timestamp', 'voltage', 'current', 'power',
117
  'energy_kwh', 'bill_pkr', 'carbon_kg', 'runtime_hrs']
@@ -146,31 +141,39 @@ with st.sidebar:
146
  """, unsafe_allow_html=True)
147
 
148
  st.markdown('<div class="eg-section">Connection</div>', unsafe_allow_html=True)
149
- demo_mode = st.toggle("🎮 Demo Mode (No Hardware)", value=st.session_state.demo_mode)
150
- st.session_state.demo_mode = demo_mode
151
-
152
- if not demo_mode:
153
- ports = [p.device for p in serial.tools.list_ports.comports()]
154
- sel_port = st.selectbox("Serial Port", ports if ports else ["No ports found"])
155
- baud = st.selectbox("Baud Rate", [9600, 115200], index=0)
156
- c1, c2 = st.columns(2)
157
- with c1:
158
- if st.button("▶ Connect", use_container_width=True):
159
- try:
160
- st.session_state.serial_conn = serial.Serial(sel_port, baud, timeout=1)
161
- st.session_state.connected = True
162
- st.success("Connected!")
163
- except Exception as e:
164
- st.error(str(e))
165
- with c2:
166
- if st.button("■ Disconnect", use_container_width=True):
167
- if st.session_state.serial_conn:
168
- try: st.session_state.serial_conn.close()
169
- except: pass
170
- st.session_state.connected = False
171
- st.session_state.serial_conn = None
172
-
173
- # Status indicator
 
 
 
 
 
 
 
 
174
  if demo_mode:
175
  st.markdown('<p class="status-demo">◉ DEMO MODE ACTIVE</p>', unsafe_allow_html=True)
176
  elif st.session_state.connected:
@@ -178,32 +181,6 @@ with st.sidebar:
178
  else:
179
  st.markdown('<p class="status-off">◉ DISCONNECTED</p>', unsafe_allow_html=True)
180
 
181
- st.markdown('<div class="eg-section">Manual Override</div>', unsafe_allow_html=True)
182
- manual_override = st.toggle("✏️ Set Voltage & Current Manually", value=False)
183
- if manual_override:
184
- ov1, ov2 = st.columns(2)
185
- with ov1:
186
- manual_v = st.number_input(
187
- "Voltage (V)", min_value=0.0, max_value=260.0,
188
- value=220.0, step=0.5,
189
- help="Overrides simulated / sensor voltage"
190
- )
191
- with ov2:
192
- manual_i = st.number_input(
193
- "Current (A)", min_value=0.0, max_value=30.0,
194
- value=1.80, step=0.01,
195
- help="Overrides simulated / sensor current"
196
- )
197
- st.markdown(
198
- f'<div style="font-size:0.70rem;color:#00ff99;margin:-6px 0 4px 0;">'
199
- f'P = {manual_v:.1f} V × {manual_i:.3f} A = <strong style="color:#ff4466">'
200
- f'{manual_v * manual_i:.1f} W</strong></div>',
201
- unsafe_allow_html=True
202
- )
203
- else:
204
- manual_v = None
205
- manual_i = None
206
-
207
  st.markdown('<div class="eg-section">Settings</div>', unsafe_allow_html=True)
208
  rate = st.number_input("💰 Tariff (PKR / kWh)", 1.0, 500.0, 50.0, 1.0)
209
  carbon = st.number_input("🌱 Carbon Factor (kg CO₂ / kWh)", 0.1, 3.0, 0.82, 0.01)
@@ -224,48 +201,39 @@ with st.sidebar:
224
  st.session_state.demo_tick = 0
225
  st.success("Cleared!")
226
 
227
- # Buffer info
228
  n = len(st.session_state.data_log)
229
  st.markdown(f'<div style="color:#4a6080;font-size:0.72rem;margin-top:8px;">Buffer: {n}/500 readings</div>',
230
  unsafe_allow_html=True)
231
 
 
 
 
 
 
 
 
 
 
 
232
  # ── Data Functions ───────────────────────────────────────────
233
  def _demo_reading():
234
- """Simulate a realistic city-model reading.
235
- If manual override is active, centres the reading on the user-supplied
236
- voltage and current (with a tiny noise floor so charts stay alive).
237
- """
238
- t = st.session_state.demo_tick
239
  st.session_state.demo_tick += 1
240
-
241
- if manual_override and manual_v is not None and manual_i is not None:
242
- # Use manual values — add ±0.5 V / ±0.005 A noise so the chart isn't flat
243
- v = manual_v + random.uniform(-0.5, 0.5)
244
- i = max(0.0, manual_i + random.uniform(-0.005, 0.005))
245
- else:
246
- # AC voltage ~220 V with ±6 V fluctuation
247
- v = 220 + 5 * math.sin(t * 0.07) + random.uniform(-2, 2)
248
- # Load current varies 0.8–2.8 A (city model lamps + motors)
249
- i = 1.8 + 0.6 * math.sin(t * 0.04) + 0.2 * math.sin(t * 0.13) + random.uniform(-0.05, 0.05)
250
- i = max(0.1, i)
251
-
252
  p = v * i
253
-
254
  dt_h = 1 / 3600
255
  st.session_state.demo_energy += (p / 1000) * dt_h
256
-
257
  e = st.session_state.demo_energy
258
  b = e * rate
259
  co2 = e * carbon
260
  rth = t / 3600
261
-
262
- return dict(voltage=round(v,2), current=round(i,3),
263
- power=round(p,2), energy_kwh=round(e,6),
264
- bill_pkr=round(b,4), carbon_kg=round(co2,6),
265
- runtime_hrs=round(rth,5))
266
 
267
  def _arduino_reading():
268
- """Read one CSV line from Arduino serial."""
269
  conn = st.session_state.serial_conn
270
  if not conn or not st.session_state.connected:
271
  return None
@@ -274,23 +242,21 @@ def _arduino_reading():
274
  if ',' in raw:
275
  p = raw.split(',')
276
  if len(p) == 7:
277
- return dict(voltage=float(p[0]), current=float(p[1]),
278
- power=float(p[2]), energy_kwh=float(p[3]),
279
- bill_pkr=float(p[4]), carbon_kg=float(p[5]),
280
  runtime_hrs=float(p[6]))
281
  except Exception:
282
  pass
283
  return None
284
 
285
  def _log(data):
286
- """Append to session log; keep last 500 rows."""
287
  row = pd.DataFrame([{"timestamp": datetime.now(), **data}])
288
  st.session_state.data_log = pd.concat(
289
  [st.session_state.data_log, row], ignore_index=True
290
  ).tail(500)
291
  st.session_state.latest = data
292
 
293
- # ── Fetch current reading ────────────────────────────────────
294
  if st.session_state.demo_mode:
295
  _log(_demo_reading())
296
  elif st.session_state.connected:
@@ -314,27 +280,22 @@ st.markdown("""
314
  <hr style="border-color:#1a2840; margin:6px 0 14px 0;">
315
  """, unsafe_allow_html=True)
316
 
317
- # ── Tabs ─────────────────────────────────────────────────────
318
  tab1, tab2, tab3, tab4 = st.tabs([
319
- "⚡ Live Dashboard",
320
- "🗺️ City Map",
321
- "📈 Analytics",
322
- "📄 Report",
323
  ])
324
 
325
  # ════════════════════════════════════════════════════════════
326
  # TAB 1 – LIVE DASHBOARD
327
  # ════════════════════════════════════════════════════════════
328
  with tab1:
329
- # ── 6 metric cards ──────────────────────────────────────
330
  mc = st.columns(6)
331
  cards = [
332
- ("⚡ VOLTAGE", f"{latest['voltage']:.1f} V", "#00c8ff"),
333
- ("🔌 CURRENT", f"{latest['current']:.3f} A", "#ff9500"),
334
- ("💡 POWER", f"{latest['power']:.1f} W", "#ff4466"),
335
  ("🔋 ENERGY", f"{latest['energy_kwh']:.5f} kWh", "#00ff99"),
336
- ("💰 BILL", f" {latest['bill_pkr']:.3f}", "#ffd700"),
337
- ("🌱 CO₂", f"{latest['carbon_kg']:.5f} kg", "#88ff00"),
338
  ]
339
  for col, (lbl, val, clr) in zip(mc, cards):
340
  with col:
@@ -346,11 +307,9 @@ with tab1:
346
 
347
  st.markdown("<br>", unsafe_allow_html=True)
348
 
349
- # ── 3 gauges ────────────────────────────────────────────
350
  def gauge(value, title, max_v, color, unit, threshold=0.85):
351
  fig = go.Figure(go.Indicator(
352
- mode="gauge+number",
353
- value=value,
354
  title={'text': title, 'font': {'color': '#8a9ab0', 'size': 12,
355
  'family': 'Share Tech Mono'}},
356
  number={'suffix': f' {unit}', 'font': {'color': color, 'size': 20,
@@ -359,12 +318,11 @@ with tab1:
359
  'axis': {'range': [0, max_v], 'tickcolor': '#2a3a50',
360
  'tickfont': {'size': 9, 'color': '#4a6080'}},
361
  'bar': {'color': color, 'thickness': 0.25},
362
- 'bgcolor': '#0d1422',
363
- 'bordercolor': '#1a2840', 'borderwidth': 1,
364
  'steps': [
365
- {'range': [0, max_v * 0.5], 'color': '#0d1422'},
366
- {'range': [max_v * 0.5, max_v * threshold], 'color': '#111d2e'},
367
- {'range': [max_v * threshold, max_v], 'color': '#1a1020'},
368
  ],
369
  'threshold': {'line': {'color': '#ff4466', 'width': 2},
370
  'thickness': 0.75, 'value': max_v * threshold}
@@ -376,273 +334,211 @@ with tab1:
376
 
377
  g1, g2, g3 = st.columns(3)
378
  with g1: st.plotly_chart(gauge(latest['voltage'], "VOLTAGE (V)", 260, "#00c8ff", "V"), use_container_width=True)
379
- with g2: st.plotly_chart(gauge(latest['current'], "CURRENT (A)", 5, "#ff9500", "A"), use_container_width=True)
380
- with g3: st.plotly_chart(gauge(latest['power'], "POWER (W)", 1100, "#ff4466", "W"), use_container_width=True)
381
 
382
- # ── Real-time charts ────────────────────────────────────
383
  if len(df) > 1:
384
  rc1, rc2 = st.columns(2)
385
-
386
  with rc1:
387
  st.markdown('<div class="eg-section">Voltage & Current — Live</div>', unsafe_allow_html=True)
388
  fig_vc = go.Figure()
389
- fig_vc.add_trace(go.Scatter(
390
- x=df['timestamp'], y=df['voltage'],
391
- name='Voltage (V)', line=dict(color='#00c8ff', width=1.8), yaxis='y1'
392
- ))
393
- fig_vc.add_trace(go.Scatter(
394
- x=df['timestamp'], y=df['current'],
395
- name='Current (A)', line=dict(color='#ff9500', width=1.8), yaxis='y2'
396
- ))
397
  fig_vc.update_layout(
398
- paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
399
- font_color='white', height=260,
400
  yaxis=dict(title='V', color='#00c8ff', gridcolor='#0d1e2e'),
401
- yaxis2=dict(title='A', overlaying='y', side='right',
402
- color='#ff9500', gridcolor='#0d1e2e'),
403
  legend=dict(bgcolor='#0d1422', font=dict(size=10)),
404
- margin=dict(l=8,r=8,t=8,b=8),
405
- )
406
  st.plotly_chart(fig_vc, use_container_width=True)
407
-
408
  with rc2:
409
  st.markdown('<div class="eg-section">Power — Live</div>', unsafe_allow_html=True)
410
  fig_pw = go.Figure()
411
- fig_pw.add_trace(go.Scatter(
412
- x=df['timestamp'], y=df['power'],
413
  fill='tozeroy', name='Power (W)',
414
  line=dict(color='#ff4466', width=1.8),
415
- fillcolor='rgba(255,68,102,0.15)'
416
- ))
417
  fig_pw.update_layout(
418
- paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
419
- font_color='white', height=260,
420
  yaxis=dict(title='Watts', gridcolor='#0d1e2e'),
421
- margin=dict(l=8,r=8,t=8,b=8),
422
- )
423
  st.plotly_chart(fig_pw, use_container_width=True)
424
  else:
425
- st.info("📡 Collecting readings Charts will appear after a few seconds.")
426
-
427
 
428
  # ════════════════════════════════════════════════════════════
429
  # TAB 2 – CITY MAP
430
  # ════════════════════════════════════════════════════════════
431
  with tab2:
432
  map_col, info_col = st.columns([3, 1])
433
-
434
  with map_col:
435
- st.markdown('<div class="eg-section">City Energy Monitor — Location</div>',
436
- unsafe_allow_html=True)
437
-
438
- # Build coverage ring
439
- ring_lats = [loc['lat'] + 0.012 * math.cos(math.radians(i)) for i in range(361)]
440
- ring_lons = [loc['lon'] + 0.018 * math.sin(math.radians(i)) for i in range(361)]
441
-
442
  fig_map = go.Figure()
443
-
444
- # Coverage ring
445
- fig_map.add_trace(go.Scattermapbox(
446
- lat=ring_lats, lon=ring_lons,
447
- mode='lines',
448
- line=dict(color='rgba(0,200,255,0.4)', width=2),
449
- name='Monitor Zone', showlegend=False,
450
- ))
451
-
452
- # Main marker
453
- fig_map.add_trace(go.Scattermapbox(
454
- lat=[loc['lat']], lon=[loc['lon']],
455
- mode='markers+text',
456
- marker=dict(size=18, color='#00c8ff',
457
- symbol='circle', opacity=0.9),
458
- text=[f"⚡ {city_choice}"],
459
- textposition='top right',
460
  textfont=dict(color='white', size=13, family='Share Tech Mono'),
461
- name=city_choice,
462
- ))
463
-
464
  fig_map.update_layout(
465
- mapbox=dict(
466
- style='open-street-map',
467
- center=dict(lat=loc['lat'], lon=loc['lon']),
468
- zoom=13,
469
- ),
470
- paper_bgcolor='#080c14',
471
- font_color='white',
472
- height=480,
473
- margin=dict(l=0, r=0, t=0, b=0),
474
- showlegend=False,
475
- )
476
  st.plotly_chart(fig_map, use_container_width=True)
477
 
478
  with info_col:
479
  st.markdown('<div class="eg-section">Location</div>', unsafe_allow_html=True)
480
  st.markdown(f"""
481
- <div style="font-family:'Share Tech Mono',monospace; font-size:0.78rem;
482
- color:#8a9ab0; line-height:1.9;">
483
- <div style="color:#00c8ff; font-size:0.95rem; margin-bottom:4px;">{city_choice}</div>
484
- {loc['desc']}<br>
485
- LAT: {loc['lat']:.4f}°<br>
486
- LON: {loc['lon']:.4f}°
487
- </div>
488
- """, unsafe_allow_html=True)
489
-
490
  st.markdown('<div class="eg-section">Live Readings</div>', unsafe_allow_html=True)
491
- st.metric("Voltage", f"{latest['voltage']:.1f} V")
492
- st.metric("Current", f"{latest['current']:.3f} A")
493
- st.metric("Power", f"{latest['power']:.1f} W")
494
-
495
  st.markdown('<div class="eg-section">Totals</div>', unsafe_allow_html=True)
496
- st.metric("Energy", f"{latest['energy_kwh']:.5f} kWh")
497
- st.metric("Bill", f" {latest['bill_pkr']:.3f}")
498
- st.metric("CO₂", f"{latest['carbon_kg']:.5f} kg")
499
-
500
- # Voltage health check
501
  st.markdown('<div class="eg-section">Power Quality</div>', unsafe_allow_html=True)
502
  v = latest['voltage']
503
- if 210 <= v <= 240:
504
- st.success("Voltage Normal")
505
- elif 195 <= v < 210 or 240 < v <= 255:
506
- st.warning("⚠️ Voltage Borderline")
507
- else:
508
- st.error("❌ Voltage Abnormal")
509
-
510
 
511
  # ════════════════════════════════════════════════════════════
512
  # TAB 3 – ANALYTICS
513
  # ════════════════════════════════════════════════════════════
514
  with tab3:
515
  if len(df) < 5:
516
- st.info("📡 Need at least 5 readings — collecting data")
517
  else:
518
- # Summary row
519
  st.markdown('<div class="eg-section">Summary Statistics</div>', unsafe_allow_html=True)
520
  s1,s2,s3,s4,s5 = st.columns(5)
521
- s1.metric("Avg Voltage", f"{df['voltage'].mean():.2f} V", f"σ={df['voltage'].std():.2f}")
522
- s2.metric("Avg Current", f"{df['current'].mean():.3f} A", f"σ={df['current'].std():.3f}")
523
  s3.metric("Avg Power", f"{df['power'].mean():.1f} W")
524
  s4.metric("Peak Power", f"{df['power'].max():.1f} W")
525
  s5.metric("Total Energy", f"{df['energy_kwh'].iloc[-1]:.5f} kWh")
526
 
527
  st.markdown('<div class="eg-section">Energy & Bill Trends</div>', unsafe_allow_html=True)
528
  ac1, ac2 = st.columns(2)
529
-
530
  with ac1:
531
  fig_e = px.area(df, x='timestamp', y='energy_kwh',
532
  color_discrete_sequence=['#00ff99'],
533
- labels={'energy_kwh': 'kWh', 'timestamp': ''})
534
  fig_e.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
535
- font_color='white', height=260,
536
- margin=dict(l=8,r=8,t=8,b=8))
537
  st.plotly_chart(fig_e, use_container_width=True)
538
-
539
  with ac2:
540
  fig_bc = go.Figure()
541
  fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['bill_pkr'],
542
- name='Bill (PKR)', yaxis='y1',
543
- line=dict(color='#ffd700', width=1.8)))
544
  fig_bc.add_trace(go.Scatter(x=df['timestamp'], y=df['carbon_kg'],
545
- name='CO₂ (kg)', yaxis='y2',
546
- line=dict(color='#88ff00', width=1.8)))
547
  fig_bc.update_layout(
548
- paper_bgcolor='#080c14', plot_bgcolor='#0d1422', font_color='white',
549
- height=260,
550
  yaxis=dict(title='PKR', color='#ffd700', gridcolor='#0d1e2e'),
551
- yaxis2=dict(title='kg CO₂', overlaying='y', side='right',
552
- color='#88ff00'),
553
  legend=dict(bgcolor='#0d1422', font=dict(size=10)),
554
- margin=dict(l=8,r=8,t=8,b=8),
555
- )
556
  st.plotly_chart(fig_bc, use_container_width=True)
557
 
558
- # Power histogram
559
  st.markdown('<div class="eg-section">Power Distribution</div>', unsafe_allow_html=True)
560
  fig_h = px.histogram(df, x='power', nbins=30,
561
  color_discrete_sequence=['#ff4466'],
562
- labels={'power': 'Power (W)', 'count': 'Frequency'})
563
  fig_h.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
564
- font_color='white', height=240,
565
- margin=dict(l=8,r=8,t=8,b=8))
566
  st.plotly_chart(fig_h, use_container_width=True)
567
 
568
- # AI Prediction (linear regression on energy)
569
- if len(df) >= 15:
 
 
 
 
570
  st.markdown('<div class="eg-section">AI Energy Prediction (Linear Regression)</div>',
571
  unsafe_allow_html=True)
572
- x = np.arange(len(df))
573
- y = df['energy_kwh'].values
574
  coeffs = np.polyfit(x, y, 1)
575
-
576
- n_future = 60
577
- fx = np.arange(len(df), len(df) + n_future)
578
- fy = np.polyval(coeffs, fx)
579
- ft = [df['timestamp'].iloc[-1] + timedelta(seconds=i) for i in range(1, n_future+1)]
580
-
581
  fig_pr = go.Figure()
582
- fig_pr.add_trace(go.Scatter(x=df['timestamp'], y=df['energy_kwh'],
583
- name='Actual', line=dict(color='#00ff99', width=2)))
584
  fig_pr.add_trace(go.Scatter(x=ft, y=fy,
585
- name='Predicted (60 s)', yaxis='y1',
586
- line=dict(color='#ffd700', width=1.8, dash='dot')))
587
  fig_pr.update_layout(paper_bgcolor='#080c14', plot_bgcolor='#0d1422',
588
  font_color='white', height=260,
589
  yaxis=dict(title='kWh', gridcolor='#0d1e2e'),
590
  legend=dict(bgcolor='#0d1422'),
591
  margin=dict(l=8,r=8,t=8,b=8))
592
  st.plotly_chart(fig_pr, use_container_width=True)
593
-
594
- # Extrapolate
595
- rate_kwh_per_s = coeffs[0]
596
- daily = rate_kwh_per_s * 86400
597
  monthly = daily * 30
598
-
599
  p1,p2,p3,p4 = st.columns(4)
600
- p1.metric("Rate", f"{rate_kwh_per_s*3600:.4f} kWh/hr")
601
- p2.metric("📅 Daily Est.", f"{daily:.4f} kWh", f" {daily*rate:.2f}")
602
- p3.metric("📅 Monthly Est.", f"{monthly:.3f} kWh", f" {monthly*rate:.2f}")
603
- p4.metric("🌱 Monthly CO₂", f"{monthly*carbon:.3f} kg")
604
-
605
- # Data table
606
- st.markdown('<div class="eg-section">Data Log (last 50 readings)</div>',
607
- unsafe_allow_html=True)
608
  disp = df.tail(50).copy()
609
- disp['timestamp'] = disp['timestamp'].dt.strftime('%H:%M:%S')
610
  st.dataframe(disp, use_container_width=True, height=260)
611
-
612
  csv_bytes = df.to_csv(index=False).encode()
613
- st.download_button("⬇️ Download CSV", csv_bytes,
614
  f"energyguru_{datetime.now():%Y%m%d_%H%M%S}.csv",
615
  "text/csv", use_container_width=True)
616
 
617
-
618
  # ════════════════════════════════════════════════════════════
619
- # TAB 4 – REPORT GENERATOR
620
  # ════════════════════════════════════════════════════════════
621
  with tab4:
622
  st.markdown('<div class="eg-section">Report Configuration</div>', unsafe_allow_html=True)
623
-
624
  rc1, rc2 = st.columns(2)
625
  with rc1:
626
- rpt_title = st.text_input("Report Title", "EnergyGuru – Power Calculus Report")
627
- institution = st.text_input("Institution", "Smart Energy Lab")
628
- operator = st.text_input("Operator", "")
629
- project_id = st.text_input("Project ID", "ENERGYGURU-2025-001")
630
  with rc2:
631
  notes = st.text_area("Notes / Remarks",
632
  "Generated by EnergyGuru Power Calculus System.\n"
633
  "Arduino-based IoT Energy Monitoring | City Model.")
634
 
635
- generate_btn = st.button("📊 Generate PDF Report", type="primary",
636
- use_container_width=True)
637
-
638
- if generate_btn:
639
  if len(df) < 2:
640
- st.error("⚠️ Not enough data — collect at least 2 readings first.")
641
  else:
642
  try:
643
- from fpdf import FPDF # pip install fpdf2
 
 
 
 
 
 
 
 
 
 
 
 
644
 
645
- # ── Compute summary stats ───────────────────
646
  avg_v = df['voltage'].mean()
647
  avg_i = df['current'].mean()
648
  avg_p = df['power'].mean()
@@ -652,55 +548,41 @@ with tab4:
652
  tot_b = df['bill_pkr'].iloc[-1]
653
  tot_co2 = df['carbon_kg'].iloc[-1]
654
  n_reads = len(df)
655
- dur_s = n_reads # 1 reading per second
656
 
657
- # ── AI Recommendations ──────────────────────
658
  recs = []
659
  if avg_p > 500:
660
  recs.append("HIGH load detected — consider switching off idle appliances.")
661
  if avg_v < 210 or avg_v > 235:
662
- recs.append("Voltage outside safe range (210–235 V) — check power supply.")
663
  if df['voltage'].std() > 8:
664
  recs.append("High voltage fluctuation — consider a voltage stabiliser.")
665
  recs.append("Use LED lighting to reduce city model consumption by ~70%.")
666
- recs.append("Schedule high-load demos during off-peak hours (22:0006:00).")
667
  recs.append("Install capacitor banks to improve power factor.")
668
  recs.append("Regular maintenance reduces standby losses significantly.")
669
 
670
- # ── Build PDF ───────────────────────────────
671
  class EnergyPDF(FPDF):
672
  def header(self):
673
- # Dark top bar
674
  self.set_fill_color(8, 12, 20)
675
  self.rect(0, 0, 210, 297, 'F')
676
-
677
  self.set_fill_color(0, 40, 60)
678
  self.rect(0, 0, 210, 22, 'F')
679
-
680
  self.set_fill_color(0, 200, 255)
681
  self.rect(0, 0, 4, 22, 'F')
682
-
683
  self.set_font('Helvetica', 'B', 14)
684
  self.set_text_color(0, 200, 255)
685
  self.set_xy(8, 4)
686
- self.cell(100, 7, 'ENERGYGURU POWER CALCULUS', ln=False)
687
-
688
  self.set_font('Helvetica', '', 7)
689
  self.set_text_color(80, 120, 160)
690
  self.set_xy(8, 13)
691
- self.cell(0, 5,
692
- f'AI-Assisted Energy Usage Analyzer | '
693
- f'Generated: {datetime.now():%Y-%m-%d %H:%M:%S}')
694
  self.ln(14)
695
-
696
  def footer(self):
697
  self.set_y(-14)
698
  self.set_font('Helvetica', 'I', 7)
699
  self.set_text_color(50, 70, 100)
700
- self.cell(0, 8,
701
- f'EnergyGuru Power Calculus | {institution} | Page {self.page_no()}',
702
- align='C')
703
-
704
  def section_title(self, txt):
705
  self.set_fill_color(0, 30, 50)
706
  self.set_draw_color(0, 200, 255)
@@ -708,177 +590,128 @@ with tab4:
708
  self.rect(self.get_x(), self.get_y(), 185, 8, 'DF')
709
  self.set_font('Helvetica', 'B', 9)
710
  self.set_text_color(0, 200, 255)
711
- self.cell(0, 8, f' {txt}', ln=True)
712
  self.ln(2)
713
-
714
  def kv_row(self, label, value, fill_idx):
715
- if fill_idx % 2 == 0:
716
- self.set_fill_color(13, 20, 34)
717
- else:
718
- self.set_fill_color(10, 16, 28)
719
  self.set_text_color(100, 140, 180)
720
  self.set_font('Helvetica', '', 9)
721
- self.cell(90, 7, f' {label}', fill=True)
722
  self.set_text_color(220, 230, 240)
723
  self.set_font('Helvetica', 'B', 9)
724
- self.cell(95, 7, f' {value}', fill=True, ln=True)
725
 
726
  pdf = EnergyPDF()
727
  pdf.set_auto_page_break(auto=True, margin=18)
728
  pdf.add_page()
729
 
730
- # Title block
731
  pdf.set_font('Helvetica', 'B', 17)
732
  pdf.set_text_color(0, 200, 255)
733
- pdf.cell(0, 10, rpt_title, ln=True, align='C')
734
  pdf.ln(1)
735
-
736
  pdf.set_font('Helvetica', '', 9)
737
  pdf.set_text_color(80, 120, 160)
738
- pdf.cell(0, 6, f'Institution: {institution} | Project: {project_id}', ln=True, align='C')
739
- pdf.cell(0, 6, f'Location: {city_choice}Lat {loc["lat"]:.4f}° Lon {loc["lon"]:.4f}°', ln=True, align='C')
740
  if operator:
741
- pdf.cell(0, 6, f'Operator: {operator}', ln=True, align='C')
742
- pdf.cell(0, 6, f'Date: {datetime.now():%B %d, %Y} Time: {datetime.now():%H:%M:%S}', ln=True, align='C')
743
  pdf.ln(4)
744
-
745
- # Divider
746
  pdf.set_draw_color(0, 60, 90)
747
  pdf.set_line_width(0.4)
748
  pdf.line(15, pdf.get_y(), 195, pdf.get_y())
749
  pdf.ln(5)
750
 
751
- # ── Section 1: Measurements ──────────────────
752
  pdf.section_title('1. MEASUREMENT SUMMARY')
753
- rows = [
754
- ("Average Voltage", f"{avg_v:.2f} V"),
755
- ("Average Current", f"{avg_i:.3f} A"),
756
- ("Average Power", f"{avg_p:.2f} W"),
757
- ("Peak Power", f"{max_p:.2f} W"),
758
- ("Minimum Power", f"{min_p:.2f} W"),
759
- ("Total Energy Consumed", f"{tot_e:.6f} kWh"),
760
- ("Electricity Bill", f"PKR {tot_b:.4f}"),
761
- ("Carbon Footprint", f"{tot_co2:.6f} kg CO₂"),
762
- ("Tariff Rate", f"PKR {rate:.2f} / kWh"),
763
- ("Carbon Factor", f"{carbon:.2f} kg CO₂ / kWh"),
764
- ("Total Readings", f"{n_reads}"),
765
- ("Monitoring Duration", f"{dur_s} seconds ({dur_s/60:.1f} min)"),
766
- ]
767
- for idx, (lbl, val) in enumerate(rows):
768
  pdf.kv_row(lbl, val, idx)
769
  pdf.ln(5)
770
 
771
- # ── Section 2: Arduino Calculations ─────────
772
  pdf.section_title('2. ARDUINO CALCULATIONS')
773
  pdf.set_fill_color(8, 16, 26)
774
  pdf.set_font('Courier', '', 8)
775
  pdf.set_text_color(0, 220, 120)
776
- calc_lines = [
777
- '',
778
- f' // Instantaneous Power',
779
- f' power_W = voltage_V * current_A',
780
- f' = {avg_v:.2f} V * {avg_i:.3f} A = {avg_p:.2f} W',
781
- '',
782
- f' // Energy accumulation (per interval)',
783
- f' energy_kWh += (power_W / 1000.0) * dt_hours',
784
- f' total_energy = {tot_e:.6f} kWh',
785
  '',
786
- f' // Electricity Bill',
787
- f' bill_PKR = energy_kWh * tariff',
788
- f' = {tot_e:.6f} * {rate:.2f} = PKR {tot_b:.4f}',
 
 
789
  '',
790
- f' // Carbon Footprint',
791
- f' carbon_kg = energy_kWh * carbon_factor',
792
- f' = {tot_e:.6f} * {carbon:.2f} = {tot_co2:.6f} kg CO2',
793
- '',
794
- f' // Apparent Power',
795
- f' S (VA) = {avg_v:.2f} V * {avg_i:.3f} A = {avg_v*avg_i:.2f} VA',
796
- '',
797
- ]
798
- for ln_text in calc_lines:
799
- pdf.cell(0, 6, ln_text, fill=True, ln=True)
800
  pdf.ln(4)
801
 
802
- # ── Section 3: Last 20 readings ──────────────
803
- pdf.section_title('3. RECENT READINGS (last 20)')
804
- hdrs = ['Time', 'V (V)', 'I (A)', 'P (W)',
805
- 'kWh', 'Bill ₨', 'CO₂ kg']
806
- c_widths = [26, 22, 22, 25, 32, 30, 28]
807
-
808
- # Table header
809
- pdf.set_fill_color(0, 40, 60)
810
- pdf.set_text_color(0, 200, 255)
811
- pdf.set_font('Helvetica', 'B', 8)
812
- for h, w in zip(hdrs, c_widths):
813
- pdf.cell(w, 7, h, fill=True, align='C')
814
  pdf.ln()
815
-
816
- pdf.set_font('Helvetica', '', 8)
817
- recent = df.tail(20)
818
- for idx, (_, row) in enumerate(recent.iterrows()):
819
- bg = (13, 20, 34) if idx % 2 == 0 else (10, 16, 28)
820
- pdf.set_fill_color(*bg)
821
- pdf.set_text_color(180, 200, 220)
822
- ts = row['timestamp'].strftime('%H:%M:%S') if hasattr(row['timestamp'], 'strftime') else str(row['timestamp'])[:8]
823
- vals = [ts,
824
- f"{row['voltage']:.1f}",
825
- f"{row['current']:.3f}",
826
- f"{row['power']:.1f}",
827
- f"{row['energy_kwh']:.6f}",
828
- f"{row['bill_pkr']:.4f}",
829
- f"{row['carbon_kg']:.6f}"]
830
- for v, w in zip(vals, c_widths):
831
- pdf.cell(w, 6, v, fill=True, align='C')
832
  pdf.ln()
833
  pdf.ln(4)
834
 
835
- # ── Section 4: AI Recommendations ───────────
836
  pdf.section_title('4. AI ENERGY RECOMMENDATIONS')
837
- pdf.set_font('Helvetica', '', 9)
838
- for i, rec in enumerate(recs):
839
- icon = '⚠' if rec.startswith('HIGH') or rec.startswith('Voltage') or rec.startswith('High') else '✓'
840
- clr = (255, 180, 60) if icon == '⚠' else (100, 220, 130)
841
- pdf.set_text_color(*clr)
842
- pdf.cell(0, 8, f' {icon} {rec}', ln=True)
843
- pdf.ln(3)
844
-
845
- # ── Notes ────────────────────────────────────
846
  if notes.strip():
847
- pdf.set_draw_color(0, 60, 90)
848
- pdf.line(15, pdf.get_y(), 195, pdf.get_y())
849
  pdf.ln(3)
850
- pdf.set_font('Helvetica', 'B', 9)
851
- pdf.set_text_color(60, 100, 140)
852
- pdf.cell(0, 7, 'NOTES:', ln=True)
853
- pdf.set_font('Helvetica', '', 8)
854
- pdf.set_text_color(140, 160, 180)
855
  for ln_text in notes.split('\n'):
856
- pdf.cell(0, 6, ln_text, ln=True)
857
 
858
- # ── Output ───────────────────────────────────
859
  pdf_bytes = bytes(pdf.output())
860
- st.success("Report generated successfully!")
861
- filename = f"EnergyGuru_Report_{datetime.now():%Y%m%d_%H%M%S}.pdf"
862
- st.download_button("⬇️ Download PDF Report", pdf_bytes,
863
- filename, "application/pdf",
864
- type="primary", use_container_width=True)
865
-
866
- # Quick preview
867
- st.markdown('<div class="eg-section">Report Preview</div>',
868
- unsafe_allow_html=True)
869
  pc = st.columns(4)
870
- pc[0].metric("Total Energy", f"{tot_e:.6f} kWh")
871
- pc[1].metric("Total Bill", f" {tot_b:.4f}")
872
- pc[2].metric("CO₂", f"{tot_co2:.6f} kg")
873
- pc[3].metric("Peak Power", f"{max_p:.1f} W")
874
 
875
  except ImportError:
876
- st.error("⚠️ `fpdf2` is not installed. Run: **pip install fpdf2**")
877
  except Exception as ex:
878
  st.error(f"Error: {ex}")
879
- st.exception(ex)
880
 
881
- # ── Auto refresh (live updates) ──────────────────────────────
882
  if st.session_state.demo_mode or st.session_state.connected:
883
  time.sleep(1)
884
  st.rerun()
 
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
  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="⚡",
 
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;
 
73
  text-transform: uppercase;
74
  letter-spacing: 1.5px;
75
  }
 
 
76
  .eg-section {
77
  font-family: 'Share Tech Mono', monospace;
78
  color: #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;
 
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
  "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']
 
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
  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)
 
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
  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:
 
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:
 
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
  '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}
 
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
  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)
 
590
  self.rect(self.get_x(), self.get_y(), 185, 8, 'DF')
591
  self.set_font('Helvetica', 'B', 9)
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)
600
  self.set_text_color(220, 230, 240)
601
  self.set_font('Helvetica', 'B', 9)
602
+ self.cell(95, 7, pdf_safe(f' {value}'), fill=True, ln=True)
603
 
604
  pdf = EnergyPDF()
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()