Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -178,6 +178,32 @@ 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">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)
|
|
@@ -205,15 +231,24 @@ with st.sidebar:
|
|
| 205 |
|
| 206 |
# ββ Data Functions βββββββββββββββββββββββββββββββββββββββββββ
|
| 207 |
def _demo_reading():
|
| 208 |
-
"""Simulate a realistic city-model reading.
|
|
|
|
|
|
|
|
|
|
| 209 |
t = st.session_state.demo_tick
|
| 210 |
st.session_state.demo_tick += 1
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
p = v * i
|
| 218 |
|
| 219 |
dt_h = 1 / 3600
|
|
@@ -531,25 +566,20 @@ with tab3:
|
|
| 531 |
st.plotly_chart(fig_h, use_container_width=True)
|
| 532 |
|
| 533 |
# AI Prediction (linear regression on energy)
|
| 534 |
-
|
| 535 |
-
reg_df['energy_kwh'] = pd.to_numeric(reg_df['energy_kwh'], errors='coerce')
|
| 536 |
-
reg_df['timestamp'] = pd.to_datetime(reg_df['timestamp'], errors='coerce')
|
| 537 |
-
reg_df = reg_df.dropna(subset=['energy_kwh', 'timestamp']).reset_index(drop=True)
|
| 538 |
-
|
| 539 |
-
if len(reg_df) >= 15:
|
| 540 |
st.markdown('<div class="eg-section">AI Energy Prediction (Linear Regression)</div>',
|
| 541 |
unsafe_allow_html=True)
|
| 542 |
-
x = np.arange(len(
|
| 543 |
-
y =
|
| 544 |
coeffs = np.polyfit(x, y, 1)
|
| 545 |
|
| 546 |
n_future = 60
|
| 547 |
-
fx = np.arange(len(
|
| 548 |
fy = np.polyval(coeffs, fx)
|
| 549 |
-
ft = [
|
| 550 |
|
| 551 |
fig_pr = go.Figure()
|
| 552 |
-
fig_pr.add_trace(go.Scatter(x=
|
| 553 |
name='Actual', line=dict(color='#00ff99', width=2)))
|
| 554 |
fig_pr.add_trace(go.Scatter(x=ft, y=fy,
|
| 555 |
name='Predicted (60 s)', yaxis='y1',
|
|
@@ -576,8 +606,7 @@ with tab3:
|
|
| 576 |
st.markdown('<div class="eg-section">Data Log (last 50 readings)</div>',
|
| 577 |
unsafe_allow_html=True)
|
| 578 |
disp = df.tail(50).copy()
|
| 579 |
-
disp['timestamp'] =
|
| 580 |
-
disp['timestamp'] = disp['timestamp'].dt.strftime('%H:%M:%S').fillna('--:--:--')
|
| 581 |
st.dataframe(disp, use_container_width=True, height=260)
|
| 582 |
|
| 583 |
csv_bytes = df.to_csv(index=False).encode()
|
|
@@ -628,31 +657,16 @@ with tab4:
|
|
| 628 |
# ββ AI Recommendations ββββββββββββββββββββββ
|
| 629 |
recs = []
|
| 630 |
if avg_p > 500:
|
| 631 |
-
recs.append("HIGH load detected
|
| 632 |
if avg_v < 210 or avg_v > 235:
|
| 633 |
-
recs.append("Voltage outside safe range (210
|
| 634 |
if df['voltage'].std() > 8:
|
| 635 |
-
recs.append("High voltage fluctuation
|
| 636 |
recs.append("Use LED lighting to reduce city model consumption by ~70%.")
|
| 637 |
-
recs.append("Schedule high-load demos during off-peak hours (22:00
|
| 638 |
recs.append("Install capacitor banks to improve power factor.")
|
| 639 |
recs.append("Regular maintenance reduces standby losses significantly.")
|
| 640 |
|
| 641 |
-
def pdf_safe(text):
|
| 642 |
-
if text is None:
|
| 643 |
-
return ""
|
| 644 |
-
normalized = str(text).translate(str.maketrans({
|
| 645 |
-
"β": "-",
|
| 646 |
-
"β": "-",
|
| 647 |
-
"β’": "|",
|
| 648 |
-
"Β°": " deg",
|
| 649 |
-
"β": "2",
|
| 650 |
-
"β¨": "Rs",
|
| 651 |
-
"β": "[+]",
|
| 652 |
-
"β ": "[!]",
|
| 653 |
-
}))
|
| 654 |
-
return normalized.encode("latin-1", "replace").decode("latin-1")
|
| 655 |
-
|
| 656 |
# ββ Build PDF βββββββββββββββββββββββββββββββ
|
| 657 |
class EnergyPDF(FPDF):
|
| 658 |
def header(self):
|
|
@@ -669,14 +683,14 @@ with tab4:
|
|
| 669 |
self.set_font('Helvetica', 'B', 14)
|
| 670 |
self.set_text_color(0, 200, 255)
|
| 671 |
self.set_xy(8, 4)
|
| 672 |
-
self.cell(100, 7,
|
| 673 |
|
| 674 |
self.set_font('Helvetica', '', 7)
|
| 675 |
self.set_text_color(80, 120, 160)
|
| 676 |
self.set_xy(8, 13)
|
| 677 |
-
self.cell(0, 5,
|
| 678 |
f'AI-Assisted Energy Usage Analyzer | '
|
| 679 |
-
f'Generated: {datetime.now():%Y-%m-%d %H:%M:%S}')
|
| 680 |
self.ln(14)
|
| 681 |
|
| 682 |
def footer(self):
|
|
@@ -684,7 +698,7 @@ with tab4:
|
|
| 684 |
self.set_font('Helvetica', 'I', 7)
|
| 685 |
self.set_text_color(50, 70, 100)
|
| 686 |
self.cell(0, 8,
|
| 687 |
-
|
| 688 |
align='C')
|
| 689 |
|
| 690 |
def section_title(self, txt):
|
|
@@ -694,7 +708,7 @@ with tab4:
|
|
| 694 |
self.rect(self.get_x(), self.get_y(), 185, 8, 'DF')
|
| 695 |
self.set_font('Helvetica', 'B', 9)
|
| 696 |
self.set_text_color(0, 200, 255)
|
| 697 |
-
self.cell(0, 8,
|
| 698 |
self.ln(2)
|
| 699 |
|
| 700 |
def kv_row(self, label, value, fill_idx):
|
|
@@ -704,10 +718,10 @@ with tab4:
|
|
| 704 |
self.set_fill_color(10, 16, 28)
|
| 705 |
self.set_text_color(100, 140, 180)
|
| 706 |
self.set_font('Helvetica', '', 9)
|
| 707 |
-
self.cell(90, 7,
|
| 708 |
self.set_text_color(220, 230, 240)
|
| 709 |
self.set_font('Helvetica', 'B', 9)
|
| 710 |
-
self.cell(95, 7,
|
| 711 |
|
| 712 |
pdf = EnergyPDF()
|
| 713 |
pdf.set_auto_page_break(auto=True, margin=18)
|
|
@@ -716,16 +730,16 @@ with tab4:
|
|
| 716 |
# Title block
|
| 717 |
pdf.set_font('Helvetica', 'B', 17)
|
| 718 |
pdf.set_text_color(0, 200, 255)
|
| 719 |
-
pdf.cell(0, 10,
|
| 720 |
pdf.ln(1)
|
| 721 |
|
| 722 |
pdf.set_font('Helvetica', '', 9)
|
| 723 |
pdf.set_text_color(80, 120, 160)
|
| 724 |
-
pdf.cell(0, 6,
|
| 725 |
-
pdf.cell(0, 6,
|
| 726 |
if operator:
|
| 727 |
-
pdf.cell(0, 6,
|
| 728 |
-
pdf.cell(0, 6,
|
| 729 |
pdf.ln(4)
|
| 730 |
|
| 731 |
# Divider
|
|
@@ -744,7 +758,7 @@ with tab4:
|
|
| 744 |
("Minimum Power", f"{min_p:.2f} W"),
|
| 745 |
("Total Energy Consumed", f"{tot_e:.6f} kWh"),
|
| 746 |
("Electricity Bill", f"PKR {tot_b:.4f}"),
|
| 747 |
-
("Carbon Footprint", f"{tot_co2:.6f} kg
|
| 748 |
("Tariff Rate", f"PKR {rate:.2f} / kWh"),
|
| 749 |
("Carbon Factor", f"{carbon:.2f} kg COβ / kWh"),
|
| 750 |
("Total Readings", f"{n_reads}"),
|
|
@@ -788,7 +802,7 @@ with tab4:
|
|
| 788 |
# ββ Section 3: Last 20 readings ββββββββββββββ
|
| 789 |
pdf.section_title('3. RECENT READINGS (last 20)')
|
| 790 |
hdrs = ['Time', 'V (V)', 'I (A)', 'P (W)',
|
| 791 |
-
'kWh', 'Bill
|
| 792 |
c_widths = [26, 22, 22, 25, 32, 30, 28]
|
| 793 |
|
| 794 |
# Table header
|
|
@@ -796,7 +810,7 @@ with tab4:
|
|
| 796 |
pdf.set_text_color(0, 200, 255)
|
| 797 |
pdf.set_font('Helvetica', 'B', 8)
|
| 798 |
for h, w in zip(hdrs, c_widths):
|
| 799 |
-
pdf.cell(w, 7,
|
| 800 |
pdf.ln()
|
| 801 |
|
| 802 |
pdf.set_font('Helvetica', '', 8)
|
|
@@ -814,7 +828,7 @@ with tab4:
|
|
| 814 |
f"{row['bill_pkr']:.4f}",
|
| 815 |
f"{row['carbon_kg']:.6f}"]
|
| 816 |
for v, w in zip(vals, c_widths):
|
| 817 |
-
pdf.cell(w, 6,
|
| 818 |
pdf.ln()
|
| 819 |
pdf.ln(4)
|
| 820 |
|
|
@@ -822,10 +836,10 @@ with tab4:
|
|
| 822 |
pdf.section_title('4. AI ENERGY RECOMMENDATIONS')
|
| 823 |
pdf.set_font('Helvetica', '', 9)
|
| 824 |
for i, rec in enumerate(recs):
|
| 825 |
-
icon = '
|
| 826 |
-
clr = (255, 180, 60) if icon == '
|
| 827 |
pdf.set_text_color(*clr)
|
| 828 |
-
pdf.cell(0, 8,
|
| 829 |
pdf.ln(3)
|
| 830 |
|
| 831 |
# ββ Notes ββββββββββββββββββββββββββββββββββββ
|
|
@@ -839,7 +853,7 @@ with tab4:
|
|
| 839 |
pdf.set_font('Helvetica', '', 8)
|
| 840 |
pdf.set_text_color(140, 160, 180)
|
| 841 |
for ln_text in notes.split('\n'):
|
| 842 |
-
pdf.cell(0, 6,
|
| 843 |
|
| 844 |
# ββ Output βββββββββββββββββββββββββββββββββββ
|
| 845 |
pdf_bytes = bytes(pdf.output())
|
|
@@ -867,4 +881,4 @@ with tab4:
|
|
| 867 |
# ββ Auto refresh (live updates) ββββββββββββββββββββββββββββββ
|
| 868 |
if st.session_state.demo_mode or st.session_state.connected:
|
| 869 |
time.sleep(1)
|
| 870 |
-
st.rerun()
|
|
|
|
| 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)
|
|
|
|
| 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
|
|
|
|
| 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',
|
|
|
|
| 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()
|
|
|
|
| 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:00β06: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):
|
|
|
|
| 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):
|
|
|
|
| 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):
|
|
|
|
| 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):
|
|
|
|
| 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)
|
|
|
|
| 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
|
|
|
|
| 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}"),
|
|
|
|
| 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
|
|
|
|
| 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)
|
|
|
|
| 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 |
|
|
|
|
| 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 ββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 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())
|
|
|
|
| 881 |
# ββ Auto refresh (live updates) ββββββββββββββββββββββββββββββ
|
| 882 |
if st.session_state.demo_mode or st.session_state.connected:
|
| 883 |
time.sleep(1)
|
| 884 |
+
st.rerun()
|