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

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_username": "",
"mqtt_password": "",
"temperature_sensors": ["sensor1", "sensor2", "wb-ms-12v"],
"target_temperature": 20.0,
"control_topic": "devices/wb-gpio/controls/EXT1_R3A1"
"contours": [
{
"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
import paho.mqtt.client as mqtt
import threading
from optparse import OptionParser
from contour import Contour
# Configuration
CONFIG_FILE = "/etc/wb-temperature-monitor/config.json"
parser = OptionParser()
parser.add_option("--debug", action="store_true")
(options, args) = parser.parse_args()
# Global dictionary for temperature values
temperature_data: Dict[str, float] = {}
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.INFO,
level = logging_level,
format = '%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def load_config():
"""Load configuration from file"""
global config
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}")
@@ -47,64 +65,84 @@ def load_config():
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 sensor in config.get('temperature_sensors', []):
topic = f"devices/{sensor}/controls/t"
client.subscribe(topic)
logger.info(f"Subscribed to: {topic}")
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:
# Extract sensor name from topic
# Topic format: devices/%NAME%/controls/t
parts = msg.topic.split('/')
if len(parts) >= 2:
sensor_name = parts[1] # Get the %NAME% part
# Convert payload to float
for k in contours:
if (k.sensor_topic == msg.topic):
temperature = float(msg.payload.decode('utf-8'))
# Store in global dictionary
temperature_data[sensor_name] = temperature
logger.debug(f"Updated {sensor_name}: {temperature}°C")
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"""
target_temp = config.get('target_temperature')
control_topic = config.get('control_topic', 'devices/wb-gpio/controls/EXT1_R3A1')
if target_temp is None:
logger.error("Target temperature not configured")
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
any_below_target = False
# Check all sensors
for sensor, temp in temperature_data.items():
if temp < target_temp:
logger.info(f"Sensor {sensor} below target: {temp} < {target_temp}")
any_below_target = True
break # No need to check further if one is below
# Control GPIO
if any_below_target:
# Send "1" to turn on/activate
mqtt_client.publish(control_topic, "1", retain=True)
logger.info(f"Activated: {control_topic}")
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:
# Send "0" to turn off/deactivate
mqtt_client.publish(control_topic, "0", retain=True)
logger.debug(f"Deactivated: {control_topic}")
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"""
@@ -113,7 +151,6 @@ def main_cycle():
check_temperatures()
except Exception as e:
logger.error(f"Error in main cycle: {e}")
# Wait 5 seconds
time.sleep(5)
@@ -142,13 +179,8 @@ def setup_mqtt():
raise
def main():
"""Main function"""
logger.info("Starting Wirenboard Temperature Monitor")
# Load configuration
load_config()
# Setup MQTT
setup_mqtt()
# Start main cycle in a separate thread