article
Скважина с двумя насосами
Сценарий 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)