Сценарий 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)