article
Контур отопления
Сценарий algo_itp.py ведёт один контур отопления: погодозависимая уставка подачи, смесительный клапан 0–10 В, циркуляционный насос.
Возможности
- Кривая подачи. По температуре улицы считается уставка: холоднее на улице — горячее в подаче. Границы кривой задаются регистрами (улица мин / «лето от», подача мин / макс).
- ПИД клапана. Клапан на AO0 (0 В закрыт, 10 В открыт). На веб-странице видно текущий процент.
- Насос с последовательностью. Пуск → пауза 10 с → работа. При останове клапан закрывается, насос ещё 20 с гоняет контур, потом стоп.
- Ночное снижение. Если включён ночной график, с 22:00 до 06:00 уставка подачи снижается на заданное число градусов (не ниже «подача мин»).
- Летний стоп. Если нет угрозы заморозки и улица теплее порога «лето от» — контур не греет.
- Защита от замораживания. Холодная подача (ниже 8 °C) или мороз на улице: насос и клапан работают на максимальную уставку подачи, даже если питание снято.
- Ручной режим. Оператор задаёт процент клапана, насос включён.
- Контроль обвязки. Обрыв NTC улицы или подачи, нет потока после пуска (30 с), термореле насоса — авария и безопасный стоп (клапан 0, насос выкл).
- Наработка насоса в часах (счёт до перезагрузки контроллера).
Питание, режим, уставки кривой и ночи — с страницы Управление. Статус, уставка, клапан и часы — только чтение.
Сферы применения
Одна установка, один смесительный контур — не ЦТП района и не каскад котлов.
- индивидуальный тепловой пункт квартиры, таунхауса, небольшого здания;
- подмешивание в контуре тёплого пола или радиаторов после готового источника (котёл, теплосеть);
- теплица или бытовка, где нужен погодозависимый контур, а не только «вкл/выкл ТЭН»;
- учебный или пилотный ИТП: кривая, клапан, насос и журнал в браузере без отдельной панели.
Не закрывает: несколько независимых контуров на одном контроллере (не хватит AO/насосов), погодозависимое управление котлом по шине, коммерческий учёт тепла.
Клеммы
| Клемма | Назначение |
|---|---|
| AI0 NTC | улица |
| AI1 NTC | подача |
| AO0 0–10 В | смесительный клапан |
| DO0 | насос циркуляции |
| DI0 НО | реле потока (есть поток = замкнут) |
| DI1 НЗ | термореле насоса (норма = замкнут) |
Сценарий
'''
Небольшой ИТП (тепловой пункт).
Погодозависимая кривая подачи, ПИД смесительного клапана 0–10 В,
циркуляционный насос, ночное снижение, защита от замораживания.
Клеммы:
AI0 NTC температура улицы
AI1 NTC температура подачи
AO0 0-10В смесительный клапан (0 В = закрыт)
DO0 насос циркуляции
DI0 НО реле потока (True = есть поток)
DI1 НЗ термореле насоса (True = норма)
'''
from dev import Ai, Ao, Do, Di, Reg, Alarm, Event, PID, count, webconf, run, conv
from time import localtime
from math import isnan
from micropython import const
STEP = const(1)
STATUSES = ['Выкл', 'Пуск насоса', 'Работа', 'Стоп насоса']
ST_OFF = const(0)
ST_START = const(1)
ST_WORK = const(2)
ST_STOP = const(3)
TIME_START = const(10)
TIME_STOP = const(20)
FROST_SUPPLY = const(8)
count(Reg=13, Ai=2, Ao=1, Do=1, Di=2, Alarm=5, Event=2)
webconf(Reg=True, Pin=True, Display=False, Console=True)
rPower = Reg(0).conf('bool', 'Питание')
rMode = Reg(1).conf('enum', 'Режим', items=['Авто', 'Ручной'], default=0)
rValveMan = Reg(2).conf('int', 'Клапан ручн. %', limits=[0, 100], default=0)
rTnarMin = Reg(3).conf('int', 'Улица кривая мин', limits=[-30, 0], default=-25)
rTnarMax = Reg(4).conf('int', 'Лето от, °C', limits=[5, 20], default=10)
rTpodMin = Reg(5).conf('int', 'Подача мин', limits=[30, 55], default=40)
rTpodMax = Reg(6).conf('int', 'Подача макс', limits=[50, 90], default=75)
rNight = Reg(7).conf('int', 'Ночь −°C', limits=[0, 15], default=5)
rSched = Reg(8).conf('bool', 'Ночной график')
rStatus = Reg(9).conf('enum', 'Статус', items=STATUSES, access='r')
rTset = Reg(10).conf('int', 'Уставка подачи', access='r')
rValve = Reg(11).conf('int', 'Клапан %', access='r')
rHours = Reg(12).conf('uint', 'Насос, ч', access='r')
aiOut = Ai(0).conf(name='Улица', type='ntc', access='r')
aiSup = Ai(1).conf(name='Подача', type='ntc', access='r')
aoVal = Ao(0).conf(name='Клапан 0-10В', access='r')
doPump = Do(0).conf(name='Насос', access='r')
diFlow = Di(0).conf(name='Поток', type='no', access='r')
diThrm = Di(1).conf(name='Термореле насоса', type='nz', access='r')
aOut = Alarm(0).conf('Ошибка датчика улицы')
aSup = Alarm(1).conf('Ошибка датчика подачи')
aFrost = Alarm(2).conf('Заморозка контура')
aFlow = Alarm(3).conf('Нет потока')
aPump = Alarm(4).conf('Авария насоса')
ePower = Event(0).conf('Питание')
ePump = Event(1).conf('Насос')
pid = PID(kP=2.0, kI=0.05, kD=0.0, limits=[0, 100])
status = ST_OFF
timer = 0
flowWait = 0
hourSec = 0
def log(msg):
t = localtime()
print('{0}/{1:02}/{2:02} {3:02}:{4:02}:{5:02} {6}'.format(
t[0], t[1], t[2], t[3], t[4], t[5], msg))
def setPump(on, reason):
if doPump.val() == on:
return
doPump.val(on)
ePump.add(1 if on else 0)
log('Насос {} ({})'.format('вкл' if on else 'выкл', reason))
def setValve(percent):
if percent < 0:
percent = 0
if percent > 100:
percent = 100
rValve.val(int(percent))
aoVal.val(conv(percent, 0, 100, 0.0, 10.0))
def setStatus(st):
global status, timer
status = st
rStatus.val(st)
if st == ST_START:
timer = TIME_START
elif st == ST_STOP:
timer = TIME_STOP
else:
timer = 0
log('Статус ' + STATUSES[st])
def stopSafe(reason):
pid.reset()
setValve(0)
setPump(False, reason)
setStatus(ST_OFF)
def curveSetpoint(tnar):
t0 = rTnarMin.val()
t1 = rTnarMax.val()
p0 = rTpodMax.val()
p1 = rTpodMin.val()
if t1 <= t0:
return p1
tset = conv(tnar, t0, t1, p0, p1)
if rSched.val():
h = localtime()[3]
if h >= 22 or h < 6:
tset -= rNight.val()
if tset < p1:
tset = p1
if tset > p0:
tset = p0
return int(tset)
def step():
global status, timer, flowWait, hourSec
tnar = aiOut.val()
tsup = aiSup.val()
sensorFail = False
if isnan(tnar):
aOut.on()
sensorFail = True
else:
aOut.off()
if isnan(tsup):
aSup.on()
sensorFail = True
else:
aSup.off()
if not diThrm.val():
if not aPump.val():
aPump.on()
log('Авария термореле насоса')
rPower.val(False)
if sensorFail:
stopSafe('датчик')
rTset.val(0)
return
frost = tsup < FROST_SUPPLY or tnar < -1
if frost and tsup < FROST_SUPPLY:
if not aFrost.val():
aFrost.on()
log('Заморозка подачи {:.1f}'.format(tsup))
else:
aFrost.off()
if rPower.changed():
ePower.add(1 if rPower.val() else 0)
log('Питание {}'.format('вкл' if rPower.val() else 'выкл'))
if not rPower.val() and not frost:
setStatus(ST_STOP if doPump.val() else ST_OFF)
wantHeat = rPower.val() or frost
summer = (not frost) and tnar > rTnarMax.val()
if summer:
wantHeat = False
tset = curveSetpoint(tnar)
if frost:
tset = rTpodMax.val()
rTset.val(tset)
if rMode.val() == 1 and rPower.val() and not aPump.val():
setPump(True, 'ручной')
setValve(rValveMan.val())
rStatus.val(ST_WORK)
status = ST_WORK
if doPump.val():
hourSec += 1
if hourSec >= 3600:
hourSec = 0
rHours.inc()
return
if not wantHeat:
if status == ST_WORK or status == ST_START:
setStatus(ST_STOP)
if status == ST_STOP:
setValve(0)
if timer > 0:
timer -= 1
else:
stopSafe('стоп')
else:
setValve(0)
setPump(False, 'выкл')
rStatus.val(ST_OFF)
return
if status == ST_OFF:
setStatus(ST_START)
setPump(True, 'пуск')
setValve(0)
flowWait = 0
return
if status == ST_START:
setPump(True, 'пуск')
if timer > 0:
timer -= 1
return
setStatus(ST_WORK)
return
if status == ST_WORK:
setPump(True, 'работа')
u = pid(tsup, tset)
setValve(u)
if not diFlow.val():
flowWait += 1
if flowWait > 30:
if not aFlow.val():
aFlow.on()
log('Нет потока')
rPower.val(False)
stopSafe('нет потока')
return
else:
flowWait = 0
aFlow.off()
hourSec += 1
if hourSec >= 3600:
hourSec = 0
rHours.inc()
rStatus.val(status)
doPump.off()
setValve(0)
rStatus.val(ST_OFF)
log('Старт ИТП')
run(step, STEP)