Логика включения-выключения контуров и котла. Конфиг контуров, гистерезис и прочее

This commit is contained in:
ftc
2025-12-07 14:56:25 +03:00
parent b4e77a02d6
commit 4a84636d0e
4 changed files with 226 additions and 134 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
config.json
+30 -4
View File
@@ -1,9 +1,35 @@
{ {
"mqtt_host": "localhost", "mqtt_host": "192.168.0.45",
"mqtt_port": 1883, "mqtt_port": 1883,
"mqtt_username": "", "mqtt_username": "",
"mqtt_password": "", "mqtt_password": "",
"temperature_sensors": ["sensor1", "sensor2", "wb-ms-12v"], "contours": [
"target_temperature": 20.0, {
"control_topic": "devices/wb-gpio/controls/EXT1_R3A1" "sensor": "/devices/workroom_weather/controls/t",
"control": "/devices/wb-gpio/controls/EXT1_R3A1"
},
{
"sensor": "/devices/bathroom_weather/controls/t",
"control": "/devices/wb-gpio/controls/EXT1_R3A2"
},
{
"sensor": "/devices/entrance_weather/controls/t",
"control": "/devices/wb-gpio/controls/EXT1_R3A3"
},
{
"sensor": "/devices/corridor_weather/controls/t",
"control": "/devices/wb-gpio/controls/EXT1_R3A4"
},
{
"sensor": "/devices/techroom_weather/controls/t",
"control": "/devices/wb-gpio/controls/EXT1_R3A5"
},
{
"sensor": "/devices/kitchen_weather/controls/t",
"control": "/devices/wb-gpio/controls/EXT1_R3A6"
}
],
"target_temperature": 21.0,
"hysteresis": 0.5,
"boiler_control": "/devices/wb-gpio/controls/EXT1_R3A8"
} }
+32
View File
@@ -0,0 +1,32 @@
import time
class Contour:
EXPIRE_TIME = 120 # sensor value considered expired in 2 minutes
CHANGE_FREQ = 600 # change of state allowed after 10 minutes
current_value = None
value_time = 0
state = None
state_change_time = 0
def __init__(self, config, logger):
self.sensor_topic = config['sensor']
self.out_topic = config['control']
self.logger = logger
def update(self, value):
self.current_value = value
self.value_time = time.time()
def get_value(self):
if (time.time() - self.value_time <= self.EXPIRE_TIME):
return self.current_value
return None
def check_state(self, state_value):
if self.state == state_value:
return
if (self.state is None) or (time.time() - self.state_change_time > self.CHANGE_FREQ):
self.state = state_value
self.logger.info("Contour " + self.sensor_topic + " changed state to " + str(self.state))
+82 -50
View File
@@ -10,30 +10,48 @@ import logging
from typing import Dict, Any from typing import Dict, Any
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import threading import threading
from optparse import OptionParser
from contour import Contour
# Configuration parser = OptionParser()
CONFIG_FILE = "/etc/wb-temperature-monitor/config.json" parser.add_option("--debug", action="store_true")
(options, args) = parser.parse_args()
# Global dictionary for temperature values CONFIG_FILE = "config.json"
temperature_data: Dict[str, float] = {} # 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] = {} config: Dict[str, Any] = {}
# MQTT client # MQTT client
mqtt_client = None mqtt_client = None
# Setup logging # Setup logging
logging_level = logging.INFO
if (options.debug):
logging_level = logging.DEBUG
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level = logging_level,
format = '%(asctime)s - %(levelname)s - %(message)s' format = '%(asctime)s - %(levelname)s - %(message)s'
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_config(): def load_config():
"""Load configuration from file""" """Load configuration from file"""
global config global config, contours, target_temp, boiler_control, hysteresis
try: try:
with open(CONFIG_FILE, 'r') as f: with open(CONFIG_FILE, 'r') as f:
config = json.load(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}") logger.info(f"Configuration loaded: {config}")
except FileNotFoundError: except FileNotFoundError:
logger.error(f"Config file not found: {CONFIG_FILE}") logger.error(f"Config file not found: {CONFIG_FILE}")
@@ -47,64 +65,84 @@ def load_config():
def on_connect(client, userdata, flags, rc): def on_connect(client, userdata, flags, rc):
"""MQTT connection callback""" """MQTT connection callback"""
global contours
if rc == 0: if rc == 0:
logger.info("Connected to MQTT broker") logger.info("Connected to MQTT broker")
# Subscribe to all temperature sensor topics # Subscribe to all temperature sensor topics
for sensor in config.get('temperature_sensors', []): for k in contours:
topic = f"devices/{sensor}/controls/t" client.subscribe(k.sensor_topic)
client.subscribe(topic) logger.info(f"Subscribed to: {k.sensor_topic}")
logger.info(f"Subscribed to: {topic}")
else: else:
logger.error(f"Connection failed with code: {rc}") logger.error(f"Connection failed with code: {rc}")
def on_message(client, userdata, msg): def on_message(client, userdata, msg):
"""MQTT message callback - store temperature values""" """MQTT message callback - store temperature values"""
try: try:
# Extract sensor name from topic for k in contours:
# Topic format: devices/%NAME%/controls/t if (k.sensor_topic == msg.topic):
parts = msg.topic.split('/')
if len(parts) >= 2:
sensor_name = parts[1] # Get the %NAME% part
# Convert payload to float
temperature = float(msg.payload.decode('utf-8')) temperature = float(msg.payload.decode('utf-8'))
k.update(temperature)
# Store in global dictionary logger.debug(f"Updated {k.sensor_topic}: {temperature}°C")
temperature_data[sensor_name] = temperature
logger.debug(f"Updated {sensor_name}: {temperature}°C")
except ValueError: except ValueError:
logger.error(f"Invalid temperature value: {msg.payload}") logger.error(f"Invalid temperature value: {msg.payload}")
except Exception as e: except Exception as e:
logger.error(f"Error processing message: {e}") logger.error(f"Error processing message: {e}")
any_opened_start = 0
any_opened = False
def check_temperatures(): def check_temperatures():
"""Check temperatures against target and control GPIO""" """Check temperatures against target and control GPIO"""
target_temp = config.get('target_temperature') global any_opened_start, any_opened
control_topic = config.get('control_topic', 'devices/wb-gpio/controls/EXT1_R3A1') sum_t = 0
count_t = 0
if target_temp is None: for c in contours:
logger.error("Target temperature not configured") 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 return
any_below_target = False for c in contours:
contour_t = c.get_value()
# Check all sensors # use average value from all other sensors if contour's
for sensor, temp in temperature_data.items(): # sensor is offline
if temp < target_temp: if contour_t is None:
logger.info(f"Sensor {sensor} below target: {temp} < {target_temp}") contour_t = sum_t / count_t
any_below_target = True if contour_t < target_temp - hysteresis:
break # No need to check further if one is below # should turn on contour
c.check_state(True)
# Control GPIO if contour_t > target_temp + hysteresis:
if any_below_target: # should turn off contour
# Send "1" to turn on/activate c.check_state(False)
mqtt_client.publish(control_topic, "1", retain=True) if c.state:
logger.info(f"Activated: {control_topic}") # 0 means "open"
mqtt_client.publish(c.out_topic, "0", retain=True)
else: else:
# Send "0" to turn off/deactivate mqtt_client.publish(c.out_topic, "1", retain=True)
mqtt_client.publish(control_topic, "0", retain=True)
logger.debug(f"Deactivated: {control_topic}") 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(): def main_cycle():
"""Main 5-second cycle""" """Main 5-second cycle"""
@@ -113,7 +151,6 @@ def main_cycle():
check_temperatures() check_temperatures()
except Exception as e: except Exception as e:
logger.error(f"Error in main cycle: {e}") logger.error(f"Error in main cycle: {e}")
# Wait 5 seconds # Wait 5 seconds
time.sleep(5) time.sleep(5)
@@ -142,13 +179,8 @@ def setup_mqtt():
raise raise
def main(): def main():
"""Main function"""
logger.info("Starting Wirenboard Temperature Monitor") logger.info("Starting Wirenboard Temperature Monitor")
# Load configuration
load_config() load_config()
# Setup MQTT
setup_mqtt() setup_mqtt()
# Start main cycle in a separate thread # Start main cycle in a separate thread