Сценарий algo_pump.py держит давление в магистрали двумя насосами: включает по гистерезису, крутит по наработке, не пускает оба сразу после остановки.

Возможности

  • Давление с датчика 0–10 В на AI2. 0 В = 0 бар, 10 В = шкала «Pмакс» (по умолчанию 10 бар). Текущее давление на экране.
  • Гистерезис. Ниже уставки минус гистерезис — пуск следующего насоса. Выше уставки плюс гистерезис — стоп того, кто дольше работает.
  • Ротация. Если крутится один насос дольше заданных часов, его меняют на резервный (у кого меньше наработка). Ротация 0 ч — не переключать по часам.
  • Пауза между пусками (секунды), чтобы не дёргать сеть и не бить гидроудар. На экране видно, сколько осталось ждать.
  • Сухой ход. Нет воды в скважине / ёмкости — оба насоса стоп, авария.
  • Термореле на каждый насос: аварийный снимается с линии, второй может продолжать.
  • Высокое давление (около 95% шкалы датчика) — стоп обоих, сброс питания, авария.
  • Часы наработки каждого насоса (до перезагрузки контроллера).

Питание, уставка, гистерезис, шкала датчика, период ротации и пауза пуска — с Управления.

Сферы применения

Два насоса и один датчик давления — типичный малый узел, не КНС на весь посёлок.

  • скважина или колодец с накопителем / гидроаккумулятором;
  • повысительная насосная небольшого здания, полива, мойки;
  • циркуляция или подпитка контура, где важно «не гонять один насос до смерти»;
  • удалённый объект: давление, сухой ход и термоконтакты в журнале, уведомление через сервер.

Не закрывает: частотное регулирование (нет 0–10 В на ПЧ в этом сценарии — только пуск/стоп DO), три и более насосов, пожарный насос с сертификацией, измерение расхода.

Клеммы

КлеммаНазначение
AI2 0–10 Вдавление (AI0 и AI1 не используются)
DO0, DO1насос 1 и 2
DI0 НЗесть вода (норма = замкнут)
DI1, DI2 НЗтермореле насосов (норма = замкнут)

Сценарий

  '''
Насосная / скважина с двумя насосами.

Поддержание давления, ротация по наработке, задержка между пусками,
сухой ход, термореле.

Клеммы:
  AI2  0-10В  датчик давления (0 В = 0 бар, 10 В = шкала «Pмакс»)
  DO0         насос 1
  DO1         насос 2
  DI0  НЗ     реле «есть вода» / поплавок скважины (True = вода есть)
  DI1  НЗ     термореле насоса 1 (True = норма)
  DI2  НЗ     термореле насоса 2 (True = норма)
'''
from dev import Ai, Do, Di, Reg, Alarm, Event, count, webconf, run, conv
from time import localtime
from micropython import const

STEP = const(1)
PUMP_COUNT = const(2)

count(Reg=9, Ai=3, Ao=0, Do=2, Di=3, Alarm=4, Event=3)
webconf(Reg=True, Pin=True, Display=False, Console=True)

rPower    = Reg(0).conf('bool', 'Питание')
rSetP     = Reg(1).conf('float', 'Уставка, бар', limits=[0.5, 8.0], step=0.1, dec=1, default=3.0)
rHist     = Reg(2).conf('float', 'Гистерезис, бар', limits=[0.1, 2.0], step=0.1, dec=1, default=0.4)
rPmax     = Reg(3).conf('float', 'Шкала датчика, бар', limits=[1.0, 16.0], step=0.5, dec=1, default=10.0)
rRotHour  = Reg(4).conf('uint', 'Ротация, ч', limits=[0, 48], default=6)
rDelay    = Reg(5).conf('uint', 'Пауза пуска, с', limits=[5, 120], default=20)
rPress    = Reg(6).conf('float', 'Давление, бар', access='r', dec=1)
rWait     = Reg(7).conf('uint', 'Пауза, с', access='r')
rHour1    = Reg(8).conf('uint', 'Насос 1, ч', access='r')
rHour2    = Reg(9).conf('uint', 'Насос 2, ч', access='r')

# AI0 и AI1 на плате — NTC; давление сидит на AI2, поэтому Ai=3.
Ai(0).conf(name='не используется', access='-')
Ai(1).conf(name='не используется', access='-')
aiP = Ai(2).conf(name='Давление 0-10В', type='raw', access='r')

doP = [Do(0).conf(name='Насос 1', access='r'),
       Do(1).conf(name='Насос 2', access='r')]
diWater = Di(0).conf(name='Вода в скважине', type='nz', access='r')
diTh = [Di(1).conf(name='Термореле 1', type='nz', access='r'),
        Di(2).conf(name='Термореле 2', type='nz', access='r')]

aWater = Alarm(0).conf('Сухой ход')
aTh    = [Alarm(1).conf('Авария насоса 1'), Alarm(2).conf('Авария насоса 2')]
aHigh  = Alarm(3).conf('Высокое давление')
ePower = Event(0).conf('Питание')
ePump  = [Event(1).conf('Насос 1'), Event(2).conf('Насос 2')]

hourRegs = [rHour1, rHour2]
secOn = [0, 0]
secOff = [0, 0]
pause = 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 pressure():
    return conv(aiP.val(), 0.0, 10.0, 0.0, rPmax.val())


def setPump(i, on, reason):
    global pause
    if doP[i].val() == on:
        return
    doP[i].val(on)
    ePump[i].add(1 if on else 0)
    secOn[i] = 0
    secOff[i] = 0
    if on:
        pause = rDelay.val()
    log('Насос {} {} ({})'.format(i + 1, 'вкл' if on else 'выкл', reason))


def allOff(reason):
    for i in range(PUMP_COUNT):
        if doP[i].val():
            setPump(i, False, reason)


def pickStart():
    best = None
    for i in range(PUMP_COUNT):
        if doP[i].val() or aTh[i].val() or not diTh[i].val():
            continue
        if best is None or hourRegs[i].val() < hourRegs[best].val():
            best = i
        elif hourRegs[i].val() == hourRegs[best].val() and secOff[i] > secOff[best]:
            best = i
    return best


def pickStop():
    best = None
    for i in range(PUMP_COUNT):
        if not doP[i].val():
            continue
        if best is None or secOn[i] > secOn[best]:
            best = i
    return best


def countOn():
    n = 0
    for i in range(PUMP_COUNT):
        if doP[i].val():
            n += 1
    return n


def step():
    global pause

    if rPower.changed():
        ePower.add(1 if rPower.val() else 0)
        log('Питание {}'.format('вкл' if rPower.val() else 'выкл'))
        if not rPower.val():
            allOff('питание')

    for i in range(PUMP_COUNT):
        if not diTh[i].val():
            if not aTh[i].val():
                aTh[i].on()
                log('Термореле насоса {}'.format(i + 1))
            if doP[i].val():
                setPump(i, False, 'термореле')
        if doP[i].val():
            secOn[i] += 1
            if secOn[i] % 3600 == 0:
                hourRegs[i].inc()
        else:
            secOff[i] += 1

    p = pressure()
    rPress.val(p)

    if p > rPmax.val() * 0.95:
        if not aHigh.val():
            aHigh.on()
            log('Давление {:.2f} бар'.format(p))
        allOff('высокое давление')
        rPower.val(False)
        return
    aHigh.off()

    if not diWater.val():
        if not aWater.val():
            aWater.on()
            log('Сухой ход')
        allOff('сухой ход')
        rWait.val(pause)
        return
    aWater.off()

    if not rPower.val():
        allOff('выкл')
        rWait.val(0)
        return

    if pause > 0:
        pause -= 1
        rWait.val(pause)
        return
    rWait.val(0)

    pOn = rSetP.val() - rHist.val()
    pOff = rSetP.val() + rHist.val()
    n = countOn()
    rotSec = rRotHour.val() * 3600

    if p < pOn and n < PUMP_COUNT:
        i = pickStart()
        if i is not None:
            setPump(i, True, 'давление')
    elif p > pOff and n > 0:
        i = pickStop()
        if i is not None:
            setPump(i, False, 'давление')
    elif rotSec > 0 and n == 1:
        running = pickStop()
        idle = pickStart()
        if running is not None and idle is not None and secOn[running] > rotSec:
            setPump(running, False, 'ротация')
            setPump(idle, True, 'ротация')


allOff('старт')
log('Старт насосной')
run(step, STEP)