Compare commits

..

62 Commits

Author SHA1 Message Date
e90e56c3f8 fix: Update configuration instructions to reflect changes in secrets management and file structure 2025-11-28 10:20:49 -05:00
5cfa36e558 fix: Remove test_send.py script to clean up unused code 2025-11-17 17:16:30 -05:00
c8102e62ee fix: Refactor main loop for graceful shutdown and improved error handling 2025-11-15 14:11:20 -05:00
d76b11430c fix: Remove redundant garbage collection calls in send_discord_message function 2025-11-15 13:54:08 -05:00
cb274545a3 fix: Remove unused variable 'schedules' and optimize garbage collection in schedule handling 2025-11-15 13:53:09 -05:00
6cd1349633 fix: Remove unused variables and trigger garbage collection in schedule handling 2025-11-15 13:49:20 -05:00
bcecf2a81a fix: Add garbage collection calls to optimize memory usage in web server operations 2025-11-15 13:40:41 -05:00
621a48f011 fix: Adjust memory threshold for Discord message sending to enhance reliability 2025-11-15 13:15:45 -05:00
ce816af9e7 fix: Adjust memory threshold for Discord message sending and improve error logging 2025-11-15 13:10:48 -05:00
519cb25038 refactor: Optimize import statements and restructure monitoring logic for improved performance 2025-11-15 13:10:41 -05:00
f81d89980b refactor: Remove debug print statements from POST request handling in TempWebServer 2025-11-15 13:10:32 -05:00
7fc7661dad fix: Adjust memory threshold for Discord message sending and add debug logging for RAM usage 2025-11-15 12:02:50 -05:00
3b7982a3a3 fix: Adjust memory threshold for Discord message sending to improve reliability 2025-11-15 11:58:32 -05:00
697f0bf31e fix: Improve Discord message sending logic and memory management 2025-11-15 11:52:54 -05:00
b632a76d5a refactor: Remove debug_force_send function to streamline message sending process 2025-11-15 11:22:36 -05:00
d670067b89 fix: Optimize memory management in debug_force_send and send_discord_message functions 2025-11-15 10:32:04 -05:00
ac860207d9 fix: Increase memory thresholds for Discord message sending and adjust garbage collection logging 2025-11-15 10:27:26 -05:00
03b26b5339 feat: Add debug_force_send function for memory tracking and testing 2025-11-15 10:19:49 -05:00
5a8d14eb4d fix: Enable debug logging in send_discord_message for better memory tracking 2025-11-15 10:03:15 -05:00
79445bf879 fix: Add debug logging to send_discord_message for memory checks and import impact 2025-11-15 09:58:09 -05:00
4400fb5a74 fix: Adjust memory thresholds for Discord message sending to match device capabilities 2025-11-15 09:46:22 -05:00
c6f46e097b fix: Increase memory thresholds and backoff duration for Discord message sending 2025-11-15 09:42:16 -05:00
d2c0f68488 fix: Enhance Discord message sending with memory checks and scheduling 2025-11-15 09:36:44 -05:00
13e3a56fa6 fix: Add low-memory guard and cooldown for Discord message sending
This isn't quite the fix though just want to save my position till tomorrow and see what changes come up before and after
2025-11-14 21:48:19 -05:00
efea4a1384 fix: Enhance Discord message sending with aggressive GC and low-memory guard 2025-11-14 21:28:10 -05:00
73b5a5aefe fix: Improve HTTP response handling and clarify default values in schedule configuration 2025-11-14 21:18:20 -05:00
03766d6b09 fix: Improve HTTP response handling and add schedule JavaScript support 2025-11-14 21:13:44 -05:00
e5f9331d30 fix: Clarify logic for matching AC and heater adjustments in synchronization 2025-11-14 20:49:51 -05:00
6128e585b8 fix: Improve error handling in web server request processing 2025-11-14 20:47:42 -05:00
81174b78e4 fix: Enhance live synchronization logic for heater and AC inputs with last changed tracking 2025-11-14 20:38:48 -05:00
70cc2cad81 fix: Refactor live synchronization logic for heater and AC inputs in schedule form 2025-11-14 20:37:06 -05:00
6bc7b1da93 fix: Implement live synchronization for heater and AC inputs in schedule form 2025-11-14 20:29:12 -05:00
eceee9c88d syncs while typing and guarantees posted values follow the rule 2025-11-14 20:19:13 -05:00
72eb3c2acf fix: Enhance schedule synchronization logic for heater and AC targets 2025-11-14 19:42:21 -05:00
eff69cfe52 fix: Implement auto-sync for heater and AC targets in scheduling and settings
Fixes #17
2025-11-14 18:17:17 -05:00
63588ee3f1 Merge branch 'main' of https://gitea.rcs1.top/sickprodigy/Auto-Garden 2025-11-14 17:19:52 -05:00
8363406647 fix: Move discord_webhook import to after config loading and update WiFi connection comment. Save on ram usage 2025-11-14 17:19:44 -05:00
df08692726 fix: Add type ignore comments for import errors 2025-11-14 17:19:44 -05:00
0030e0a932 fix: Add type ignore comments for imports in multiple scripts to improve compatibility 2025-11-14 17:19:43 -05:00
d95f212d2e Add example configuration file, moved everything from secrets.py to here.
Feat: Also refactored some of the logic in discord_webhook.py and networking.py to be more friendly towards the pico with ram usage.

Fixes #26
2025-11-14 17:18:17 -05:00
0f7c4cc4d7 fix: Move discord_webhook import to after config loading and update WiFi connection comment. Save on ram usage 2025-11-14 17:04:47 -05:00
a9641947ba fix: Add type ignore comments for import errors 2025-11-14 17:02:19 -05:00
63ff2cec77 fix: Add type ignore comments for imports in multiple scripts to improve compatibility 2025-11-14 16:53:51 -05:00
6890d0570e Add example configuration file, moved everything from secrets.py to here.
Feat: Also refactored some of the logic in discord_webhook.py and networking.py to be more friendly towards the pico with ram usage.
2025-11-14 16:50:53 -05:00
a20bbd7cdf Ignore config.json 2025-11-14 15:57:53 -05:00
7edd209abe start of moving secrets.py to config.json 2025-11-14 15:55:36 -05:00
2c39ebd985 feat: Update TemperatureMonitor to send alerts via Discord with improved messaging function 2025-11-11 17:13:34 -05:00
1016e96b58 feat: Add static IP configuration options to config and main files. Also remove creation of config from web_server.py because I was already doing it in main.py like it should be done, somewhere first.
Fixes #25
2025-11-11 16:55:27 -05:00
b3c56864ac update: clean up code formatting 2025-11-10 18:59:18 -05:00
95e159ee5d feat: Update README with recent enhancements including immediate schedule application, aggressive memory management, and improved config persistence 2025-11-09 12:43:37 -05:00
5da44e1397 feat: Enhance schedule application by saving updated config to file and ensuring target persistence 2025-11-09 12:43:32 -05:00
b346be9431 feat: Implement immediate application of active schedule on startup and enhance schedule resume handling
Fixes #24
2025-11-09 12:25:16 -05:00
229bde85e9 feat: Add temperature validation in TemperatureMonitor and implement aggressive garbage collection in main loop 2025-11-09 11:54:12 -05:00
dae6971112 feat: Implement NTP sync function with garbage collection and improve schedule handling in web server
reduce ram usage bascically.
Fixes #22 (more garbage collection all it needed, but went further to try and cut more memory usage)
Fixes #21 (Just forgot to already marke this one as completed. Possibly in this commit too)
2025-11-09 11:29:18 -05:00
3c2e936d56 feat: Add advanced settings page and update handling for temperature hold configurations
Fixes #20

didn't rename it that way but have done quite a bit already want to save working point
2025-11-09 10:48:49 -05:00
9da21f7c89 feat: Implement temporary hold management with config integration and improved notifications
fixes #19
2025-11-09 10:11:00 -05:00
b6aae121bb feat: Add dynamic config reload and timezone offset handling in web server
Fixes #18
Already Fixed #13
after long hours of tedious back and forth coding to figure out wtf was happening jesus
2025-11-09 09:24:21 -05:00
24b53b9446 Removed stuff from the try block so it doesn't loop. just initialize in the beginning 2025-11-09 01:18:16 -05:00
749eb956a5 feat: Enhance request handling with improved header parsing and error management
Fixes #16
Fixes #15
Fixes #14
Fixes #6
2025-11-09 00:54:07 -05:00
5ce7cd43a4 feat: Improve HTTP response handling in web server with proper headers 2025-11-08 19:28:11 -05:00
bb46a69eba feat: Update Discord message function with improved comments and error handling 2025-11-08 19:28:06 -05:00
b018b427f6 feat: Enhance NTP synchronization with timeout and error handling 2025-11-08 19:28:00 -05:00
13 changed files with 1460 additions and 543 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/__pycache__/
secrets.py
config.json
/pymakr-test/
.gitignore
.vscode/

View File

@@ -2,9 +2,14 @@
> Automated climate control system using Raspberry Pi Pico W with web interface and scheduling
## Overview
## Recent Updates
This project provides automated climate monitoring and control using a Raspberry Pi Pico W. Features dual-zone temperature monitoring, AC/heater control, time-based scheduling, and a web interface for easy management.
- 🆕 **Immediate schedule application:** When resuming scheduling from hold mode, the system now instantly applies the current schedule targets (no delay).
- 🆕 **Aggressive memory management:** Garbage collection runs every 5 seconds for improved reliability.
- 🆕 **Manual hold settings:** `ac_target` and `heater_target` in `config.json` now only store your last manual hold settings, not schedule targets.
- 🆕 **NTP sync optimization:** NTP modules are loaded only when needed, saving RAM.
- 🆕 **Temperature validation:** Impossible sensor readings are ignored for safety.
- 🆕 **Improved config persistence:** All changes are saved and reloaded immediately.
## Features
@@ -17,6 +22,7 @@ This project provides automated climate monitoring and control using a Raspberry
- ✅ Configurable alert thresholds
- ✅ Exception recovery (system won't crash permanently)
- ✅ Graceful shutdown with Ctrl+C
-**Aggressive garbage collection for stability**
- **Climate Control**
- ✅ Automated AC control with temperature swing logic
@@ -24,6 +30,7 @@ This project provides automated climate monitoring and control using a Raspberry
- ✅ Short-cycle protection for both AC and heater
- ✅ Dual relay control via opto-coupler for 110V AC
- ✅ Mutual exclusion (AC and heater never run simultaneously)
-**Manual hold settings are preserved and not overwritten by schedules**
- **Scheduling System**
- ✅ 4 configurable time-based schedules per day
@@ -33,6 +40,7 @@ This project provides automated climate monitoring and control using a Raspberry
- ✅ Permanent hold mode (manual control until restart)
- ✅ Schedule configuration persists through reboots
- ✅ Hold modes reset to Automatic on restart (safety feature)
-**Immediate schedule application after resuming from hold**
- **Web Interface**
- ✅ Real-time temperature display
@@ -43,11 +51,23 @@ This project provides automated climate monitoring and control using a Raspberry
- ✅ Countdown timer for temporary holds
- ✅ Mobile-responsive design
- ✅ Auto-refresh dashboard (30 seconds)
-**Settings and schedule changes are reflected instantly**
- **Planned Features**
- 🚧 Humidity monitoring (DHT22/SHT31)
- 🚧 Soil moisture monitoring
- 🚧 Additional relay control for fans, grow lights
## Configuration Notes
- **AC/Heater target settings:**
- `ac_target` and `heater_target` in `config.json` are updated whenever you use Temp Hold, Perm Hold, or when a schedule is applied.
- When schedules are active, these values are updated to match the current schedules targets.
- This ensures the config file always reflects the current operating temperatures, whether in hold mode or schedule mode.
- **Immediate schedule application:**
- When you click "Resume Scheduling," the system applies the current schedule targets instantly, so the dashboard updates without delay.
- **Memory management:**
- Garbage collection runs every 5 seconds to prevent memory fragmentation and crashes.
- **Sensor validation:**
- Temperatures outside the range -50°F to 150°F are ignored to prevent false readings.
## Quick Start
@@ -126,56 +146,28 @@ RUN pin → Button → GND
### 4. Configuration
**Create `secrets.py`** (copy from `secrets.example.py`):
**Edit `config.json`** (created automatically on first boot, or edit manually):
```python
secrets = {
'ssid': 'YOUR_WIFI_NAME',
'password': 'YOUR_WIFI_PASSWORD',
'discord_webhook_url': 'https://discord.com/api/webhooks/...',
'discord_alert_webhook_url': 'https://discord.com/api/webhooks/...',
}
```
**Sensor Configuration in `main.py`:**
```python
# Sensor configuration
SENSOR_CONFIG = {
'inside': {
'pin': 10,
'label': 'Inside',
'alert_high': 80.0,
'alert_low': 70.0
},
'outside': {
'pin': 11,
'label': 'Outside',
'alert_high': 85.0,
'alert_low': 68.0
}
}
```
**Default Climate Settings (auto-saved to config.json):**
```python
# Default config (created on first boot)
```json
{
"ac_target": 77.0, # AC target temperature (°F)
"ac_swing": 1.0, # AC turns on at 78°F, off at 76°F
"heater_target": 72.0, # Heater target temperature (°F)
"heater_swing": 2.0, # Heater turns on at 70°F, off at 74°F
"temp_hold_duration": 3600, # Temporary hold lasts 1 hour (3600 seconds)
"schedule_enabled": true, # Schedules active by default
"schedules": [ # 4 time-based schedules
"ssid": "YOUR_WIFI_NAME",
"password": "YOUR_WIFI_PASSWORD",
"discord_webhook_url": "https://discord.com/api/webhooks/...",
"discord_alert_webhook_url": "https://discord.com/api/webhooks/...",
"ac_target": 77.0,
"ac_swing": 1.0,
"heater_target": 72.0,
"heater_swing": 2.0,
"temp_hold_duration": 3600,
"schedule_enabled": true,
"schedules": [
{
"time": "06:00",
"name": "Morning",
"ac_target": 75.0,
"heater_target": 72.0
},
# ... 3 more schedules
}
// ... 3 more schedules
]
}
```
@@ -189,7 +181,6 @@ Upload all files to your Pico:
```text
/
├── main.py
├── secrets.py
├── config.json # Auto-generated on first boot
└── scripts/
├── air_conditioning.py # AC/Heater controller classes
@@ -208,9 +199,7 @@ The Pico will auto-start `main.py` on boot and be accessible at **<http://192.16
```text
Auto-Garden/
├── main.py # Entry point, configuration, system initialization
├── secrets.py # WiFi & Discord credentials (gitignored)
├── secrets.example.py # Template for secrets.py
├── config.json # Persistent configuration (auto-generated)
├── config.json # Persistent configuration and credentials (auto-generated)
└── scripts/
├── air_conditioning.py # AC & Heater controllers with short-cycle protection
├── discord_webhook.py # Discord notification handling
@@ -427,14 +416,14 @@ report_interval=30 # Discord report frequency
**WiFi not connecting:**
- Verify SSID/password in `secrets.py`
- Verify SSID/password in `config.json`
- Check 2.4GHz WiFi (Pico W doesn't support 5GHz)
- LED should be solid when connected
- Check serial console for connection status
**Discord messages not sending:**
- Verify webhook URLs in `secrets.py`
- Verify webhook URLs in `config.json`
- Test webhooks with curl/Postman first
- Check Pico has internet access (ping test)
- Look for error messages in serial console

View File

@@ -1,5 +1,5 @@
from machine import Pin
import time
from machine import Pin # type: ignore
import time # type: ignore
class ACController:
"""Control AC unit via opto-coupler relay."""

View File

@@ -1,8 +1,25 @@
import urequests as requests
from secrets import secrets
# Minimal module-level state (only what we need)
_CONFIG = {"discord_webhook_url": None, "discord_alert_webhook_url": None}
# Cooldown after low-memory failures (epoch seconds)
_NEXT_ALLOWED_SEND_TS = 0
def set_config(cfg: dict):
"""Initialize module with minimal values from loaded config (call from main)."""
global _CONFIG
if not cfg:
_CONFIG = {"discord_webhook_url": None, "discord_alert_webhook_url": None}
return
_CONFIG = {
"discord_webhook_url": cfg.get("discord_webhook_url"),
"discord_alert_webhook_url": cfg.get("discord_alert_webhook_url"),
}
def _get_webhook_url(is_alert: bool = False):
if is_alert:
return _CONFIG.get("discord_alert_webhook_url") or _CONFIG.get("discord_webhook_url")
return _CONFIG.get("discord_webhook_url")
def _escape_json_str(s: str) -> str:
# minimal JSON string escaper for quotes/backslashes and control chars
s = s.replace("\\", "\\\\")
s = s.replace('"', '\\"')
s = s.replace("\n", "\\n")
@@ -10,46 +27,110 @@ def _escape_json_str(s: str) -> str:
s = s.replace("\t", "\\t")
return s
def send_discord_message(message, username="Auto Garden Bot", is_alert=False):
def send_discord_message(message, username="Auto Garden Bot", is_alert=False, debug: bool = False):
"""
Send Discord message with aggressive GC and low-memory guard to avoid ENOMEM.
When debug=True prints mem_free at important steps so you can see peak usage.
Returns True on success, False otherwise.
"""
global _NEXT_ALLOWED_SEND_TS
resp = None
# Use alert webhook if specified, otherwise normal webhook
if is_alert:
url = secrets.get('discord_alert_webhook_url') or secrets.get('discord_webhook_url')
else:
url = secrets.get('discord_webhook_url')
try:
url = _get_webhook_url(is_alert=is_alert)
if not url:
# print("DEBUG: no webhook URL in secrets")
if debug: print("DBG: no webhook URL configured")
return False
url = url.strip().strip('\'"')
# Respect cooldown if we recently saw ENOMEM
try:
import time # type: ignore
now = time.time()
if _NEXT_ALLOWED_SEND_TS and now < _NEXT_ALLOWED_SEND_TS:
if debug: print("DBG: backing off until", _NEXT_ALLOWED_SEND_TS)
return False
except:
pass
# build JSON by hand so emoji (and other unicode) are preserved as UTF-8 bytes
content = _escape_json_str(message)
user = _escape_json_str(username)
try:
# Lightweight local imports and GC
import gc # type: ignore
import time # type: ignore
gc.collect()
# Quick mem check before importing urequests/SSL
mem = getattr(gc, "mem_free", lambda: None)()
# Require larger headroom based on device testing (adjust if you re-test)
if mem is not None and mem < 95000:
print("Discord send skipped: ENOMEM ({} bytes free)".format(mem))
return False
# Import urequests only when we plan to send
try:
import urequests as requests # type: ignore
except Exception as e:
print("Discord send failed: urequests import error:", e)
try:
_NEXT_ALLOWED_SEND_TS = time.time() + 60
except:
pass
return False
gc.collect()
if debug:
try: print("DBG: mem after import:", gc.mem_free() // 1024, "KB")
except: pass
# Build tiny payload
url = str(url).strip().strip('\'"')
content = _escape_json_str(str(message)[:140])
user = _escape_json_str(str(username)[:32])
body_bytes = ('{"content":"%s","username":"%s"}' % (content, user)).encode("utf-8")
headers = {"Content-Type": "application/json; charset=utf-8"}
headers = {"Content-Type": "application/json"}
resp = requests.post(url, data=body_bytes, headers=headers)
status = getattr(resp, "status", getattr(resp, "status_code", None))
if status and 200 <= status < 300:
# print("Discord message sent")
return True
else:
# print(f"Discord webhook failed with status {status}")
return False
return bool(status and 200 <= status < 300)
except Exception as e:
# print("Failed to send Discord message:", e)
return False
finally:
if resp:
print("Discord send failed:", e)
try:
if ("ENOMEM" in str(e)) or isinstance(e, MemoryError):
import time # type: ignore
_NEXT_ALLOWED_SEND_TS = time.time() + 60
except:
pass
return False
finally:
try:
if resp:
resp.close()
except:
pass
# remove local refs and unload heavy modules to free peak RAM (urequests, ussl/ssl)
try:
if 'resp' in locals(): del resp
if 'body_bytes' in locals(): del body_bytes
if 'content' in locals(): del content
if 'user' in locals(): del user
if 'headers' in locals(): del headers
if 'requests' in locals(): del requests
except:
pass
try:
import sys
# remove urequests and SSL modules from module cache so their memory can be reclaimed
for m in ('urequests', 'ussl', 'ssl'):
if m in sys.modules:
try:
del sys.modules[m]
except:
pass
except:
pass
try:
import gc # type: ignore
gc.collect()
except:
pass

View File

@@ -1,5 +1,5 @@
from machine import Pin
import time
from machine import Pin # type: ignore
import time # type: ignore
class HeaterController:
"""Control heater via opto-coupler relay."""

View File

@@ -1,5 +1,5 @@
import time # type: ignore
from scripts.discord_webhook import send_discord_message
import scripts.discord_webhook as discord_webhook
from scripts.temperature_sensor import TemperatureSensor
class Monitor:
@@ -77,6 +77,12 @@ class TemperatureMonitor(Monitor):
temp = list(temps.values())[0] # Get first temp reading
# ===== ADD THIS: Validate temperature is reasonable =====
if temp < -50 or temp > 150: # Sanity check (outside normal range)
print("⚠️ Warning: {} sensor returned invalid temp: {:.1f}°F".format(self.label, temp))
return # Don't cache invalid reading
# ===== END: Validation =====
# Cache the reading for web server (avoid blocking reads)
self.last_temp = temp
self.last_read_time = current_time
@@ -102,13 +108,11 @@ class TemperatureMonitor(Monitor):
self.alert_start_time = current_time
print(alert_message)
# Send to appropriate Discord channel
# send alert (use module-level discord_webhook; set_config must be called in main)
if self.send_alerts_to_separate_channel:
from scripts.discord_webhook import send_alert_message
send_alert_message(alert_message)
discord_webhook.send_discord_message(alert_message, is_alert=True)
else:
from scripts.discord_webhook import send_discord_message
send_discord_message(alert_message)
discord_webhook.send_discord_message(alert_message)
self.alert_sent = True
@@ -133,13 +137,11 @@ class TemperatureMonitor(Monitor):
)
print(recovery_message)
# Send to appropriate Discord channel
# send recovery message
if self.send_alerts_to_separate_channel:
from scripts.discord_webhook import send_alert_message
send_alert_message(recovery_message)
discord_webhook.send_discord_message(recovery_message, is_alert=True)
else:
from scripts.discord_webhook import send_discord_message
send_discord_message(recovery_message)
discord_webhook.send_discord_message(recovery_message)
self.alert_sent = False
self.alert_start_time = None
@@ -192,14 +194,14 @@ class ACMonitor(Monitor):
# Too hot, turn AC on
if self.ac.turn_on():
if not self.last_notified_state:
send_discord_message(f"❄️ AC turned ON - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
discord_webhook.send_discord_message(f"❄️ AC turned ON - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
self.last_notified_state = True
elif current_temp < (self.target_temp - self.temp_swing):
# Cool enough, turn AC off
if self.ac.turn_off():
if self.last_notified_state:
send_discord_message(f"✅ AC turned OFF - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
discord_webhook.send_discord_message(f"✅ AC turned OFF - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
self.last_notified_state = False
# Else: within temp_swing range, maintain current state
@@ -238,27 +240,28 @@ class HeaterMonitor(Monitor):
# Too cold, turn heater on
if self.heater.turn_on():
if not self.last_notified_state:
send_discord_message(f"🔥 Heater turned ON - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
discord_webhook.send_discord_message(f"🔥 Heater turned ON - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
self.last_notified_state = True
elif current_temp > (self.target_temp + self.temp_swing):
# Warm enough, turn heater off
if self.heater.turn_off():
if self.last_notified_state:
send_discord_message(f"✅ Heater turned OFF - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
discord_webhook.send_discord_message(f"✅ Heater turned OFF - Current: {current_temp:.1f}°F, Target: {self.target_temp:.1f}°F")
self.last_notified_state = False
# Else: within temp_swing range, maintain current state
class WiFiMonitor(Monitor):
"""Monitor WiFi connection and handle reconnection."""
def __init__(self, wifi, led, interval=5, reconnect_cooldown=60):
def __init__(self, wifi, led, interval=5, reconnect_cooldown=60, config=None):
super().__init__(interval)
self.wifi = wifi
self.led = led
self.reconnect_cooldown = reconnect_cooldown
self.last_reconnect_attempt = 0
self.was_connected = wifi.isconnected() if wifi else False
self.config = config
def run(self):
"""Check WiFi status, blink LED, attempt reconnect if needed."""
@@ -278,10 +281,10 @@ class WiFiMonitor(Monitor):
if time.ticks_diff(now, self.last_reconnect_attempt) >= (self.reconnect_cooldown * 1000):
self.last_reconnect_attempt = now
# print("Attempting WiFi reconnect...")
self.wifi = connect_wifi(self.led)
self.wifi = connect_wifi(self.led, config=self.config)
if self.wifi and self.wifi.isconnected():
send_discord_message("WiFi connection restored 🔄")
discord_webhook.send_discord_message("WiFi connection restored 🔄")
self.was_connected = True
else:
# Slow blink when connected
@@ -291,7 +294,7 @@ class WiFiMonitor(Monitor):
# Notify if connection was just restored
if not self.was_connected:
send_discord_message("WiFi connection restored 🔄")
discord_webhook.send_discord_message("WiFi connection restored 🔄")
self.was_connected = True
def run_monitors(monitors):

View File

@@ -1,19 +1,32 @@
import network
import time
from secrets import secrets
import network # type: ignore
import time # type: ignore
def connect_wifi(led=None, max_retries=3, timeout=20):
def connect_wifi(led=None, max_retries=3, timeout=20, config=None):
"""
Connect to WiFi using credentials from secrets.py
Connect to WiFi using credentials from provided config dict.
Args:
led: Optional LED pin for visual feedback
max_retries: Number of connection attempts (default: 3)
timeout: Seconds to wait for connection per attempt (default: 20)
config: Dict loaded from config.json, must contain config['wifi'] with 'ssid' and 'password'
Returns:
WLAN object if connected, None if failed
"""
if config is None:
print("connect_wifi: config is required")
return None
wifi_cfg = config.get('wifi') or {}
# support either config['wifi'] = {'ssid','password'} OR top-level 'ssid'/'password'
ssid = wifi_cfg.get('ssid') or config.get('ssid')
password = wifi_cfg.get('password') or config.get('password')
if not ssid or not password:
print("connect_wifi: missing wifi credentials in config['wifi']")
return None
wlan = network.WLAN(network.STA_IF)
# Ensure clean state
@@ -41,13 +54,13 @@ def connect_wifi(led=None, max_retries=3, timeout=20):
# Try connecting with retries
for attempt in range(1, max_retries + 1):
if wlan.isconnected():
print(f"Already connected to WiFi")
print("Already connected to WiFi")
break
print(f'Connecting to WiFi (attempt {attempt}/{max_retries})...')
print(f'Connecting to WiFi SSID: {ssid} (attempt {attempt}/{max_retries})...')
try:
wlan.connect(secrets['ssid'], secrets['password'])
wlan.connect(ssid, password)
except Exception as e:
print(f"Connection attempt failed: {e}")
if attempt < max_retries:
@@ -62,7 +75,17 @@ def connect_wifi(led=None, max_retries=3, timeout=20):
break
if led:
try:
# some LED wrappers use toggle(), others use on/off
if hasattr(led, "toggle"):
led.toggle()
else:
# flash quickly to show activity
led.on()
time.sleep(0.05)
led.off()
except Exception:
pass
time.sleep(0.5)
wait_time += 0.5
@@ -85,17 +108,24 @@ def connect_wifi(led=None, max_retries=3, timeout=20):
if not wlan.isconnected():
print('WiFi connection failed after all attempts!')
if led:
try:
# prefer available method names
if hasattr(led, "off"):
led.off()
except Exception:
pass
return None
# Success feedback
if led:
# Double pulse on successful connection
try:
for _ in range(2):
led.on()
time.sleep(0.2)
led.off()
time.sleep(0.2)
except Exception:
pass
print('Connected to WiFi successfully!')

View File

@@ -20,7 +20,6 @@ class ScheduleMonitor:
self.last_check = 0
self.current_schedule = None
self.last_applied_schedule = None
self.temp_hold_start_time = None # When temporary hold was activated
self.temp_hold_duration = config.get('temp_hold_duration', 3600) # Use config value, default 1 hour
def should_run(self):
@@ -86,25 +85,66 @@ class ScheduleMonitor:
if not schedule:
return
# Check if this is a different schedule than last applied
schedule_id = schedule.get('time', '') + schedule.get('name', '')
if schedule_id == self.last_applied_schedule:
return # Already applied
try:
# Track whether we changed persisted values to avoid unnecessary writes
changed = False
# Update AC settings if provided
if 'ac_target' in schedule:
self.ac_monitor.target_temp = float(schedule['ac_target'])
new_ac = float(schedule['ac_target'])
if self.config.get('ac_target') != new_ac:
self.config['ac_target'] = new_ac
changed = True
self.ac_monitor.target_temp = new_ac
if 'ac_swing' in schedule:
self.ac_monitor.temp_swing = float(schedule['ac_swing'])
new_ac_swing = float(schedule['ac_swing'])
if self.config.get('ac_swing') != new_ac_swing:
self.config['ac_swing'] = new_ac_swing
changed = True
self.ac_monitor.temp_swing = new_ac_swing
# Update heater settings if provided
if 'heater_target' in schedule:
self.heater_monitor.target_temp = float(schedule['heater_target'])
new_ht = float(schedule['heater_target'])
if self.config.get('heater_target') != new_ht:
self.config['heater_target'] = new_ht
changed = True
self.heater_monitor.target_temp = new_ht
if 'heater_swing' in schedule:
self.heater_monitor.temp_swing = float(schedule['heater_swing'])
new_ht_swing = float(schedule['heater_swing'])
if self.config.get('heater_swing') != new_ht_swing:
self.config['heater_swing'] = new_ht_swing
changed = True
self.heater_monitor.temp_swing = new_ht_swing
# Save updated config only if something changed
if changed:
try:
import json
with open('config.json', 'w') as f:
json.dump(self.config, f)
except Exception as e:
print("⚠️ Could not save config: {}".format(e))
else:
# import once and update module-level webhook config
try:
import scripts.discord_webhook as discord_webhook
discord_webhook.set_config(self.config)
except Exception:
pass
print("✅ Config updated with active schedule targets")
else:
# nothing to persist
try:
import scripts.discord_webhook as discord_webhook
except Exception:
discord_webhook = None
# Log the change
schedule_name = schedule.get('name', 'Unnamed')
@@ -115,16 +155,17 @@ class ScheduleMonitor:
print("Heater Target: {}°F".format(self.heater_monitor.target_temp))
print("="*50 + "\n")
# Send Discord notification
# Send Discord notification (use discord_webhook if available)
try:
from scripts.discord_webhook import send_discord_message
if 'discord_webhook' not in locals() or discord_webhook is None:
import scripts.discord_webhook as discord_webhook
message = "🕐 Schedule '{}' applied - AC: {}°F | Heater: {}°F".format(
schedule_name,
self.ac_monitor.target_temp,
self.heater_monitor.target_temp
)
send_discord_message(message)
except:
discord_webhook.send_discord_message(message)
except Exception:
pass
self.last_applied_schedule = schedule_id
@@ -137,40 +178,41 @@ class ScheduleMonitor:
# ===== START: Check if temporary hold has expired =====
if not self.config.get('schedule_enabled', False) and not self.config.get('permanent_hold', False):
# We're in temporary hold mode
if self.temp_hold_start_time is None:
# Just entered hold mode, record start time
self.temp_hold_start_time = time.time()
print("⏸️ Temporary hold started - will auto-resume in {} minutes".format(
self.temp_hold_duration // 60
))
else:
# Check if hold has expired
elapsed = time.time() - self.temp_hold_start_time
# In temporary hold mode - check if timer expired
temp_hold_start = self.config.get('temp_hold_start_time') # <-- READ FROM CONFIG NOW
if temp_hold_start is not None:
elapsed = time.time() - temp_hold_start
if elapsed >= self.temp_hold_duration:
# Hold expired, resume schedules
print("⏰ Temporary hold expired - resuming automatic mode")
# Timer expired - resume automatic scheduling
print("⏰ Temporary hold expired - resuming schedule")
self.config['schedule_enabled'] = True
self.config['permanent_hold'] = False
self.temp_hold_start_time = None
self.config['temp_hold_start_time'] = None
# Save updated config
try:
import json
with open('config.json', 'w') as f:
json.dump(self.config, f)
except:
except Exception as e:
print("⚠️ Could not save config: {}".format(e))
else:
# ensure in-memory webhook config updated
try:
import scripts.discord_webhook as discord_webhook
discord_webhook.set_config(self.config)
except Exception:
pass
# Send Discord notification
print("✅ Config updated - automatic mode resumed")
# Notify user
try:
from scripts.discord_webhook import send_discord_message
send_discord_message("⏰ Temporary hold expired - Automatic mode resumed")
except:
import scripts.discord_webhook as discord_webhook
discord_webhook.send_discord_message("⏰ Temporary hold expired - Schedule resumed automatically")
except Exception:
pass
else:
# Not in temporary hold, reset timer
self.temp_hold_start_time = None
# ===== END: Check if temporary hold has expired =====
# Find and apply active schedule

View File

@@ -1,7 +1,7 @@
import machine
import onewire
import ds18x20
import time
import machine # type: ignore
import onewire # type: ignore
import ds18x20 # type: ignore
import time # type: ignore
class TemperatureSensor:
def __init__(self, pin=10, label=None):

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,13 @@
{
"static_ip": "192.168.1.69",
"subnet": "255.255.255.0",
"gateway": "192.168.1.1",
"dns": "192.168.1.1",
"timezone_offset": -5,
"ssid": " Change_to_wifi_SSID",
"password": "Change_to_wifi_Pasword",
"discord_webhook_url": "https://discord.com/api/webhooks/key/long-Combination_1234-ChangeMe",
"discord_alert_webhook_url": "https://discord.com/api/webhooks/key/long-Combination_1234-ChangeMe",
"ac_target": 77.0,
"ac_swing": 1.0,
"heater_target": 72.0,
@@ -31,5 +40,6 @@
}
],
"schedule_enabled": true,
"permanent_hold": false
"permanent_hold": false,
"temp_hold_start_time": null
}

338
main.py
View File

@@ -1,12 +1,9 @@
from machine import Pin, WDT # type: ignore
from machine import Pin, RTC # type: ignore
import time # type: ignore
import network # type: ignore
import json
import gc # type: ignore # ADD THIS - for garbage collection
# Enable watchdog (8 seconds timeout - auto-reboot if frozen)
# wdt = WDT(timeout=8000) # Maximum is 8388ms, use 8000ms (8 seconds)
# print("Watchdog enabled (8s timeout)")
import sys
# Initialize pins (LED light onboard)
led = Pin("LED", Pin.OUT)
@@ -24,14 +21,53 @@ except Exception as e:
# Import after WiFi reset
from scripts.networking import connect_wifi
from scripts.discord_webhook import send_discord_message
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, HeaterMonitor, run_monitors
from scripts.temperature_sensor import TemperatureSensor
from scripts.air_conditioning import ACController
from scripts.heating import HeaterController
from scripts.web_server import TempWebServer
from scripts.scheduler import ScheduleMonitor # NEW: Import scheduler for time-based temp changes
from scripts.memory_check import check_memory_once # Just the function
# ===== NEW: NTP Sync Function (imports locally) =====
def sync_ntp_time(timezone_offset):
"""
Sync time with NTP server (imports modules locally to save RAM).
Returns True if successful, False otherwise.
"""
try:
# Import ONLY when needed (freed by GC after function ends)
import socket # type: ignore
import struct # type: ignore
NTP_DELTA = 2208988800
host = "pool.ntp.org"
NTP_QUERY = bytearray(48)
NTP_QUERY[0] = 0x1B
# Create socket with timeout
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(3.0) # 3-second timeout
try:
addr = socket.getaddrinfo(host, 123)[0][-1]
s.sendto(NTP_QUERY, addr)
msg = s.recv(48)
val = struct.unpack("!I", msg[40:44])[0]
utc_timestamp = val - NTP_DELTA
# Apply timezone offset
local_timestamp = utc_timestamp + (timezone_offset * 3600)
# Set RTC with local time
tm = time.gmtime(local_timestamp)
RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
return True
finally:
s.close()
except Exception as e:
print("NTP sync failed: {}".format(e))
return False
finally:
# Force garbage collection to free socket/struct modules
gc.collect()
# ===== END: NTP Sync Function =====
# ===== START: Configuration Loading =====
# Load saved settings from config.json file on Pico
@@ -47,10 +83,17 @@ def load_config():
print("No saved config found, creating default config.json...")
default_config = {
'static_ip': '192.168.86.43',
'subnet': '255.255.255.0',
'gateway': '192.168.86.1',
'dns': '192.168.86.1',
'timezone_offset': -6, # Timezone offset from UTC (CST=-6, EST=-5, MST=-7, PST=-8, add 1 for DST)
'ac_target': 75.0, # Default AC target temp
'ac_swing': 1.0, # Default AC tolerance (+/- degrees)
'heater_target': 72.0, # Default heater target temp
'heater_swing': 2.0, # Default heater tolerance (+/- degrees)
'temp_hold_duration': 3600, # Default hold duration in seconds (1 hour)
'temp_hold_start_time': None, # No hold active at startup
'schedules': [ # Default 4 schedules
{
'time': '06:00',
@@ -93,8 +136,19 @@ def load_config():
return default_config
# global variables for Discord webhook status
discord_sent = False
discord_send_attempts = 0
pending_discord_message = None
# Load configuration from file
config = load_config()
import scripts.discord_webhook as discord_webhook
# Initialize discord webhook module with loaded config (must be done BEFORE any send_discord_message calls)
discord_webhook.set_config(config)
# Get timezone offset from config (with fallback to -6 if not present)
TIMEZONE_OFFSET = config.get('timezone_offset', -6)
# ===== START: Reset hold modes on startup =====
# Always reset to automatic mode on boot (don't persist hold states)
@@ -102,6 +156,8 @@ if 'schedule_enabled' in config:
config['schedule_enabled'] = True # Always enable schedules on boot
if 'permanent_hold' in config:
config['permanent_hold'] = False # Always clear permanent hold on boot
if 'temp_hold_start_time' in config:
config['temp_hold_start_time'] = None # Clear temp hold start time
# Save the reset config immediately
try:
@@ -114,16 +170,16 @@ except Exception as e:
# ===== END: Configuration Loading =====
# ===== START: WiFi Connection =====
# Connect to WiFi using credentials from secrets.py
wifi = connect_wifi(led)
# Connect to WiFi using credentials from config.json
wifi = connect_wifi(led, config=config)
# Set static IP and print WiFi details
if wifi and wifi.isconnected():
# Configure static IP (easier to bookmark web interface)
static_ip = '192.168.86.43' # Change this to match your network
subnet = '255.255.255.0'
gateway = '192.168.86.1' # Usually your router IP
dns = '192.168.86.1' # Usually your router IP
# Get static IP settings from config
static_ip = config.get('static_ip')
subnet = config.get('subnet')
gateway = config.get('gateway')
dns = config.get('dns')
# Apply static IP configuration
wifi.ifconfig((static_ip, subnet, gateway, dns))
@@ -141,23 +197,49 @@ if wifi and wifi.isconnected():
print(f"Web Interface: http://{ifconfig[0]}")
print("="*50 + "\n")
# Send startup notification to Discord
send_discord_message(f"Pico W online at http://{ifconfig[0]}")
# Try sending Discord webhook NOW, before creating other objects
gc.collect()
ram_free = gc.mem_free()
print(f"DEBUG: Free RAM before Discord send: {ram_free // 1024} KB")
mem_ok = ram_free > 95000
if mem_ok:
ok = discord_webhook.send_discord_message("Pico W online at http://{}".format(ifconfig[0]), debug=False)
if ok:
print("Discord startup notification sent")
discord_sent = True
else:
print("Discord startup notification failed")
pending_discord_message = "Pico W online at http://{}".format(ifconfig[0])
discord_send_attempts = 1
else:
print("Not enough memory for Discord startup notification")
pending_discord_message = "Pico W online at http://{}".format(ifconfig[0])
discord_send_attempts = 1
# ===== Moved to later so discord could fire off startup message hopefully =====
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, HeaterMonitor, run_monitors
from scripts.temperature_sensor import TemperatureSensor
from scripts.air_conditioning import ACController
from scripts.heating import HeaterController
from scripts.web_server import TempWebServer
from scripts.scheduler import ScheduleMonitor
from scripts.memory_check import check_memory_once
# Start web server early so page can load even if time sync is slow
web_server = TempWebServer(port=80)
web_server.start()
# Attempt time sync non-blocking (short timeout + retry flag)
# ===== INITIAL NTP SYNC (using function) =====
ntp_synced = False
try:
import ntptime # type: ignore
ntptime.settime()
ntp_synced = True
print("Time synced with NTP server")
ntp_synced = sync_ntp_time(TIMEZONE_OFFSET)
if ntp_synced:
print("Time synced with NTP server (UTC{:+d})".format(TIMEZONE_OFFSET))
else:
print("Initial NTP sync failed, will retry in background...")
except Exception as e:
print("Initial NTP sync failed: {}".format(e))
# Will retry later in loop
print("Initial NTP sync error: {}".format(e))
# ===== END: INITIAL NTP SYNC =====
else:
# WiFi connection failed
@@ -166,6 +248,8 @@ else:
print("="*50 + "\n")
# ===== END: WiFi Connection =====
# ===== START: Sensor Configuration =====
# Define all temperature sensors and their alert thresholds
SENSOR_CONFIG = {
@@ -239,6 +323,24 @@ schedule_monitor = ScheduleMonitor(
config=config, # Pass config with schedules
interval=60 # Check schedule every 60 seconds
)
# ===== APPLY ACTIVE SCHEDULE IMMEDIATELY ON STARTUP =====
if config.get('schedule_enabled', False):
try:
# Find and apply the current active schedule
active_schedule = schedule_monitor._find_active_schedule()
if active_schedule:
schedule_monitor._apply_schedule(active_schedule)
print("✅ Active schedule applied on startup: {}".format(
active_schedule.get('name', 'Unnamed')
))
else:
print(" No active schedule found (using manual targets)")
except Exception as e:
print("⚠️ Warning: Could not apply startup schedule: {}".format(e))
else:
print(" Schedules disabled - using manual targets")
# ===== END: APPLY ACTIVE SCHEDULE ON STARTUP =====
# ===== END: Schedule Monitor Setup =====
# ===== START: Print Current Settings =====
@@ -259,103 +361,127 @@ print("="*50 + "\n")
check_memory_once()
# ===== END: Startup Memory Check =====
# ===== START: Monitor Setup =====
# Set up all monitoring systems (run in order during main loop)
monitors = [
# WiFi monitor: Checks connection, reconnects if needed, blinks LED
WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60),
# Schedule monitor: Changes temp targets based on time of day
schedule_monitor,
# AC monitor: Automatically turns AC on/off based on temperature
ac_monitor,
# Heater monitor: Automatically turns heater on/off based on temperature
heater_monitor,
# Inside temperature monitor: Logs temps, sends alerts if out of range
TemperatureMonitor(
sensor=sensors['inside'],
label=SENSOR_CONFIG['inside']['label'],
check_interval=10, # Check temp every 10 seconds
report_interval=30, # Log to CSV every 30 seconds
alert_high=SENSOR_CONFIG['inside']['alert_high'], # High temp alert threshold
alert_low=SENSOR_CONFIG['inside']['alert_low'], # Low temp alert threshold
log_file="/temp_logs.csv", # CSV file path
send_alerts_to_separate_channel=True # Use separate Discord channel
),
# Outside temperature monitor: Logs temps, sends alerts if out of range
TemperatureMonitor(
sensor=sensors['outside'],
label=SENSOR_CONFIG['outside']['label'],
check_interval=10, # Check temp every 10 seconds
report_interval=30, # Log to CSV every 30 seconds
alert_high=SENSOR_CONFIG['outside']['alert_high'], # High temp alert threshold
alert_low=SENSOR_CONFIG['outside']['alert_low'], # Low temp alert threshold
log_file="/temp_logs.csv", # CSV file path
send_alerts_to_separate_channel=False # Use main Discord channel
),
]
# ===== END: Monitor Setup =====
print("Starting monitoring loop...")
print("Press Ctrl+C to stop\n")
# Add NTP retry flags (before main loop)
retry_ntp_attempts = 0
max_ntp_attempts = 5 # Try up to 5 times after initial failure
last_ntp_sync = time.time() # Track when we last synced
try:
while True:
# ===== START: Main Loop =====
# Main monitoring loop (runs forever until Ctrl+C)
while True:
# ===== START: Main Loop =====
# Main monitoring loop (runs forever until Ctrl+C)
last_monitor_run = {
"wifi": 0,
"schedule": 0,
"ac": 0,
"heater": 0,
"inside_temp": 0,
"outside_temp": 0,
}
while True:
now = time.time()
# WiFi monitor every 5 seconds (can be stateless)
if now - last_monitor_run["wifi"] >= 5:
from scripts.monitors import WiFiMonitor
wifi_monitor = WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60, config=config)
try:
# Run all monitors (each checks if it's time to run via should_run())
run_monitors(monitors)
wifi_monitor.run()
except Exception as e:
print("WiFiMonitor error:", e)
del wifi_monitor
gc.collect()
last_monitor_run["wifi"] = now
# Web requests
# Schedule monitor every 60 seconds (persistent)
if now - last_monitor_run["schedule"] >= 60:
try:
schedule_monitor.run()
except Exception as e:
print("ScheduleMonitor error:", e)
last_monitor_run["schedule"] = now
# AC monitor every 30 seconds (persistent)
if now - last_monitor_run["ac"] >= 30:
try:
ac_monitor.run()
except Exception as e:
print("ACMonitor error:", e)
last_monitor_run["ac"] = now
# Heater monitor every 30 seconds (persistent)
if now - last_monitor_run["heater"] >= 30:
try:
heater_monitor.run()
except Exception as e:
print("HeaterMonitor error:", e)
last_monitor_run["heater"] = now
# Inside temperature monitor every 10 seconds (can be stateless)
if now - last_monitor_run["inside_temp"] >= 10:
from scripts.monitors import TemperatureMonitor
inside_monitor = TemperatureMonitor(
sensor=sensors['inside'],
label=SENSOR_CONFIG['inside']['label'],
check_interval=10,
report_interval=30,
alert_high=SENSOR_CONFIG['inside']['alert_high'],
alert_low=SENSOR_CONFIG['inside']['alert_low'],
log_file="/temp_logs.csv",
send_alerts_to_separate_channel=True
)
inside_monitor.run()
del inside_monitor
gc.collect()
last_monitor_run["inside_temp"] = now
# Outside temperature monitor every 10 seconds (can be stateless)
if now - last_monitor_run["outside_temp"] >= 10:
from scripts.monitors import TemperatureMonitor
outside_monitor = TemperatureMonitor(
sensor=sensors['outside'],
label=SENSOR_CONFIG['outside']['label'],
check_interval=10,
report_interval=30,
alert_high=SENSOR_CONFIG['outside']['alert_high'],
alert_low=SENSOR_CONFIG['outside']['alert_low'],
log_file="/temp_logs.csv",
send_alerts_to_separate_channel=False
)
outside_monitor.run()
del outside_monitor
gc.collect()
last_monitor_run["outside_temp"] = now
# Web requests (keep web server loaded if needed)
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor, config)
# Retry NTP sync every ~10s if not yet synced
if not ntp_synced and retry_ntp_attempts < max_ntp_attempts:
# Try once immediately, then whenever (time.time() % 10) < 1 (rough 10s window)
try:
import ntptime # type: ignore
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
ntptime.settime()
ntp_synced = True
print("NTP sync succeeded on retry #{}".format(retry_ntp_attempts + 1))
except Exception as e:
# Increment only when an actual attempt was made
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
retry_ntp_attempts += 1
print("NTP retry {} failed: {}".format(retry_ntp_attempts, e))
# Enable garbage collection to free memory
gc.collect()
time.sleep(0.1)
except KeyboardInterrupt:
# Graceful shutdown on Ctrl+C
print("\n\n" + "="*50)
# ===== END: Main Loop =====
except KeyboardInterrupt:
print("\n" + "="*50)
print("Shutting down gracefully...")
print("="*50)
try:
print("Turning off AC...")
ac_controller.turn_off()
except Exception as e:
print("AC shutdown error:", e)
try:
print("Turning off heater...")
heater_controller.turn_off()
except Exception as e:
print("Heater shutdown error:", e)
try:
print("Turning off LED...")
led.low()
print("Shutdown complete!")
print("="*50 + "\n")
break
except Exception as e:
# If loop crashes, print error and keep running
print("❌ Main loop error: {}".format(e))
import sys
sys.print_exception(e)
print("⚠️ Pausing 5 seconds before retrying...")
time.sleep(5) # Brief pause before retrying
# ===== END: Main Loop =====
print("LED shutdown error:", e)
print("Shutdown complete!")
print("="*50)

View File

@@ -1,6 +0,0 @@
secrets = {
'ssid': ' Change_to_wifi_SSID',
'password': 'Change_to_wifi_Pasword',
'discord_webhook_url': 'https://discord.com/api/webhooks/key/long-Combination_1234-ChangeMe', # normal updates
'discord_alert_webhook_url': 'https://discord.com/api/webhooks/key/long-Combination_1234-ChangeMe', # alerts only
}