#!/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 contours = [] 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 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') 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) logger.info(f"Subscribed to: {k.sensor_topic}") else: logger.error(f"Connection failed with code: {rc}") def on_message(client, userdata, msg): """MQTT message callback - store temperature values""" 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") 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 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 if contour_t < target_temp - hysteresis: # should turn on contour c.check_state(True) if contour_t > target_temp + 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) else: mqtt_client.publish(boiler_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()