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
This commit is contained in:
parent
a20bbd7cdf
commit
d95f212d2e
@ -1,8 +1,23 @@
|
|||||||
import urequests as requests # type: ignore
|
# Minimal module-level state (only what we need)
|
||||||
from secrets import secrets
|
_CONFIG = {"discord_webhook_url": None, "discord_alert_webhook_url": None}
|
||||||
|
|
||||||
|
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:
|
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('"', '\\"')
|
s = s.replace('"', '\\"')
|
||||||
s = s.replace("\n", "\\n")
|
s = s.replace("\n", "\\n")
|
||||||
@ -11,44 +26,52 @@ def _escape_json_str(s: str) -> str:
|
|||||||
return s
|
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):
|
||||||
"""Send Discord message with 3-second timeout to prevent blocking."""
|
"""
|
||||||
|
Send Discord message. Import urequests locally to avoid occupying RAM when idle.
|
||||||
|
Returns True on success, False otherwise.
|
||||||
|
"""
|
||||||
resp = None
|
resp = None
|
||||||
|
url = _get_webhook_url(is_alert=is_alert)
|
||||||
# Use alert webhook if specified, otherwise normal webhook
|
if not url:
|
||||||
if is_alert:
|
return False
|
||||||
url = secrets.get('discord_alert_webhook_url') or secrets.get('discord_webhook_url')
|
|
||||||
else:
|
|
||||||
url = secrets.get('discord_webhook_url')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not url:
|
# local import to save RAM
|
||||||
return False
|
import urequests as requests # type: ignore
|
||||||
|
import gc
|
||||||
|
|
||||||
url = url.strip().strip('\'"')
|
url = str(url).strip().strip('\'"')
|
||||||
|
|
||||||
# Build JSON manually to preserve emoji/unicode as UTF-8
|
|
||||||
content = _escape_json_str(message)
|
content = _escape_json_str(message)
|
||||||
user = _escape_json_str(username)
|
user = _escape_json_str(username)
|
||||||
body_bytes = ('{"content":"%s","username":"%s"}' % (content, user)).encode("utf-8")
|
body_bytes = ('{"content":"%s","username":"%s"}' % (content, user)).encode("utf-8")
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||||
|
|
||||||
# Make POST request (urequests has built-in ~5s timeout)
|
|
||||||
resp = requests.post(url, data=body_bytes, headers=headers)
|
resp = requests.post(url, data=body_bytes, headers=headers)
|
||||||
|
|
||||||
status = getattr(resp, "status", getattr(resp, "status_code", None))
|
status = getattr(resp, "status", getattr(resp, "status_code", None))
|
||||||
|
success = bool(status and 200 <= status < 300)
|
||||||
if status and 200 <= status < 300:
|
if not success:
|
||||||
return True
|
# optional: print status for debugging, but avoid spamming
|
||||||
else:
|
print("Discord webhook failed, status:", status)
|
||||||
return False
|
return success
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Silently fail (don't spam console with Discord errors)
|
# avoid raising to prevent crashing monitors; print minimal info
|
||||||
|
print("Discord webhook exception:", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if resp:
|
try:
|
||||||
try:
|
if resp:
|
||||||
resp.close()
|
resp.close()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
# free large objects and modules, then force GC
|
||||||
|
try:
|
||||||
|
del resp
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
gc.collect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import time # type: ignore
|
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
|
from scripts.temperature_sensor import TemperatureSensor
|
||||||
|
|
||||||
class Monitor:
|
class Monitor:
|
||||||
@ -108,13 +108,11 @@ class TemperatureMonitor(Monitor):
|
|||||||
self.alert_start_time = current_time
|
self.alert_start_time = current_time
|
||||||
print(alert_message)
|
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:
|
if self.send_alerts_to_separate_channel:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message(alert_message, is_alert=True)
|
||||||
send_discord_message(alert_message, is_alert=True)
|
|
||||||
else:
|
else:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message(alert_message)
|
||||||
send_discord_message(alert_message)
|
|
||||||
|
|
||||||
self.alert_sent = True
|
self.alert_sent = True
|
||||||
|
|
||||||
@ -139,13 +137,11 @@ class TemperatureMonitor(Monitor):
|
|||||||
)
|
)
|
||||||
print(recovery_message)
|
print(recovery_message)
|
||||||
|
|
||||||
# Send to appropriate Discord channel
|
# send recovery message
|
||||||
if self.send_alerts_to_separate_channel:
|
if self.send_alerts_to_separate_channel:
|
||||||
from scripts.discord_webhook import send_alert_message
|
discord_webhook.send_discord_message(recovery_message, is_alert=True)
|
||||||
send_alert_message(recovery_message)
|
|
||||||
else:
|
else:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message(recovery_message)
|
||||||
send_discord_message(recovery_message)
|
|
||||||
|
|
||||||
self.alert_sent = False
|
self.alert_sent = False
|
||||||
self.alert_start_time = None
|
self.alert_start_time = None
|
||||||
@ -198,14 +194,14 @@ class ACMonitor(Monitor):
|
|||||||
# Too hot, turn AC on
|
# Too hot, turn AC on
|
||||||
if self.ac.turn_on():
|
if self.ac.turn_on():
|
||||||
if not self.last_notified_state:
|
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
|
self.last_notified_state = True
|
||||||
|
|
||||||
elif current_temp < (self.target_temp - self.temp_swing):
|
elif current_temp < (self.target_temp - self.temp_swing):
|
||||||
# Cool enough, turn AC off
|
# Cool enough, turn AC off
|
||||||
if self.ac.turn_off():
|
if self.ac.turn_off():
|
||||||
if self.last_notified_state:
|
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
|
self.last_notified_state = False
|
||||||
|
|
||||||
# Else: within temp_swing range, maintain current state
|
# Else: within temp_swing range, maintain current state
|
||||||
@ -244,27 +240,28 @@ class HeaterMonitor(Monitor):
|
|||||||
# Too cold, turn heater on
|
# Too cold, turn heater on
|
||||||
if self.heater.turn_on():
|
if self.heater.turn_on():
|
||||||
if not self.last_notified_state:
|
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
|
self.last_notified_state = True
|
||||||
|
|
||||||
elif current_temp > (self.target_temp + self.temp_swing):
|
elif current_temp > (self.target_temp + self.temp_swing):
|
||||||
# Warm enough, turn heater off
|
# Warm enough, turn heater off
|
||||||
if self.heater.turn_off():
|
if self.heater.turn_off():
|
||||||
if self.last_notified_state:
|
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
|
self.last_notified_state = False
|
||||||
|
|
||||||
# Else: within temp_swing range, maintain current state
|
# Else: within temp_swing range, maintain current state
|
||||||
|
|
||||||
class WiFiMonitor(Monitor):
|
class WiFiMonitor(Monitor):
|
||||||
"""Monitor WiFi connection and handle reconnection."""
|
"""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)
|
super().__init__(interval)
|
||||||
self.wifi = wifi
|
self.wifi = wifi
|
||||||
self.led = led
|
self.led = led
|
||||||
self.reconnect_cooldown = reconnect_cooldown
|
self.reconnect_cooldown = reconnect_cooldown
|
||||||
self.last_reconnect_attempt = 0
|
self.last_reconnect_attempt = 0
|
||||||
self.was_connected = wifi.isconnected() if wifi else False
|
self.was_connected = wifi.isconnected() if wifi else False
|
||||||
|
self.config = config
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Check WiFi status, blink LED, attempt reconnect if needed."""
|
"""Check WiFi status, blink LED, attempt reconnect if needed."""
|
||||||
@ -284,10 +281,10 @@ class WiFiMonitor(Monitor):
|
|||||||
if time.ticks_diff(now, self.last_reconnect_attempt) >= (self.reconnect_cooldown * 1000):
|
if time.ticks_diff(now, self.last_reconnect_attempt) >= (self.reconnect_cooldown * 1000):
|
||||||
self.last_reconnect_attempt = now
|
self.last_reconnect_attempt = now
|
||||||
# print("Attempting WiFi reconnect...")
|
# 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():
|
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
|
self.was_connected = True
|
||||||
else:
|
else:
|
||||||
# Slow blink when connected
|
# Slow blink when connected
|
||||||
@ -297,7 +294,7 @@ class WiFiMonitor(Monitor):
|
|||||||
|
|
||||||
# Notify if connection was just restored
|
# Notify if connection was just restored
|
||||||
if not self.was_connected:
|
if not self.was_connected:
|
||||||
send_discord_message("WiFi connection restored 🔄")
|
discord_webhook.send_discord_message("WiFi connection restored 🔄")
|
||||||
self.was_connected = True
|
self.was_connected = True
|
||||||
|
|
||||||
def run_monitors(monitors):
|
def run_monitors(monitors):
|
||||||
|
|||||||
@ -1,30 +1,43 @@
|
|||||||
import network
|
import network
|
||||||
import time
|
import time
|
||||||
from secrets import secrets
|
|
||||||
|
|
||||||
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:
|
Args:
|
||||||
led: Optional LED pin for visual feedback
|
led: Optional LED pin for visual feedback
|
||||||
max_retries: Number of connection attempts (default: 3)
|
max_retries: Number of connection attempts (default: 3)
|
||||||
timeout: Seconds to wait for connection per attempt (default: 20)
|
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:
|
Returns:
|
||||||
WLAN object if connected, None if failed
|
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)
|
wlan = network.WLAN(network.STA_IF)
|
||||||
|
|
||||||
# Ensure clean state
|
# Ensure clean state
|
||||||
try:
|
try:
|
||||||
if wlan.active():
|
if wlan.active():
|
||||||
wlan.active(False)
|
wlan.active(False)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
wlan.active(True)
|
wlan.active(True)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"WiFi activation error: {e}")
|
print(f"WiFi activation error: {e}")
|
||||||
print("Attempting reset...")
|
print("Attempting reset...")
|
||||||
@ -37,66 +50,83 @@ def connect_wifi(led=None, max_retries=3, timeout=20):
|
|||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
print(f"WiFi reset failed: {e2}")
|
print(f"WiFi reset failed: {e2}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Try connecting with retries
|
# Try connecting with retries
|
||||||
for attempt in range(1, max_retries + 1):
|
for attempt in range(1, max_retries + 1):
|
||||||
if wlan.isconnected():
|
if wlan.isconnected():
|
||||||
print(f"Already connected to WiFi")
|
print("Already connected to WiFi")
|
||||||
break
|
break
|
||||||
|
|
||||||
print(f'Connecting to WiFi (attempt {attempt}/{max_retries})...')
|
print(f'Connecting to WiFi SSID: {ssid} (attempt {attempt}/{max_retries})...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wlan.connect(secrets['ssid'], secrets['password'])
|
wlan.connect(ssid, password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Connection attempt failed: {e}")
|
print(f"Connection attempt failed: {e}")
|
||||||
if attempt < max_retries:
|
if attempt < max_retries:
|
||||||
print("Retrying in 3 seconds...")
|
print("Retrying in 3 seconds...")
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Wait for connection with timeout
|
# Wait for connection with timeout
|
||||||
wait_time = 0
|
wait_time = 0
|
||||||
while wait_time < timeout:
|
while wait_time < timeout:
|
||||||
if wlan.isconnected():
|
if wlan.isconnected():
|
||||||
break
|
break
|
||||||
|
|
||||||
if led:
|
if led:
|
||||||
led.toggle()
|
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)
|
time.sleep(0.5)
|
||||||
wait_time += 0.5
|
wait_time += 0.5
|
||||||
|
|
||||||
# Print progress dots every 2 seconds
|
# Print progress dots every 2 seconds
|
||||||
if int(wait_time * 2) % 4 == 0:
|
if int(wait_time * 2) % 4 == 0:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
|
|
||||||
print() # New line after dots
|
print() # New line after dots
|
||||||
|
|
||||||
if wlan.isconnected():
|
if wlan.isconnected():
|
||||||
break
|
break
|
||||||
|
|
||||||
print(f'Connection attempt {attempt} failed')
|
print(f'Connection attempt {attempt} failed')
|
||||||
if attempt < max_retries:
|
if attempt < max_retries:
|
||||||
print("Retrying in 3 seconds...")
|
print("Retrying in 3 seconds...")
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
# Final connection check
|
# Final connection check
|
||||||
if not wlan.isconnected():
|
if not wlan.isconnected():
|
||||||
print('WiFi connection failed after all attempts!')
|
print('WiFi connection failed after all attempts!')
|
||||||
if led:
|
if led:
|
||||||
led.off()
|
try:
|
||||||
|
# prefer available method names
|
||||||
|
if hasattr(led, "off"):
|
||||||
|
led.off()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Success feedback
|
# Success feedback
|
||||||
if led:
|
if led:
|
||||||
# Double pulse on successful connection
|
try:
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
led.on()
|
led.on()
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
led.off()
|
led.off()
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
print('Connected to WiFi successfully!')
|
print('Connected to WiFi successfully!')
|
||||||
|
|
||||||
return wlan
|
return wlan
|
||||||
@ -90,32 +90,61 @@ class ScheduleMonitor:
|
|||||||
return # Already applied
|
return # Already applied
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Track whether we changed persisted values to avoid unnecessary writes
|
||||||
|
changed = False
|
||||||
|
|
||||||
# Update AC settings if provided
|
# Update AC settings if provided
|
||||||
if 'ac_target' in schedule:
|
if 'ac_target' in schedule:
|
||||||
self.ac_monitor.target_temp = float(schedule['ac_target'])
|
new_ac = float(schedule['ac_target'])
|
||||||
self.config['ac_target'] = 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:
|
if 'ac_swing' in schedule:
|
||||||
self.ac_monitor.temp_swing = float(schedule['ac_swing'])
|
new_ac_swing = float(schedule['ac_swing'])
|
||||||
self.config['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
|
# Update heater settings if provided
|
||||||
if 'heater_target' in schedule:
|
if 'heater_target' in schedule:
|
||||||
self.heater_monitor.target_temp = float(schedule['heater_target'])
|
new_ht = float(schedule['heater_target'])
|
||||||
self.config['heater_target'] = 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:
|
if 'heater_swing' in schedule:
|
||||||
self.heater_monitor.temp_swing = float(schedule['heater_swing'])
|
new_ht_swing = float(schedule['heater_swing'])
|
||||||
self.config['heater_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 to file so targets persist
|
# Save updated config only if something changed
|
||||||
try:
|
if changed:
|
||||||
import json
|
try:
|
||||||
with open('config.json', 'w') as f:
|
import json
|
||||||
json.dump(self.config, f)
|
with open('config.json', 'w') as f:
|
||||||
print("✅ Config updated with active schedule targets")
|
json.dump(self.config, f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("⚠️ Could not save config: {}".format(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
|
# Log the change
|
||||||
schedule_name = schedule.get('name', 'Unnamed')
|
schedule_name = schedule.get('name', 'Unnamed')
|
||||||
@ -125,21 +154,22 @@ class ScheduleMonitor:
|
|||||||
print("AC Target: {}°F".format(self.ac_monitor.target_temp))
|
print("AC Target: {}°F".format(self.ac_monitor.target_temp))
|
||||||
print("Heater Target: {}°F".format(self.heater_monitor.target_temp))
|
print("Heater Target: {}°F".format(self.heater_monitor.target_temp))
|
||||||
print("="*50 + "\n")
|
print("="*50 + "\n")
|
||||||
|
|
||||||
# Send Discord notification
|
# Send Discord notification (use discord_webhook if available)
|
||||||
try:
|
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(
|
message = "🕐 Schedule '{}' applied - AC: {}°F | Heater: {}°F".format(
|
||||||
schedule_name,
|
schedule_name,
|
||||||
self.ac_monitor.target_temp,
|
self.ac_monitor.target_temp,
|
||||||
self.heater_monitor.target_temp
|
self.heater_monitor.target_temp
|
||||||
)
|
)
|
||||||
send_discord_message(message)
|
discord_webhook.send_discord_message(message)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.last_applied_schedule = schedule_id
|
self.last_applied_schedule = schedule_id
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error applying schedule: {}".format(e))
|
print("Error applying schedule: {}".format(e))
|
||||||
|
|
||||||
@ -165,16 +195,24 @@ class ScheduleMonitor:
|
|||||||
import json
|
import json
|
||||||
with open('config.json', 'w') as f:
|
with open('config.json', 'w') as f:
|
||||||
json.dump(self.config, f)
|
json.dump(self.config, f)
|
||||||
print("✅ Config updated - automatic mode resumed")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("⚠️ Could not save config: {}".format(e))
|
print("⚠️ Could not save config: {}".format(e))
|
||||||
|
else:
|
||||||
# Notify user
|
# ensure in-memory webhook config updated
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
import scripts.discord_webhook as discord_webhook
|
||||||
send_discord_message("⏰ Temporary hold expired - Schedule resumed automatically")
|
discord_webhook.set_config(self.config)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print("✅ Config updated - automatic mode resumed")
|
||||||
|
|
||||||
|
# Notify user
|
||||||
|
try:
|
||||||
|
import scripts.discord_webhook as discord_webhook
|
||||||
|
discord_webhook.send_discord_message("⏰ Temporary hold expired - Schedule resumed automatically")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# ===== END: Check if temporary hold has expired =====
|
# ===== END: Check if temporary hold has expired =====
|
||||||
|
|
||||||
# Find and apply active schedule
|
# Find and apply active schedule
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import socket
|
import socket
|
||||||
import time # type: ignore
|
import time # type: ignore
|
||||||
import json
|
import json
|
||||||
|
import scripts.discord_webhook as discord_webhook
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class TempWebServer:
|
class TempWebServer:
|
||||||
"""Simple web server for viewing temperatures and adjusting settings."""
|
"""Simple web server for viewing temperatures and adjusting settings."""
|
||||||
@ -106,8 +109,8 @@ class TempWebServer:
|
|||||||
response_bytes = response.encode('utf-8')
|
response_bytes = response.encode('utf-8')
|
||||||
|
|
||||||
# Send headers
|
# Send headers
|
||||||
conn.send('HTTP/1.1 200 OK\r\n')
|
conn.sendall(b'HTTP/1.1 200 OK\r\n')
|
||||||
conn.send('Content-Type: text/html; charset=utf-8\r\n')
|
conn.sendall(b'Content-Type: text/html; charset=utf-8\r\n')
|
||||||
conn.send('Content-Length: {}\r\n'.format(len(response_bytes)))
|
conn.send('Content-Length: {}\r\n'.format(len(response_bytes)))
|
||||||
conn.send('Connection: close\r\n')
|
conn.send('Connection: close\r\n')
|
||||||
conn.send('\r\n')
|
conn.send('\r\n')
|
||||||
@ -224,6 +227,12 @@ class TempWebServer:
|
|||||||
# Rename temp to config (atomic on most filesystems)
|
# Rename temp to config (atomic on most filesystems)
|
||||||
os.rename('config.tmp', 'config.json')
|
os.rename('config.tmp', 'config.json')
|
||||||
|
|
||||||
|
# Update discord module in-memory config so webhook URLs are current
|
||||||
|
try:
|
||||||
|
discord_webhook.set_config(config)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
print("Settings saved to config.json")
|
print("Settings saved to config.json")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -276,8 +285,7 @@ class TempWebServer:
|
|||||||
|
|
||||||
# Send Discord notification
|
# Send Discord notification
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message("▶️ Schedule resumed - Automatic temperature control active")
|
||||||
send_discord_message("▶️ Schedule resumed - Automatic temperature control active")
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -302,8 +310,7 @@ class TempWebServer:
|
|||||||
schedule_monitor.reload_config(config)
|
schedule_monitor.reload_config(config)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message("⏸️ Temporary hold - Schedules paused, manual control active")
|
||||||
send_discord_message("⏸️ Temporary hold - Schedules paused, manual control active")
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -327,8 +334,7 @@ class TempWebServer:
|
|||||||
schedule_monitor.reload_config(config)
|
schedule_monitor.reload_config(config)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message("🛑 Permanent hold - Schedules disabled, manual control only")
|
||||||
send_discord_message("🛑 Permanent hold - Schedules disabled, manual control only")
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -490,12 +496,11 @@ class TempWebServer:
|
|||||||
|
|
||||||
# Send Discord notification
|
# Send Discord notification
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
|
||||||
mode = "automatic" if config.get('schedule_enabled') else "hold"
|
mode = "automatic" if config.get('schedule_enabled') else "hold"
|
||||||
message = "📅 Schedules updated ({} mode) - {} schedules configured".format(
|
message = "📅 Schedules updated ({} mode) - {} schedules configured".format(
|
||||||
mode, len(schedules)
|
mode, len(schedules)
|
||||||
)
|
)
|
||||||
send_discord_message(message)
|
discord_webhook.send_discord_message(message)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
# ===== END: Handle schedule configuration save =====
|
# ===== END: Handle schedule configuration save =====
|
||||||
@ -611,7 +616,6 @@ class TempWebServer:
|
|||||||
|
|
||||||
# ===== START: Send Discord notification =====
|
# ===== START: Send Discord notification =====
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
|
||||||
hold_label = "PERMANENT HOLD" if is_permanent else "TEMPORARY HOLD"
|
hold_label = "PERMANENT HOLD" if is_permanent else "TEMPORARY HOLD"
|
||||||
duration = "" if is_permanent else " (1 hour)"
|
duration = "" if is_permanent else " (1 hour)"
|
||||||
|
|
||||||
@ -622,7 +626,7 @@ class TempWebServer:
|
|||||||
params.get('heater_target', 'N/A'),
|
params.get('heater_target', 'N/A'),
|
||||||
duration
|
duration
|
||||||
)
|
)
|
||||||
send_discord_message(message)
|
discord_webhook.send_discord_message(message)
|
||||||
except Exception as discord_error:
|
except Exception as discord_error:
|
||||||
print("Discord notification failed: {}".format(discord_error))
|
print("Discord notification failed: {}".format(discord_error))
|
||||||
# ===== END: Send Discord notification =====
|
# ===== END: Send Discord notification =====
|
||||||
@ -1754,8 +1758,7 @@ class TempWebServer:
|
|||||||
|
|
||||||
# Discord notification
|
# Discord notification
|
||||||
try:
|
try:
|
||||||
from scripts.discord_webhook import send_discord_message
|
discord_webhook.send_discord_message("⚙️ Advanced settings updated")
|
||||||
send_discord_message("⚙️ Advanced settings updated")
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
45
config.json.Example
Normal file
45
config.json.Example
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"heater_swing": 2.0,
|
||||||
|
"temp_hold_duration": 3600,
|
||||||
|
"schedules": [
|
||||||
|
{
|
||||||
|
"time": "06:00",
|
||||||
|
"ac_target": 75.0,
|
||||||
|
"heater_target": 72.0,
|
||||||
|
"name": "Morning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "12:00",
|
||||||
|
"ac_target": 75.0,
|
||||||
|
"heater_target": 72.0,
|
||||||
|
"name": "Midday"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "18:00",
|
||||||
|
"ac_target": 75.0,
|
||||||
|
"heater_target": 72.0,
|
||||||
|
"name": "Evening"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": "22:00",
|
||||||
|
"ac_target": 75.0,
|
||||||
|
"heater_target": 72.0,
|
||||||
|
"name": "Night"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schedule_enabled": true,
|
||||||
|
"permanent_hold": false,
|
||||||
|
"temp_hold_start_time": null
|
||||||
|
}
|
||||||
10
main.py
10
main.py
@ -21,7 +21,7 @@ except Exception as e:
|
|||||||
|
|
||||||
# Import after WiFi reset
|
# Import after WiFi reset
|
||||||
from scripts.networking import connect_wifi
|
from scripts.networking import connect_wifi
|
||||||
from scripts.discord_webhook import send_discord_message
|
import scripts.discord_webhook as discord_webhook
|
||||||
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, HeaterMonitor, run_monitors
|
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, HeaterMonitor, run_monitors
|
||||||
from scripts.temperature_sensor import TemperatureSensor
|
from scripts.temperature_sensor import TemperatureSensor
|
||||||
from scripts.air_conditioning import ACController
|
from scripts.air_conditioning import ACController
|
||||||
@ -146,6 +146,8 @@ def load_config():
|
|||||||
|
|
||||||
# Load configuration from file
|
# Load configuration from file
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
# 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)
|
# Get timezone offset from config (with fallback to -6 if not present)
|
||||||
TIMEZONE_OFFSET = config.get('timezone_offset', -6)
|
TIMEZONE_OFFSET = config.get('timezone_offset', -6)
|
||||||
@ -171,7 +173,7 @@ except Exception as e:
|
|||||||
|
|
||||||
# ===== START: WiFi Connection =====
|
# ===== START: WiFi Connection =====
|
||||||
# Connect to WiFi using credentials from secrets.py
|
# Connect to WiFi using credentials from secrets.py
|
||||||
wifi = connect_wifi(led)
|
wifi = connect_wifi(led, config=config)
|
||||||
|
|
||||||
# Set static IP and print WiFi details
|
# Set static IP and print WiFi details
|
||||||
if wifi and wifi.isconnected():
|
if wifi and wifi.isconnected():
|
||||||
@ -199,7 +201,7 @@ if wifi and wifi.isconnected():
|
|||||||
|
|
||||||
# Send startup notification to Discord (with timeout, non-blocking)
|
# Send startup notification to Discord (with timeout, non-blocking)
|
||||||
try:
|
try:
|
||||||
success = send_discord_message(f"Pico W online at http://{ifconfig[0]} ✅")
|
success = discord_webhook.send_discord_message(f"Pico W online at http://{ifconfig[0]} ✅")
|
||||||
if success:
|
if success:
|
||||||
print("Discord startup notification sent")
|
print("Discord startup notification sent")
|
||||||
else:
|
else:
|
||||||
@ -345,7 +347,7 @@ check_memory_once()
|
|||||||
# Set up all monitoring systems (run in order during main loop)
|
# Set up all monitoring systems (run in order during main loop)
|
||||||
monitors = [
|
monitors = [
|
||||||
# WiFi monitor: Checks connection, reconnects if needed, blinks LED
|
# WiFi monitor: Checks connection, reconnects if needed, blinks LED
|
||||||
WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60),
|
WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60, config=config),
|
||||||
|
|
||||||
# Schedule monitor: Changes temp targets based on time of day
|
# Schedule monitor: Changes temp targets based on time of day
|
||||||
schedule_monitor,
|
schedule_monitor,
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user