Compare commits
6 Commits
99afba25c4
...
eb34922da6
| Author | SHA1 | Date | |
|---|---|---|---|
| eb34922da6 | |||
| 6156f87b05 | |||
| e82fcf46aa | |||
| f53ae05842 | |||
| 8c92f86842 | |||
| 93b68098ea |
68
Scripts/heating.py
Normal file
68
Scripts/heating.py
Normal 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")
|
||||
@ -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):
|
||||
|
||||
@ -2,36 +2,65 @@ import network
|
||||
import time
|
||||
from secrets import secrets
|
||||
|
||||
RECONNECT_COOLDOWN_MS = 60000 # 60 seconds
|
||||
|
||||
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)
|
||||
|
||||
# print("Connecting to WiFi...", end="")
|
||||
wifi.connect(secrets['ssid'], secrets['password'])
|
||||
|
||||
# Wait for connection with timeout
|
||||
max_wait = timeout
|
||||
while max_wait > 0:
|
||||
if wifi.status() < 0 or wifi.status() >= 3:
|
||||
break
|
||||
max_wait -= 1
|
||||
# print(".", end="")
|
||||
time.sleep(1)
|
||||
def connect_wifi(led=None):
|
||||
"""Connect to WiFi using credentials from secrets.py"""
|
||||
try:
|
||||
wlan = network.WLAN(network.STA_IF)
|
||||
|
||||
if wifi.isconnected():
|
||||
# print("\nConnected! Network config:", wifi.ifconfig())
|
||||
if led:
|
||||
led.on()
|
||||
# Deactivate first if already active (fixes EPERM error)
|
||||
if wlan.active():
|
||||
wlan.active(False)
|
||||
time.sleep(1)
|
||||
led.off()
|
||||
return wifi
|
||||
else:
|
||||
# print("\nConnection failed!")
|
||||
return None
|
||||
|
||||
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 = 20
|
||||
while max_wait > 0:
|
||||
if wlan.isconnected():
|
||||
break
|
||||
if led:
|
||||
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 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
188
Scripts/web_server.py
Normal 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
|
||||
78
main.py
78
main.py
@ -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,20 +77,35 @@ 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(
|
||||
relay_pin=15,
|
||||
min_run_time=30, # min run time in seconds
|
||||
min_off_time=5 # min off time in seconds
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
@ -62,6 +113,7 @@ ac_monitor = ACMonitor(
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user