Сценарий algo_server.py следит за температурой помещения и стойки, включает охлаждение, ругается на открытую дверь и пропавший шлюз в сети.

Возможности

  • Два NTC: помещение и стойка. Уставка сравнивается с более горячим из двух. Если датчик стойки оборван — считается только помещение (авария датчика стойки, охлаждение не глушится).
  • Гистерезис охлаждения. Выше уставки + гистерезис — кондиционер и вентилятор вкл. Ниже уставки − гистерезис — выкл.
  • Перегрев (уставка + 8 °C): авария и принудительное охлаждение, даже если автоохлаждение снято (пока живо термореле ККБ).
  • Дверь. Открыта — счётчик секунд на экране. Дольше 60 с — авария «дверь открыта долго». Открытие и закрытие пишутся в события.
  • Авария кондиционера по термореле / сухому контакту ККБ: охлаждение стоп.
  • Пинг шлюза 192.168.0.1 раз в 30 с (не в секундном шаге). Есть ответ — время в мс; нет — авария и событие. Адрес в коде, при другом шлюзе правят строку.
  • Автоохлаждение можно выключить: тогда кондиционер не стартует по уставке, но перегрев всё равно попытается включить его.

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

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

Небольшой закрытый объём с электроникой, не ЦОД и не система пожаротушения.

  • серверная, аппаратная, кроссовая, щит связи;
  • стойка с ИБП и коммутаторами в офисе или на производстве;
  • контейнер / бытовка с оборудованием, куда не ставят полноценную BMS;
  • удалённый узел: перегрев, дверь и «шлюз не пингуется» уходят уведомлением, если контроллер на сервере telematic.dev.

Не закрывает: фреоновый контур с ротацией компрессоров (для этого штатный пример фреона), контроль влажности, несколько независимых рядов стоек, СКУД и видеонаблюдение.

Клеммы

КлеммаНазначение
AI0 NTCтемпература помещения
AI1 NTCтемпература стойки
DO0кондиционер / вентилятор 1
DO1вентилятор 2 / резерв (включается вместе с DO0)
DI0 НОдверь открыта
DI1 НЗавария кондиционера (норма = замкнут)

Сценарий

  '''
Щит серверной / аппаратной.

Охлаждение по температуре, дверь, термореле кондиционера,
пинг шлюза. Два темпа: логика каждую секунду, сеть — раз в 30 с.

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

STEP = const(1)
PING_PERIOD = const(30)
PING_HOST = '192.168.0.1'
DOOR_MAX = const(60)

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

rPower  = Reg(0).conf('bool', 'Автоохлаждение')
rSet    = Reg(1).conf('int',  'Уставка, °C', limits=[15, 35], default=26)
rHist   = Reg(2).conf('int',  'Гистерезис', limits=[1, 5], default=2)
rTroom  = Reg(3).conf('float', 'Помещение, °C', access='r', dec=1)
rTrack  = Reg(4).conf('float', 'Стойка, °C', access='r', dec=1)
rPingMs = Reg(5).conf('uint', 'Пинг, мс', access='r')
rDoorS  = Reg(6).conf('uint', 'Дверь открыта, с', access='r')

aiRoom = Ai(0).conf(name='Помещение', type='ntc', access='r')
aiRack = Ai(1).conf(name='Стойка', type='ntc', access='r')
doAc   = Do(0).conf(name='Кондиционер', access='r')
doFan  = Do(1).conf(name='Вентилятор', access='r')
diDoor = Di(0).conf(name='Дверь открыта', type='no', access='r')
diAc   = Di(1).conf(name='Авария кондиционера', type='nz', access='r')

aRoom = Alarm(0).conf('Ошибка датчика помещения')
aRack = Alarm(1).conf('Ошибка датчика стойки')
aHot  = Alarm(2).conf('Перегрев')
aAc   = Alarm(3).conf('Авария кондиционера')
aDoor = Alarm(4).conf('Дверь открыта долго')
aNet  = Alarm(5).conf('Нет пинга шлюза')
eAc   = Event(0).conf('Кондиционер')
eNet  = Event(1).conf('Связь со шлюзом')
eDoor = Event(2).conf('Дверь')

doorSec = 0
coolOn = False


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 setCool(on, reason):
    global coolOn
    if coolOn == on:
        doAc.val(on)
        doFan.val(on)
        return
    coolOn = on
    doAc.val(on)
    doFan.val(on)
    eAc.add(1 if on else 0)
    log('Охлаждение {} ({})'.format('вкл' if on else 'выкл', reason))


def step():
    global doorSec

    tr = aiRoom.val()
    tk = aiRack.val()
    if isnan(tr):
        aRoom.on()
        rTroom.val(0)
        setCool(False, 'датчик')
        return
    aRoom.off()
    rTroom.val(tr)

    if isnan(tk):
        aRack.on()
        rTrack.val(0)
        tk = tr
    else:
        aRack.off()
        rTrack.val(tk)

    t = tr
    if tk > t:
        t = tk

    if not diAc.val():
        if not aAc.val():
            aAc.on()
            log('Авария кондиционера')
        setCool(False, 'авария ККБ')
    else:
        aAc.off()

    if diDoor.val():
        doorSec += 1
        rDoorS.val(doorSec)
        if doorSec == 1:
            eDoor.add(1)
        if doorSec >= DOOR_MAX:
            if not aDoor.val():
                aDoor.on()
                log('Дверь открыта {} с'.format(doorSec))
    else:
        if doorSec > 0:
            eDoor.add(0)
        doorSec = 0
        rDoorS.val(0)
        aDoor.off()

    tmax = rSet.val() + 8
    if t >= tmax:
        if not aHot.val():
            aHot.on()
            log('Перегрев {:.1f}'.format(t))
        if diAc.val():
            setCool(True, 'перегрев')
        return
    if t <= rSet.val():
        aHot.off()

    if not rPower.val() or aAc.val():
        setCool(False, 'выкл')
        return

    if t >= rSet.val() + rHist.val():
        setCool(True, 'температура')
    elif t <= rSet.val() - rHist.val():
        setCool(False, 'температура')


def netStep():
    ms, res = ping(PING_HOST, size=32, timeout=0.2)
    if res == 0:
        rPingMs.val(ms)
        if aNet.val():
            aNet.off()
            eNet.add(1)
            log('Шлюз {} мс'.format(ms))
    else:
        rPingMs.val(0)
        if not aNet.val():
            aNet.on()
            eNet.add(0)
            log('Нет пинга ' + PING_HOST)


doAc.off()
doFan.off()
log('Старт щита серверной')
run((step, STEP), (netStep, PING_PERIOD))