Compare commits
3 Commits
99d92a6e90
...
5ce7cd43a4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce7cd43a4 | |||
| bb46a69eba | |||
| b018b427f6 |
@@ -1,4 +1,4 @@
|
||||
import urequests as requests
|
||||
import urequests as requests # type: ignore
|
||||
from secrets import secrets
|
||||
|
||||
def _escape_json_str(s: str) -> str:
|
||||
@@ -11,6 +11,7 @@ def _escape_json_str(s: str) -> str:
|
||||
return s
|
||||
|
||||
def send_discord_message(message, username="Auto Garden Bot", is_alert=False):
|
||||
"""Send Discord message with 3-second timeout to prevent blocking."""
|
||||
resp = None
|
||||
|
||||
# Use alert webhook if specified, otherwise normal webhook
|
||||
@@ -21,31 +22,29 @@ def send_discord_message(message, username="Auto Garden Bot", is_alert=False):
|
||||
|
||||
try:
|
||||
if not url:
|
||||
# print("DEBUG: no webhook URL in secrets")
|
||||
return False
|
||||
|
||||
url = url.strip().strip('\'"')
|
||||
|
||||
# build JSON by hand so emoji (and other unicode) are preserved as UTF-8 bytes
|
||||
# Build JSON manually to preserve emoji/unicode as UTF-8
|
||||
content = _escape_json_str(message)
|
||||
user = _escape_json_str(username)
|
||||
body_bytes = ('{"content":"%s","username":"%s"}' % (content, user)).encode("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)
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
# print("Failed to send Discord message:", e)
|
||||
# Silently fail (don't spam console with Discord errors)
|
||||
return False
|
||||
finally:
|
||||
if resp:
|
||||
|
||||
@@ -61,21 +61,30 @@ class TempWebServer:
|
||||
if response is None:
|
||||
response = self._get_status_page(sensors, ac_monitor, heater_monitor, schedule_monitor)
|
||||
|
||||
# ===== START: Send response =====
|
||||
# ===== START: Send response with proper HTTP headers =====
|
||||
print("DEBUG: Sending response ({} bytes)".format(len(response)))
|
||||
try:
|
||||
conn.send(response.encode('utf-8')) # ← Changed to 'conn'
|
||||
# Check if response already has HTTP headers (like redirects)
|
||||
if response.startswith('HTTP/1.1'):
|
||||
# Response already has headers (redirect), send as-is
|
||||
conn.sendall(response.encode('utf-8'))
|
||||
else:
|
||||
# HTML response needs headers added first
|
||||
conn.send(b'HTTP/1.1 200 OK\r\n')
|
||||
conn.send(b'Content-Type: text/html; charset=utf-8\r\n')
|
||||
conn.send('Content-Length: {}\r\n'.format(len(response.encode('utf-8'))).encode('utf-8'))
|
||||
conn.send(b'Connection: close\r\n')
|
||||
conn.send(b'\r\n') # Blank line separates headers from body
|
||||
conn.sendall(response.encode('utf-8'))
|
||||
|
||||
print("DEBUG: Response sent successfully")
|
||||
except Exception as e:
|
||||
print("ERROR: Failed to send response: {}".format(e))
|
||||
finally:
|
||||
conn.close() # ← Changed to 'conn'
|
||||
conn.close()
|
||||
print("DEBUG: Client connection closed")
|
||||
# ===== END: Send response =====
|
||||
|
||||
conn.send('HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n')
|
||||
conn.sendall(response.encode('utf-8'))
|
||||
conn.close()
|
||||
except OSError:
|
||||
pass
|
||||
except Exception as e:
|
||||
|
||||
79
main.py
79
main.py
@@ -1,4 +1,4 @@
|
||||
from machine import Pin, WDT # type: ignore
|
||||
from machine import Pin, RTC # type: ignore
|
||||
import time # type: ignore
|
||||
import network # type: ignore
|
||||
import json
|
||||
@@ -141,23 +141,60 @@ 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]} ✅")
|
||||
# Send startup notification to Discord (with timeout, non-blocking)
|
||||
try:
|
||||
success = send_discord_message(f"Pico W online at http://{ifconfig[0]} ✅")
|
||||
if success:
|
||||
print("Discord startup notification sent")
|
||||
else:
|
||||
print("Discord startup notification failed (continuing anyway)")
|
||||
except Exception as e:
|
||||
print("Discord notification error: {}".format(e))
|
||||
|
||||
# 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)
|
||||
# Attempt time sync with timeout (MicroPython compatible)
|
||||
ntp_synced = False
|
||||
try:
|
||||
import ntptime # type: ignore
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# Monkey-patch ntptime.time() to add timeout
|
||||
original_time_func = ntptime.time
|
||||
|
||||
def time_with_timeout():
|
||||
"""NTP time fetch with 3-second timeout."""
|
||||
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)
|
||||
s.close()
|
||||
val = struct.unpack("!I", msg[40:44])[0]
|
||||
return val - NTP_DELTA
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
# Use patched version
|
||||
ntptime.time = time_with_timeout
|
||||
ntptime.settime()
|
||||
ntp_synced = True
|
||||
print("Time synced with NTP server")
|
||||
|
||||
except Exception as e:
|
||||
print("Initial NTP sync failed: {}".format(e))
|
||||
# Will retry later in loop
|
||||
print("Will retry in background...")
|
||||
|
||||
else:
|
||||
# WiFi connection failed
|
||||
@@ -320,18 +357,40 @@ while True:
|
||||
|
||||
# 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)
|
||||
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
|
||||
try:
|
||||
import ntptime # type: ignore
|
||||
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
|
||||
ntptime.settime()
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# Quick NTP sync with timeout
|
||||
NTP_DELTA = 2208988800
|
||||
host = "pool.ntp.org"
|
||||
NTP_QUERY = bytearray(48)
|
||||
NTP_QUERY[0] = 0x1B
|
||||
|
||||
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]
|
||||
t = val - NTP_DELTA
|
||||
|
||||
tm = time.gmtime(t)
|
||||
RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
|
||||
|
||||
ntp_synced = True
|
||||
print("NTP sync succeeded on retry #{}".format(retry_ntp_attempts + 1))
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user