234 lines
6.7 KiB
Python
234 lines
6.7 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:
|
|
for c in contours:
|
|
if not c.state:
|
|
any_opened = False
|
|
else:
|
|
for c in contours:
|
|
if c.state:
|
|
any_opened = True
|
|
any_opened_start = 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() |