Compare commits

...

6 Commits

5 changed files with 429 additions and 42 deletions

68
Scripts/heating.py Normal file
View File

@ -0,0 +1,68 @@
from machine import Pin
import time
class HeaterController:
"""Control heater via opto-coupler relay."""
def __init__(self, relay_pin=16, min_run_time=300, min_off_time=180):
"""
relay_pin: GPIO pin connected to opto-coupler input
min_run_time: Minimum seconds heater must run before turning off (prevent short cycling)
min_off_time: Minimum seconds heater must be off before turning on (element protection)
"""
self.relay = Pin(relay_pin, Pin.OUT)
self.relay.off() # Start with heater off (relay normally open)
self.min_run_time = min_run_time
self.min_off_time = min_off_time
self.is_on = False
self.last_state_change = time.ticks_ms()
def turn_on(self):
"""Turn heater on if minimum off time has elapsed."""
if self.is_on:
return True # Already on
now = time.ticks_ms()
time_since_change = time.ticks_diff(now, self.last_state_change) / 1000
if time_since_change < self.min_off_time:
remaining = int(self.min_off_time - time_since_change)
print(f"Heater cooldown: {remaining}s remaining before can turn on")
return False
self.relay.on()
self.is_on = True
self.last_state_change = now
print("Heater turned ON")
return True
def turn_off(self):
"""Turn heater off if minimum run time has elapsed."""
if not self.is_on:
return True # Already off
now = time.ticks_ms()
time_since_change = time.ticks_diff(now, self.last_state_change) / 1000
if time_since_change < self.min_run_time:
remaining = int(self.min_run_time - time_since_change)
print(f"Heater minimum runtime: {remaining}s remaining before can turn off")
return False
self.relay.off()
self.is_on = False
self.last_state_change = now
print("Heater turned OFF")
return True
def get_state(self):
"""Return current heater state."""
return self.is_on
def force_off(self):
"""Emergency shut off (bypasses timers)."""
self.relay.off()
self.is_on = False
self.last_state_change = time.ticks_ms()
print("Heater FORCE OFF")

View File

@ -142,6 +142,52 @@ class ACMonitor(Monitor):
# Else: within temp_swing range, maintain current state
class HeaterMonitor(Monitor):
"""Monitor temperature and control heater automatically."""
def __init__(self, heater_controller, temp_sensor, target_temp=70.0, temp_swing=2.0, interval=30):
"""
heater_controller: HeaterController instance
temp_sensor: TemperatureSensor instance (inside temp)
target_temp: Target temperature in °F
temp_swing: Temperature swing allowed (prevents rapid cycling)
interval: Seconds between checks
"""
super().__init__(interval)
self.heater = heater_controller
self.sensor = temp_sensor
self.target_temp = target_temp
self.temp_swing = temp_swing
self.last_notified_state = None
def run(self):
"""Check temperature and control heater."""
temps = self.sensor.read_all_temps(unit='F')
if not temps:
return
# Use first sensor reading (assuming single inside sensor)
current_temp = list(temps.values())[0]
# Heating logic with temperature swing
# Turn ON if: temp < target - temp_swing
# Turn OFF if: temp > target + temp_swing
if current_temp < (self.target_temp - self.temp_swing):
# 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")
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")
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):

View File

@ -2,36 +2,65 @@ import network
import time
from secrets import secrets
RECONNECT_COOLDOWN_MS = 60000 # 60 seconds
def connect_wifi(led=None):
"""Connect to WiFi using credentials from secrets.py"""
try:
wlan = network.WLAN(network.STA_IF)
def connect_wifi(led=None, timeout=10):
"""
Connect to WiFi using secrets['ssid'] / secrets['password'].
If `led` (machine.Pin) is provided, pulse it once on successful connect.
Returns the WLAN object or None on failure.
"""
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
# Deactivate first if already active (fixes EPERM error)
if wlan.active():
wlan.active(False)
time.sleep(1)
# print("Connecting to WiFi...", end="")
wifi.connect(secrets['ssid'], secrets['password'])
wlan.active(True)
time.sleep(1) # Give it time to initialize
except OSError as e:
print(f"WiFi activation error: {e}")
print("Attempting reset...")
try:
# Force deinit and reinit
wlan.deinit()
time.sleep(2)
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
time.sleep(1)
except Exception as e2:
print(f"WiFi reset failed: {e2}")
return None
if not wlan.isconnected():
print('Connecting to WiFi...')
try:
wlan.connect(secrets['ssid'], secrets['password'])
except Exception as e:
print(f"Connection attempt failed: {e}")
return None
# Wait for connection with timeout
max_wait = timeout
max_wait = 20
while max_wait > 0:
if wifi.status() < 0 or wifi.status() >= 3:
if wlan.isconnected():
break
max_wait -= 1
# print(".", end="")
time.sleep(1)
if wifi.isconnected():
# print("\nConnected! Network config:", wifi.ifconfig())
if led:
led.on()
time.sleep(1)
led.toggle()
time.sleep(0.5)
max_wait -= 1
print('.', end='')
print()
if not wlan.isconnected():
print('WiFi connection failed!')
if led:
led.off()
return wifi
else:
# print("\nConnection failed!")
return None
if led:
# Single pulse on successful connection
led.on()
time.sleep(0.5)
led.off()
print('Connected to WiFi')
return wlan

188
Scripts/web_server.py Normal file
View File

@ -0,0 +1,188 @@
import socket
import time
class TempWebServer:
"""Simple web server for viewing temperatures."""
def __init__(self, port=80):
self.port = port
self.socket = None
self.sensors = {}
def start(self):
"""Start the web server (non-blocking)."""
try:
self.socket = socket.socket()
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(('0.0.0.0', self.port))
self.socket.listen(1)
self.socket.setblocking(False) # Non-blocking mode
print(f"Web server started on port {self.port}")
except Exception as e:
print(f"Failed to start web server: {e}")
def check_requests(self, sensors, ac_monitor=None, heater_monitor=None):
"""Check for incoming requests (call in main loop)."""
if not self.socket:
return
try:
conn, addr = self.socket.accept()
conn.settimeout(3.0)
request = conn.recv(1024).decode('utf-8')
# Generate response
response = self._get_status_page(sensors, ac_monitor, heater_monitor)
conn.send('HTTP/1.1 200 OK\r\n')
conn.send('Content-Type: text/html\r\n')
conn.send('Connection: close\r\n\r\n')
conn.sendall(response)
conn.close()
except OSError:
pass # No connection, continue
except Exception as e:
print(f"Web server error: {e}")
def _get_status_page(self, sensors, ac_monitor, heater_monitor):
"""Generate HTML status page."""
# Get current temperatures
inside_temps = sensors['inside'].read_all_temps(unit='F')
outside_temps = sensors['outside'].read_all_temps(unit='F')
inside_temp = list(inside_temps.values())[0] if inside_temps else "N/A"
outside_temp = list(outside_temps.values())[0] if outside_temps else "N/A"
# Get AC/Heater status
ac_status = "ON" if ac_monitor and ac_monitor.ac.get_state() else "OFF"
heater_status = "ON" if heater_monitor and heater_monitor.heater.get_state() else "OFF"
# Get current time
current_time = time.localtime()
time_str = f"{current_time[0]}-{current_time[1]:02d}-{current_time[2]:02d} {current_time[3]:02d}:{current_time[4]:02d}:{current_time[5]:02d}"
html = """
<!DOCTYPE html>
<html>
<head>
<title>Auto Garden Status</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="10">
<style>
body {{
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}}
h1 {{
color: #2c3e50;
text-align: center;
}}
.card {{
background: white;
border-radius: 8px;
padding: 20px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.temp-display {{
font-size: 48px;
font-weight: bold;
text-align: center;
margin: 20px 0;
}}
.inside {{ color: #e74c3c; }}
.outside {{ color: #3498db; }}
.label {{
font-size: 18px;
color: #7f8c8d;
text-align: center;
margin-bottom: 10px;
}}
.status {{
display: flex;
justify-content: space-around;
margin-top: 20px;
}}
.status-item {{
text-align: center;
}}
.status-indicator {{
font-size: 24px;
font-weight: bold;
padding: 10px 20px;
border-radius: 5px;
display: inline-block;
margin-top: 10px;
}}
.on {{
background-color: #2ecc71;
color: white;
}}
.off {{
background-color: #95a5a6;
color: white;
}}
.footer {{
text-align: center;
color: #7f8c8d;
margin-top: 20px;
font-size: 14px;
}}
.targets {{
font-size: 14px;
color: #7f8c8d;
text-align: center;
margin-top: 10px;
}}
</style>
</head>
<body>
<h1>🌱 Auto Garden Status</h1>
<div class="card">
<div class="label">Inside Temperature</div>
<div class="temp-display inside">{inside_temp}°F</div>
</div>
<div class="card">
<div class="label">Outside Temperature</div>
<div class="temp-display outside">{outside_temp}°F</div>
</div>
<div class="card">
<div class="status">
<div class="status-item">
<div class="label"> Air Conditioning</div>
<div class="status-indicator {ac_class}">{ac_status}</div>
<div class="targets">Target: {ac_target}°F ± {ac_swing}°F</div>
</div>
<div class="status-item">
<div class="label">🔥 Heater</div>
<div class="status-indicator {heater_class}">{heater_status}</div>
<div class="targets">Target: {heater_target}°F ± {heater_swing}°F</div>
</div>
</div>
</div>
<div class="footer">
Last updated: {time}<br>
Auto-refresh every 10 seconds
</div>
</body>
</html>
""".format(
inside_temp=f"{inside_temp:.1f}" if isinstance(inside_temp, float) else inside_temp,
outside_temp=f"{outside_temp:.1f}" if isinstance(outside_temp, float) else outside_temp,
ac_status=ac_status,
ac_class="on" if ac_status == "ON" else "off",
heater_status=heater_status,
heater_class="on" if heater_status == "ON" else "off",
ac_target=ac_monitor.target_temp if ac_monitor else "N/A",
ac_swing=ac_monitor.temp_swing if ac_monitor else "N/A",
heater_target=heater_monitor.target_temp if heater_monitor else "N/A",
heater_swing=heater_monitor.temp_swing if heater_monitor else "N/A",
time=time_str
)
return html

74
main.py
View File

@ -1,21 +1,56 @@
from machine import Pin
import time
from scripts.networking import connect_wifi
from scripts.discord_webhook import send_discord_message
from scripts.monitors import TemperatureMonitor, WiFiMonitor, ACMonitor, run_monitors
from scripts.temperature_sensor import TemperatureSensor, get_configured_sensors
from scripts.air_conditioning import ACController
import network
# Initialize pins (LED light onboard)
led = Pin("LED", Pin.OUT)
led.low()
# Hard reset WiFi interface before connecting
print("Initializing WiFi...")
try:
wlan = network.WLAN(network.STA_IF)
wlan.deinit()
time.sleep(2)
print("WiFi interface reset complete")
except Exception as e:
print(f"WiFi reset warning: {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
# Connect to WiFi
wifi = connect_wifi(led)
# Send startup message if connected
# Print WiFi details
if wifi and wifi.isconnected():
ifconfig = wifi.ifconfig()
print("\n" + "="*50)
print("WiFi Connected Successfully!")
print("="*50)
print(f"IP Address: {ifconfig[0]}")
print(f"Subnet Mask: {ifconfig[1]}")
print(f"Gateway: {ifconfig[2]}")
print(f"DNS Server: {ifconfig[3]}")
print(f"Web Interface: http://{ifconfig[0]}")
print("="*50 + "\n")
# Send startup message
send_discord_message("Pico W online and connected ✅")
else:
print("\n" + "="*50)
print("WiFi Connection Failed!")
print("="*50 + "\n")
# Start web server
web_server = TempWebServer(port=80)
web_server.start()
# Sensor configuration registry (moved from temperature_sensor.py)
SENSOR_CONFIG = {
@ -32,8 +67,9 @@ SENSOR_CONFIG = {
'alert_low': 68.0
}
}
# Initialize sensors based on configuration
def get_configured_sensors(): # define the function here
def get_configured_sensors():
"""Return dictionary of configured sensor instances."""
sensors = {}
for key, config in SENSOR_CONFIG.items():
@ -41,7 +77,7 @@ def get_configured_sensors(): # define the function here
return sensors
# Get configured sensors
sensors = get_configured_sensors() # Call the function here
sensors = get_configured_sensors()
# AC Controller options
ac_controller = ACController(
@ -52,16 +88,32 @@ ac_controller = ACController(
ac_monitor = ACMonitor(
ac_controller=ac_controller,
temp_sensor=sensors['inside'], # <-- This is your inside temperature sensor
temp_sensor=sensors['inside'],
target_temp=77.0, # target temperature in Fahrenheit
temp_swing=1.0, # temp swing target_temp-temp_swing to target_temp+temp_swing
interval=30 # check temp every x seconds
)
# Heater Controller options
heater_controller = HeaterController(
relay_pin=16,
min_run_time=30, # min run time in seconds
min_off_time=5 # min off time in seconds
)
heater_monitor = HeaterMonitor(
heater_controller=heater_controller,
temp_sensor=sensors['inside'],
target_temp=80.0, # target temperature in Fahrenheit
temp_swing=2.0, # temp swing
interval=30 # check temp every x seconds
)
# Set up monitors
monitors = [
WiFiMonitor(wifi, led, interval=5, reconnect_cooldown=60), # Wifi monitor, Check WiFi every 5s
ac_monitor, # AC monitor
heater_monitor, # Heater monitor
TemperatureMonitor( # Inside temperature monitor
sensor=sensors['inside'],
label=SENSOR_CONFIG['inside']['label'],
@ -84,7 +136,11 @@ monitors = [
),
]
print("Starting monitoring loop...")
print("Press Ctrl+C to stop\n")
# Main monitoring loop
while True:
run_monitors(monitors)
web_server.check_requests(sensors, ac_monitor, heater_monitor)
time.sleep(0.1)