Compare commits

...

9 Commits

View File

@@ -50,7 +50,7 @@ class TempWebServer:
content_length = int(line.split(':')[1].strip())
break
# If POST request with body, read remaining data
# If POST request with body, read remaining data
if 'POST' in request and content_length > 0:
# Check how much body we already have
header_end = request.find('\r\n\r\n') + 4
@@ -109,15 +109,15 @@ class TempWebServer:
# Send headers
conn.sendall(b'HTTP/1.1 200 OK\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('Connection: close\r\n')
conn.send('\r\n')
conn.sendall('Content-Length: {}\r\n'.format(len(response_bytes)).encode('utf-8'))
conn.sendall(b'Connection: close\r\n')
conn.sendall(b'\r\n')
# Send body in chunks (MicroPython has small socket buffer)
chunk_size = 1024 # Send 1KB at a time
for i in range(0, len(response_bytes), chunk_size):
chunk = response_bytes[i:i+chunk_size]
conn.send(chunk)
conn.sendall(chunk)
print("DEBUG: Sent chunk {} ({} bytes)".format(i//chunk_size + 1, len(chunk)))
conn.close()
@@ -128,16 +128,16 @@ class TempWebServer:
response = self._get_settings_page(sensors, ac_monitor, heater_monitor)
response_bytes = response.encode('utf-8')
conn.send('HTTP/1.1 200 OK\r\n')
conn.send('Content-Type: text/html; charset=utf-8\r\n')
conn.send('Content-Length: {}\r\n'.format(len(response_bytes)))
conn.send('Connection: close\r\n')
conn.send('\r\n')
conn.sendall(b'HTTP/1.1 200 OK\r\n')
conn.sendall(b'Content-Type: text/html; charset=utf-8\r\n')
conn.sendall('Content-Length: {}\r\n'.format(len(response_bytes)).encode('utf-8'))
conn.sendall(b'Connection: close\r\n')
conn.sendall(b'\r\n')
chunk_size = 1024
for i in range(0, len(response_bytes), chunk_size):
chunk = response_bytes[i:i+chunk_size]
conn.send(chunk)
conn.sendall(chunk)
conn.close()
print("DEBUG: Settings page sent successfully ({} bytes total)".format(len(response_bytes)))
@@ -161,10 +161,27 @@ class TempWebServer:
print("DEBUG: Redirect sent, connection closed")
return
elif 'GET /sched.js' in request:
js = self._build_sched_js() # bytes
conn.sendall(b'HTTP/1.1 200 OK\r\n')
conn.sendall(b'Content-Type: application/javascript; charset=utf-8\r\n')
conn.sendall('Content-Length: {}\r\n'.format(len(js)).encode('utf-8'))
conn.sendall(b'Cache-Control: max-age=300\r\n')
conn.sendall(b'Connection: close\r\n')
conn.sendall(b'\r\n')
conn.sendall(js)
conn.close()
return
elif 'GET /ping' in request:
# Quick health check endpoint (no processing)
conn.send('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n')
conn.sendall(b'OK')
body = b'OK'
conn.sendall(b'HTTP/1.1 200 OK\r\n')
conn.sendall(b'Content-Type: text/plain\r\n')
conn.sendall(b'Content-Length: 2\r\n')
conn.sendall(b'Connection: close\r\n')
conn.sendall(b'\r\n')
conn.sendall(body)
conn.close()
return
@@ -175,7 +192,7 @@ class TempWebServer:
response = self._get_status_page(sensors, ac_monitor, heater_monitor, schedule_monitor)
# ===== START: Send response with proper HTTP headers =====
print("DEBUG: Sending response ({} bytes)".format(len(response)))
print("DEBUG: Sending response ({} bytes)".format(len(response.encode('utf-8'))))
try:
# Check if response already has HTTP headers (like redirects)
if response.startswith('HTTP/1.1'):
@@ -183,11 +200,11 @@ class TempWebServer:
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(b'HTTP/1.1 200 OK\r\n')
conn.sendall(b'Content-Type: text/html; charset=utf-8\r\n')
conn.sendall('Content-Length: {}\r\n'.format(len(response.encode('utf-8'))).encode('utf-8'))
conn.sendall(b'Connection: close\r\n')
conn.sendall(b'\r\n') # Blank line separates headers from body
conn.sendall(response.encode('utf-8'))
print("DEBUG: Response sent successfully")
@@ -205,6 +222,23 @@ class TempWebServer:
import sys
sys.print_exception(e)
def _build_sched_js(self):
# Keep this as bytes; no .format() so no brace escaping and less RAM churn
return (b"// schedule page sync\n"
b"window.schedSync=function(i,w){var h=document.querySelector('input[name=\"schedule_'+i+'_heater\"]');"
b"var a=document.querySelector('input[name=\"schedule_'+i+'_ac\"]');"
b"var l=document.getElementById('schedule_'+i+'_last_changed');"
b"if(!h||!a)return;var hv=parseFloat(h.value),av=parseFloat(a.value);"
b"if(w==='heater'){if(!isNaN(hv)&&!isNaN(av)&&hv>av){a.value=hv;} if(l)l.value='heater';}"
b"else{if(!isNaN(hv)&&!isNaN(av)&&av<hv){h.value=av;} if(l)l.value='ac';}};"
b"document.addEventListener('DOMContentLoaded',function(){var f=document.querySelector('form[action=\"/schedule\"]');"
b"if(!f)return;f.addEventListener('submit',function(){for(var i=0;i<4;i++){"
b"var h=document.querySelector('input[name=\"schedule_'+i+'_heater\"]');"
b"var a=document.querySelector('input[name=\"schedule_'+i+'_ac\"]');"
b"var l=document.getElementById('schedule_'+i+'_last_changed');"
b"if(!h||!a)continue;var hv=parseFloat(h.value),av=parseFloat(a.value);"
b"if(isNaN(hv)||isNaN(av))continue;if(hv>av){if(l&&l.value==='ac'){h.value=av;}else{a.value=hv;}}}});});")
def _save_config_to_file(self, config):
"""Save configuration to config.json file (atomic write)."""
try:
@@ -350,6 +384,10 @@ class TempWebServer:
pass
# ===== END: Handle mode actions =====
# Load previous schedules to compute deltas
prev = self._load_config()
prev_schedules = prev.get('schedules', [])
# ===== START: Handle schedule configuration save =====
# DEBUG: Print what we received
print("DEBUG: Received POST body parameters:")
@@ -427,11 +465,34 @@ class TempWebServer:
"Schedule {}: Temperature values must be numbers".format(i+1),
sensors, ac_monitor, heater_monitor
)
# Auto-sync both ways
if heater_target > ac_target:
ac_target = heater_target
elif ac_target < heater_target:
heater_target = ac_target
# Sync using direction of change (no dependency on last_changed)
prev_h = None
prev_a = None
if i < len(prev_schedules):
try:
prev_h = float(prev_schedules[i].get('heater_target', heater_target))
except:
prev_h = None
try:
prev_a = float(prev_schedules[i].get('ac_target', ac_target))
except:
prev_a = None
delta_h = (heater_target - prev_h) if prev_h is not None else None
delta_a = (ac_target - prev_a) if prev_a is not None else None
if ac_target < heater_target:
# AC moved down -> lower heater
if delta_a is not None and delta_a < 0 and (delta_h is None or abs(delta_h) < 1e-9):
heater_target = ac_target
# Heater moved up -> raise AC
elif delta_h is not None and delta_h > 0 and (delta_a is None or abs(delta_a) < 1e-9):
ac_target = heater_target
else:
# Fallback preference: if AC decreased more, lower heater; else raise AC
if delta_a is not None and delta_h is not None and abs(delta_a) > abs(delta_h):
heater_target = ac_target
else:
ac_target = heater_target
# Create schedule entry
schedule = {
'time': schedule_time,
@@ -554,13 +615,28 @@ class TempWebServer:
new_heater_target = params.get('heater_target', config.get('heater_target', 80.0))
new_ac_target = params.get('ac_target', config.get('ac_target', 77.0))
# Auto-sync both ways
if new_heater_target > new_ac_target:
new_ac_target = new_heater_target
params['ac_target'] = new_ac_target
elif new_ac_target < new_heater_target:
new_heater_target = new_ac_target
params['heater_target'] = new_heater_target
# Use previous values to detect direction of change
old_heater = float(config.get('heater_target', new_heater_target))
old_ac = float(config.get('ac_target', new_ac_target))
# If AC is below heater, sync based on the field that moved
if new_ac_target < new_heater_target:
# AC moved down: lower heater to AC
if new_ac_target < old_ac and new_heater_target == old_heater:
new_heater_target = new_ac_target
# Heater moved up: raise AC to heater
elif new_heater_target > old_heater and new_ac_target == old_ac:
new_ac_target = new_heater_target
else:
# Fallback: prefer AC drop rule, else heater raise
if new_ac_target < old_ac:
new_heater_target = new_ac_target
else:
new_ac_target = new_heater_target
# Reflect adjusted values back to params
params['ac_target'] = new_ac_target
params['heater_target'] = new_heater_target
# ===== END: Validate Heat <= AC =====
# ===== START: Update AC Settings =====
@@ -643,7 +719,11 @@ class TempWebServer:
# ===== FORCE GARBAGE COLLECTION BEFORE BIG ALLOCATION =====
import gc # type: ignore
gc.collect()
print("DEBUG: Memory freed, {} bytes available".format(gc.mem_free()))
try:
mf = gc.mem_free() # type: ignore
print("DEBUG: Memory freed, {} bytes available".format(mf))
except Exception:
print("DEBUG: Memory collected")
# ===== END GARBAGE COLLECTION =====
try:
@@ -1304,8 +1384,8 @@ document.addEventListener('DOMContentLoaded', function() {{
schedules.append({
'time': '',
'name': '',
'ac_target': config.get('ac_target', 75.0), # ✅ Uses 78°F from config
'heater_target': config.get('heater_target', 72.0) # ✅ Uses 70°F from config
'ac_target': config.get('ac_target', 75.0), # default if not set
'heater_target': config.get('heater_target', 72.0) # default if not set
})
# ===== DEBUG: Verify we have 4 schedules =====
@@ -1336,6 +1416,8 @@ document.addEventListener('DOMContentLoaded', function() {{
# Hidden input to mark this schedule exists (always sent)
schedule_inputs += '<input type="hidden" name="schedule_' + str(i) + '_exists" value="1">\n'
# Hidden marker to record which input was changed last for this row
schedule_inputs += '<input type="hidden" name="schedule_' + str(i) + '_last_changed" id="schedule_' + str(i) + '_last_changed" value="">\n'
schedule_inputs += '<label>Time</label>\n'
schedule_inputs += '<input type="time" name="schedule_' + str(i) + '_time" value="' + str(time_value) + '">\n'
@@ -1343,10 +1425,10 @@ document.addEventListener('DOMContentLoaded', function() {{
schedule_inputs += '<input type="text" name="schedule_' + str(i) + '_name" value="' + str(name_value) + '" placeholder="Schedule ' + str(i+1) + '">\n'
schedule_inputs += '<label>Heater (°F)</label>\n'
# Add required attribute to force validation
schedule_inputs += '<input type="number" name="schedule_' + str(i) + '_heater" value="' + str(heater_value) + '" step="0.5" min="60" max="85" required>\n'
schedule_inputs += "<input type=\"number\" name=\"schedule_" + str(i) + "_heater\" value=\"" + str(heater_value) + "\" step=\"0.5\" min=\"60\" max=\"85\" required oninput=\"schedSync(" + str(i) + ", 'heater')\" onchange=\"schedSync(" + str(i) + ", 'heater')\">\n"
schedule_inputs += '<label>AC (°F)</label>\n'
# Add required attribute to force validation
schedule_inputs += '<input type="number" name="schedule_' + str(i) + '_ac" value="' + str(ac_value) + '" step="0.5" min="60" max="90" required>\n'
schedule_inputs += "<input type=\"number\" name=\"schedule_" + str(i) + "_ac\" value=\"" + str(ac_value) + "\" step=\"0.5\" min=\"60\" max=\"90\" required oninput=\"schedSync(" + str(i) + ", 'ac')\" onchange=\"schedSync(" + str(i) + ", 'ac')\">\n"
schedule_inputs += '</div>\n'
print("DEBUG: HTML generated, length now: {} bytes".format(len(schedule_inputs)))
@@ -1425,6 +1507,7 @@ document.addEventListener('DOMContentLoaded', function() {{
}}
.btn:hover {{ transform: translateY(-2px); }}
</style>
</head>
<body>
<div class="container">
@@ -1461,34 +1544,7 @@ document.addEventListener('DOMContentLoaded', function() {{
To change modes (Automatic/Hold), return to the dashboard
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {{
for (var i = 0; i < 4; i++) {{
var heaterInput = document.querySelector('input[name="schedule_' + i + '_heater"]');
var acInput = document.querySelector('input[name="schedule_' + i + '_ac"]');
if (heaterInput && acInput) {{
heaterInput.addEventListener('input', function() {{
var idx = this.name.match(/\\d+/)[0];
var acInput = document.querySelector('input[name="schedule_' + idx + '_ac"]');
var heaterVal = parseFloat(this.value);
var acVal = parseFloat(acInput.value);
if (!isNaN(heaterVal) && heaterVal > acVal) {{
acInput.value = heaterVal;
}}
}});
acInput.addEventListener('input', function() {{
var idx = this.name.match(/\\d+/)[0];
var heaterInput = document.querySelector('input[name="schedule_' + idx + '_heater"]');
var heaterVal = parseFloat(heaterInput.value);
var acVal = parseFloat(this.value);
if (!isNaN(acVal) && acVal < heaterVal) {{
heaterInput.value = acVal;
}}
}});
}}
}}
}});
</script>
<script defer src="/sched.js"></script>
</body>
</html>
""".format(