Сценарий algo_irrigation.py поливает три зоны по очереди одним насосом: по расписанию утром и вечером или командой «полить сейчас».

Возможности

  • Три линии не пересекаются. Сначала зона 1, потом 2, потом 3. Общий насос на DO3 включается только вместе с текущей линией.
  • Длительность каждой линии в минутах задаётся отдельно (по умолчанию 10 / 10 / 5).
  • Расписание. Час утра и час вечера. В эту минуту, если включено «Авто», ставится очередь всех трёх линий. Один слот — один запуск (не крутит весь час).
  • Полить сейчас: одна линия или все. Команда после приёма сбрасывается в «Нет».
  • Запрет полива: нет воды в ёмкости, дождь, воздух холоднее порога, обрыв NTC. Идущий полив останавливается; расписание и ручной пуск в этот момент пропускаются (событие «пропуск»).
  • На экране текущая линия, остаток секунд, часы насоса (до перезагрузки), температура воздуха.

Авто, ручная команда, длительности, часы утро/вечер и порог холода — с Управления.

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

Три клапана и насос — грядки, теплица, небольшой участок, не поле с десятками секторов.

  • теплица или парник с капельными линиями;
  • огород, газон, питомник из двух–трёх контуров;
  • ферма или дача без постоянного оператора: полив сам, в дождь и мороз не льёт, «нет воды» видно в журнале;
  • совместно с баком (algo_tank.py): один контроллер набирает ёмкость, этот — раздаёт (на двух устройствах; на одном плате не хватит DO, если совмещать как есть).

Не закрывает: учёт влажности почвы, фертигацию, полив по солнцу, больше трёх линий без правки кода и клемм, одновременный полив двух зон.

Клеммы

КлеммаНазначение
AI0 NTCтемпература воздуха
DO0, DO1, DO2линии 1–3
DO3насос
DI0 НЗесть вода в ёмкости (норма = замкнут)
DI1 НОдатчик дождя (дождь = замкнут)

Сценарий

  '''
Полив теплицы / грядки, три линии по очереди.

Расписание (утро и вечер), запрет по холоду и дождю, ручной «полить сейчас».
Насос общий, линии не пересекаются.

Клеммы:
  AI0  NTC   температура воздуха
  DO0        линия 1
  DO1        линия 2
  DO2        линия 3
  DO3        насос
  DI0  НЗ    есть вода в ёмкости (True = вода есть)
  DI1  НО    датчик дождя (True = дождь, не поливать)
'''
from dev import Ai, Do, Di, Reg, Alarm, Event, count, webconf, run
from time import localtime
from math import isnan
from micropython import const

STEP = const(1)
ZONES = const(3)

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

rAuto    = Reg(0).conf('bool', 'Авто по расписанию')
rStart   = Reg(1).conf('enum', 'Полить сейчас', items=['Нет', 'Линия 1', 'Линия 2', 'Линия 3', 'Все'], default=0)
rDur1    = Reg(2).conf('uint', 'Линия 1, мин', limits=[1, 60], default=10)
rDur2    = Reg(3).conf('uint', 'Линия 2, мин', limits=[1, 60], default=10)
rDur3    = Reg(4).conf('uint', 'Линия 3, мин', limits=[1, 60], default=5)
rMornH   = Reg(5).conf('uint', 'Утро, час', limits=[0, 23], default=6)
rEveH    = Reg(6).conf('uint', 'Вечер, час', limits=[0, 23], default=20)
rTmin    = Reg(7).conf('int',  'Не поливать ниже, °C', limits=[-5, 15], default=5)
rZone    = Reg(8).conf('enum', 'Сейчас', items=['Стоп', 'Линия 1', 'Линия 2', 'Линия 3'], access='r')
rLeft    = Reg(9).conf('uint', 'Осталось, с', access='r')
rHours   = Reg(10).conf('uint', 'Насос, ч', access='r')
rTemp    = Reg(11).conf('float', 'Воздух, °C', access='r', dec=1)

aiT = Ai(0).conf(name='Воздух', type='ntc', access='r')
doZ = [Do(0).conf(name='Линия 1', access='r'),
       Do(1).conf(name='Линия 2', access='r'),
       Do(2).conf(name='Линия 3', access='r')]
doPump = Do(3).conf(name='Насос', access='r')
diWater = Di(0).conf(name='Вода в ёмкости', type='nz', access='r')
diRain  = Di(1).conf(name='Дождь', type='no', access='r')

aWater = Alarm(0).conf('Нет воды')
aCold  = Alarm(1).conf('Слишком холодно для полива')
aSens  = Alarm(2).conf('Ошибка датчика воздуха')
eRun   = Event(0).conf('Полив')
eSkip  = Event(1).conf('Пропуск полива')

queue = []
left = 0
lastSched = -1
pumpSec = 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 durations():
    return (rDur1.val() * 60, rDur2.val() * 60, rDur3.val() * 60)


def allOff():
    for z in doZ:
        z.off()
    doPump.off()
    rZone.val(0)
    rLeft.val(0)


def stopRun(reason):
    global queue, left
    was = doPump.val()
    allOff()
    queue = []
    left = 0
    if was:
        log('Полив стоп ({})'.format(reason))


def startQueue(zones, reason):
    global queue, left
    d = durations()
    queue = []
    for i in zones:
        queue.append((i, d[i]))
    if not queue:
        return
    log('Полив {} ({})'.format(zones, reason))
    eRun.add(len(queue))
    nextZone()


def nextZone():
    global queue, left
    allOff()
    if not queue:
        rZone.val(0)
        rLeft.val(0)
        log('Полив окончен')
        return
    i, sec = queue[0]
    queue = queue[1:]
    left = sec
    doZ[i].on()
    doPump.on()
    rZone.val(i + 1)
    rLeft.val(left)


def blocked():
    t = aiT.val()
    if isnan(t):
        aSens.on()
        rTemp.val(0)
        return True, 'датчик'
    aSens.off()
    rTemp.val(t)
    if t < rTmin.val():
        aCold.on()
        return True, 'холод'
    aCold.off()
    if not diWater.val():
        aWater.on()
        return True, 'нет воды'
    aWater.off()
    if diRain.val():
        return True, 'дождь'
    return False, ''


def step():
    global lastSched, left, pumpSec

    bad, why = blocked()
    if bad and doPump.val():
        stopRun(why)

    if rStart.val() != 0:
        cmd = rStart.val()
        rStart.val(0)
        if bad:
            eSkip.add()
            log('Ручной полив отклонён ({})'.format(why))
        elif cmd == 4:
            startQueue((0, 1, 2), 'вручную все')
        else:
            startQueue((cmd - 1,), 'вручную')

    t = localtime()
    hm = t[3] * 60 + t[4]
    slot = -1
    if hm == rMornH.val() * 60:
        slot = 0
    elif hm == rEveH.val() * 60:
        slot = 1
    if rAuto.val() and slot >= 0 and slot != lastSched:
        lastSched = slot
        if bad:
            eSkip.add()
            log('Расписание пропущено ({})'.format(why))
        elif not doPump.val():
            startQueue((0, 1, 2), 'расписание')
    if hm not in (rMornH.val() * 60, rEveH.val() * 60):
        lastSched = -1

    if doPump.val():
        pumpSec += 1
        if pumpSec >= 3600:
            pumpSec = 0
            rHours.inc()
        if left > 0:
            left -= 1
            rLeft.val(left)
        if left <= 0:
            nextZone()
    else:
        rZone.val(0)
        rLeft.val(0)


allOff()
log('Старт полива')
run(step, STEP)