Files
wb_heat_control/heat_control.py
T

236 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Wirenboard Temperature Monitoring Script
Monitors temperature sensors and controls GPIO based on threshold
"""
import json
import time
import logging
from typing import Dict, Any
import paho.mqtt.client as mqtt
import threading
from optparse import OptionParser
from contour import Contour
parser = OptionParser()
parser.add_option("--debug", action="store_true")
(options, args) = parser.parse_args()
CONFIG_FILE = "config.json"
# will turn on boiler after this amount of opened contour time
# (this is to make sure valve has time for opening)
BOILER_TURN_ON_AFTER = 90
# will turn off boiler if it's exhaust temperature is too high
BOILER_EXHAUST_LIMIT = 50
contours = []
boiler_exhaust_t = 0
config: Dict[str, Any] = {}
# MQTT client
mqtt_client = None
# Setup logging
logging_level = logging.INFO
if (options.debug):
logging_level = logging.DEBUG
logging.basicConfig(
level = logging_level,
format = '%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def load_config():
"""Load configuration from file"""
global config, contours, target_temp, boiler_control, hysteresis, pump_control
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
for contour_data in config['contours']:
contours.append(Contour(contour_data, logger))
target_temp = float(config.get('target_temperature'))
hysteresis = float(config.get('hysteresis'))
boiler_control = config.get('boiler_control')
pump_control = config.get('pump_control')
if (boiler_control is None):
logger.error("Boiler control not configured")
exit(1)
logger.info(f"Configuration loaded: {config}")
except FileNotFoundError:
logger.error(f"Config file not found: {CONFIG_FILE}")
raise
except json.JSONDecodeError:
logger.error(f"Invalid JSON in config file: {CONFIG_FILE}")
raise
except Exception as e:
logger.error(f"Error loading config: {e}")
raise
def on_connect(client, userdata, flags, rc):
"""MQTT connection callback"""
global contours
if rc == 0:
logger.info("Connected to MQTT broker")
# Subscribe to all temperature sensor topics
for k in contours:
client.subscribe(k.sensor_topic)
client.subscribe(k.target_topic)
logger.info(f"Subscribed to: {k.sensor_topic}")
# and boiler exhaust topic
client.subscribe(config.get('boiler_exhaust_t'))
else:
logger.error(f"Connection failed with code: {rc}")
def on_message(client, userdata, msg):
"""MQTT message callback - store temperature values"""
global contours, boiler_exhaust_t
try:
for k in contours:
if (k.sensor_topic == msg.topic):
temperature = float(msg.payload.decode('utf-8'))
k.update(temperature)
logger.debug(f"Updated {k.sensor_topic}: {temperature}°C")
if (k.target_topic == msg.topic):
temperature = float(msg.payload.decode('utf-8'))
k.update_target(temperature)
logger.debug(f"Updated target {k.target_topic}: {temperature}°C")
if msg.topic == config.get('boiler_exhaust_t'):
boiler_exhaust_t = float(msg.payload.decode('utf-8'))
logger.debug(f"Updated boiler exhaust: {boiler_exhaust_t}°C")
except ValueError:
logger.error(f"Invalid temperature value: {msg.payload}")
except Exception as e:
logger.error(f"Error processing message: {e}")
any_opened_start = 0
any_opened = False
def check_temperatures():
"""Check temperatures against target and control GPIO"""
global any_opened_start, any_opened
# check boiler exhaust, turn off if too big
if (boiler_exhaust_t > BOILER_EXHAUST_LIMIT):
logger.error("Boiler exhaust overheat")
# will open all contours (to force coolant circulation),
# turn on the pump
# and turn off the boiler of course
for c in contours:
# valves are normally-opened, so 0 means "open"
mqtt_client.publish(c.out_topic, "0", retain=True)
mqtt_client.publish(boiler_control, "0", retain=True)
mqtt_client.publish(pump_control, "1", retain=True)
return
#
sum_t = 0
count_t = 0
for c in contours:
contour_t = c.get_value()
if contour_t is None:
continue
sum_t = sum_t + contour_t
count_t = count_t + 1
if count_t == 0:
logger.error("No temperatures available, switch to full-on mode")
for c in contours:
# valves are normally-opened, so 0 means "open"
mqtt_client.publish(c.out_topic, "0", retain=True)
# turn on boiler
mqtt_client.publish(boiler_control, "1", retain=True)
return
for c in contours:
contour_t = c.get_value()
# use average value from all other sensors if contour's
# sensor is offline
if contour_t is None:
contour_t = sum_t / count_t
target_t = target_temp
if not c.target_value is None:
target_t = c.target_value
if contour_t < target_t - hysteresis:
# should turn on contour
c.check_state(True)
if contour_t > target_t + hysteresis:
# should turn off contour
c.check_state(False)
if c.state:
# 0 means "open"
mqtt_client.publish(c.out_topic, "0", retain=True)
else:
mqtt_client.publish(c.out_topic, "1", retain=True)
if any_opened:
any_opened = False
for c in contours:
if c.state:
any_opened = True
else:
for c in contours:
if c.state:
any_opened = True
any_opened_start = time.time()
logger.debug("Any opened: " + str(any_opened) + " aos " + str(any_opened_start) + " time " + str(time.time()))
if any_opened and time.time() - any_opened_start > BOILER_TURN_ON_AFTER:
mqtt_client.publish(boiler_control, "1", retain=True)
mqtt_client.publish(pump_control, "1", retain=True)
else:
mqtt_client.publish(boiler_control, "0", retain=True)
mqtt_client.publish(pump_control, "0", retain=True)
def main_cycle():
"""Main 5-second cycle"""
while True:
try:
check_temperatures()
except Exception as e:
logger.error(f"Error in main cycle: {e}")
# Wait 5 seconds
time.sleep(5)
def setup_mqtt():
"""Setup and connect MQTT client"""
global mqtt_client
mqtt_client = mqtt.Client(client_id="wb_temperature_monitor")
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
# Configure from config
mqtt_host = config.get('mqtt_host', 'localhost')
mqtt_port = config.get('mqtt_port', 1883)
mqtt_username = config.get('mqtt_username', '')
mqtt_password = config.get('mqtt_password', '')
if mqtt_username:
mqtt_client.username_pw_set(mqtt_username, mqtt_password)
try:
mqtt_client.connect(mqtt_host, mqtt_port, 60)
mqtt_client.loop_start()
except Exception as e:
logger.error(f"Failed to connect to MQTT broker: {e}")
raise
def main():
logger.info("Starting Wirenboard Temperature Monitor")
load_config()
setup_mqtt()
# Start main cycle in a separate thread
# This allows MQTT to run in background
cycle_thread = threading.Thread(target=main_cycle, daemon=True)
cycle_thread.start()
# Keep main thread alive
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Shutting down...")
mqtt_client.loop_stop()
mqtt_client.disconnect()
if __name__ == "__main__":
main()