2026-03-29 Мои чаны

Автоматизация контроля брожения: OpenResty, FlatDB, NodeMCU

Несколько лет я ставлю несколько чанов для брожения квасов и медовух. Сырье использую разное, дрожжи (если использую), то тоже разные - подбираю, смотрю, стараюсь обеспечить оптимальные условия для них, что не всегда легко, т.к. в своей “продукции” я не использую вообще свекольный сахар, только естественные моносахара и на крайний случай декстрозу для повторного дображивания, стараясь обходиться без кислотного гидролиза полисахаров вовсе.

Понятно, что при таком подходе всяческие рецепты из интернета без понимания не очень годятся. Впрочем, в целом они мне уже и не очень нужны, т.к. в целом процесс я понимаю и контролирую. Что, впрочем, требует постоянных измерений. Я постоянно контролирую PH сусла, его температуру, соленость изначального сырья, влажности и температуру в помещения брожения, уровень сахара на всех этапах и скорость брожения.

Какое-то время я обходился бумажкой и ручкой для записи всего этого дела и по ней контролировал сроки проверки сусла, время снятия с осадка и так далее. Пока не стал путаться.

В итоге мне это надоело и за пару часов я быстро накидал себе сервис для контроля брожения.

Концепция

Во-первых, мне хотелось иметь в одном месте полноценную базу данных по каждому чану, чтобы более детально и без потерь сравнивать и делать выводы. Самое же главное, - чтобы не забывать. Такое уже со мной случалось - как артефакт в подвале до сих пор стоят четыре 38 литровых чана, не снятые вовремя с осадка, и превратившися в брагу.

Во-вторых, хотелось автоматизировать наиболее значимые измерения - температуру в помещениях и в месте, куда выведана вентиляция подвала, а также самого сусла. Это дело благое, так как позволяет по динамике более детальной, чем “когда на глаза попадется и вспомнишь”, показателей много чего понять. Что именно и как - это тема совсем отдельная, поэтому пока только про сервис.

В-третьих, хотелось бы иметь напоминания автоматические о том, что пора предпринять какие-то действия, ну и предупреждения: чтобы не забыть и при необходимости делигировать. В варианте “на бумажке” делигировать работу с чанами тяжело - надо все самому в голове держать.

Реализация

Решил не измудряться и в качестве БД использовать Google Sheets, настроив там экспорт в CSV, а все необходимые мне расчетные формулы забив прямо в таблицу. Это достаточно удобно - не надо мудрить интерфейсы, все понятно и, главное, - быстро.

Поля получились следующие:

Все массы добавляемого сырья и даты снятия с осадка и добавления при этом рассчитываются сразу по шаблону типа сырья в Google Sheets. Хоть речь о примитивных формулах, это позволяет не загружать голову и ничего не записывать.

В комментарии при этом пишу значимую информацию - например, особенности сырья или название и марку применяемых дрожжей, потом анализирую по итогам, просто проставляя на чанах, а потом и на бутылках (после купажа), маркером номер. Таким образом, через год-два можно воссоздать всю историю напитка и сделать выводы.

Измерения построил максимально просто - использовал ESP 8266 и самые ходовые 1-wire термометры DS1820 для чанов (чтобы не тянуть к каждому по отдельному проводу), а для измерения влажности в помещениях - три датчика DHT11. Показания раз в 7 секунд отдаю на сервер по TCP. Ниже код сбора показаний с датчиков - все там банально за исключением того, что json с показаниями передаю в Base64. Это вызвано просто тем, что не хотелось менять специфические настройки файрволла у себя на сервере.

local results = {} -- DS1822 results

-- Read DS1822

local function readout(temp)    
    for addr, temp in pairs(temp) do  
        local uid = string.format('%s',('%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X'):format(addr:byte(1,8)))
        results[uid] = temp
        print(uid,temp)
    end
end

local function read_sensors()
    t:read_temp(readout, ds_pin, t.C)
end

-- Read the DHT11 sensor

local function dht_read(dhtpin)
    local status, temp, humi, temp_dec, humi_dec = dht11.read(dhtpin)
    if status == dht.OK then
        print("DHT Temperature:"..temp..";".."Humidity:"..humi)
        return {['temp'] = temp, ['humi'] = humi}
    elseif status == dht.ERROR_CHECKSUM then
        print( "DHT Checksum error." )
    elseif status == dht.ERROR_TIMEOUT then
        print( "DHT timed out." )
    end
    return {}
end

-- Data for TCP server

function data_out()
    read_sensors()
    local pack = {}
    pack['id'] = uid   
    pack['room'] = dht_read(dhtpin_room) 
    pack['cellar'] = dht_read(dhtpin_cellar) 
    pack['weather'] = dht_read(dhtpin_weather) 
    pack['sensors'] = results
    return encoder.toBase64(sjson.encode(pack))
end

На сервере как всегда OpenResty, для хранения и обработки результатов с датчиков использовал встраиваемую FlatDB. Немного интерфейсов и все готово. Для Google даже API не использовал - просто по крону дергаю CSV и все.

Уведомления зашил прямо в код разбора CSV и приема показаний с датчиков:

for line in io.lines(ngx.var.document_root..'/tanks.csv') do 
    if count>0 then
        local data = array.explode(line,',')
        results[count] = {notification = {typ = 'is-success', text = 'Брожение протекает нормально'}, status = 'is-primary'}
        for n,value in pairs(data) do
            local key,text = array.first(fields[n])
            if key:find('date_') and value ~='' and pcall(date(),value) and date(value) < date() then
                results[count].notification = {typ = 'is-danger', text = text}
            end
            results[count][key] = {value = value, text = text, order = n}
        end
        if not db[results[count].sensor_id.value] then
            db[results[count].sensor_id.value] = {
                {temp = 0, date = date():fmt('%Y-%d-%m')},
                {temp = 0, date = date():fmt('%Y-%d-%m')}
            }
            db:save()
        end
        results[count].stat = db[results[count].sensor_id.value]
        if (results[count]['date_blending'].value ~= '' and pcall(date(),results[count]['date_blending'].value) and date(results[count]['date_blending'].value) < date()) or (results[count]['date_end'].value ~= '' and date(results[count]['date_end'].value) < date()) then
            results[count].status = 'is-success'
        end 
    end
    count = count + 1    
end

При приеме данных с датчиков просто проверяю пределы и записываю в БД уже с уведомлением:

local limits = {
    weather = {
        temp = {
            optimal = {0,10},
            critical = {-25,35}
        },
        warm = {
            optimal = {40,60},
            critical = {10,90}
        }   
    },
    cellar = {
        temp = {
            optimal = {10,12},
            critical = {8,14}
        },
        warm = {
            optimal = {65,80},
            critical = {50,90}
        }   
    },
    room = {
        temp = {
            optimal = {18,24},
            critical = {16,28}
        },
        warm = {
            optimal = {60,75},
            critical = {50,80}
        }   
    }
}

Посмотреть можно тут: http://chan.ulgrad.ru/

Программная часть тривиальна и проста, и трудозатраты на нее - это реально пара часов, но вот с конструктивом размещения датчиков дело другое - хотя из обвязки для всего этого нужен всего один резистор, заставляет задуматься вопрос другой - как и где лучше мерить в чане температуру сусла и как герметично и химически нейтрально оформить датчики?