Fridge Alert Bot
Тревога в Telegram, когда холодильник в ресторане слишком тёплый, — и тишина, когда кто-то просто открыл дверь.
- Роль
- Личный проект — проектирование, код, тесты
- Год
- 2026
- Категория
- Боты · Инфраструктура
- Стек
- Python 3.12, FastAPI, aiogram 3, SQLite

Проблема
Холодильник, который нагрелся за ночь, — это испорченные продукты утром. Пробел в журнале температур — это проблемы на санитарной проверке. Сотрудники смотрят на термометры несколько раз в день и пропускают большинство поломок.
Дешёвые датчики температуры есть. Не хватает того, что следит за ними и зовёт человека в нужный момент — и не поднимает ложную тревогу каждый раз, когда кто-то открыл дверь. В Steady Control я работаю с датчиками температуры холодильников в отелях и ресторанах, поэтому сделал ту часть, которая решает, когда будить человека.
Решение
Датчики отправляют показания в HTTP API. Небольшой конечный автомат решает, что значит каждое показание:
- Холодильник должен быть выше лимита 5 минут, прежде чем придёт тревога. Дверь, открытая на минуту, тревогу не вызывает.
- Одна тревога на инцидент, потом сообщение «снова в норме» с длительностью и пиковой температурой.
- Для возврата к норме температура должна опуститься на 0,5 °C ниже лимита — так холодильник, который «висит» на границе, не засыпает чат сообщениями.
- Опоздавшие или повторные показания от датчика, который был офлайн, попадают в историю, но не откатывают состояние назад. Инциденты старше 15 минут записываются, но не объявляются.
В Telegram команда кухни пользуется /status, /history <sensor> (график PNG), /setlimit и
/report, а каждое утро приходит сводка за сутки.
Демо
Скриншоты

Технологии
- Python 3.12
- FastAPI
- aiogram 3
- SQLite
- matplotlib
- pytest
- Docker
Python 3.12 с аннотациями типов, FastAPI для API, aiogram 3 для бота, SQLite для хранения и matplotlib для графиков. API, бот и планировщик отчётов работают в одном asyncio-процессе и делят один сервисный объект, поэтому они никогда не расходятся во мнении о состоянии холодильника.
Фрагмент кода
Правило тревоги — чистая функция: без базы данных и без Telegram, только «состояние + показание → новое состояние и, возможно, событие». Поэтому каждый пограничный случай проверяется тестом в три строки.
def evaluate(
current: AlertState,
*,
sensor_id: str,
temperature: float,
at: datetime,
limit: float,
rules: AlertRules,
) -> tuple[AlertState, AlertEvent | None]:
"""Return the next state and, if something worth telling people happened, an event."""
above = temperature > limit
if current.state is SensorState.OK:
if above:
return AlertState(SensorState.PENDING, at, temperature), None
return current, None
started = current.breach_started_at
peak = (
temperature
if current.peak_temperature is None
else max(current.peak_temperature, temperature)
)
if current.state is SensorState.PENDING:
if not above:
return AlertState(), None
if at - started >= rules.delay:
event = AlertEvent(EventKind.ALERT, sensor_id, temperature, limit, started, peak, at)
return AlertState(SensorState.ALERT, started, peak), event
return replace(current, peak_temperature=peak), None
# ALERT
if temperature <= limit - rules.hysteresis:
event = AlertEvent(EventKind.RECOVERED, sensor_id, temperature, limit, started, peak, at)
return AlertState(), event
return replace(current, peak_temperature=peak), NoneБаг, который я нашёл и исправил
Симулятор поднимал ложные тревоги при обычном открывании двери
Что сломалось
Тест прогоняет смоделированный день открываний двери через настоящие правила тревоги. Он упал: 18 сообщений о тревоге и возврате к норме — без единой поломки оборудования. На 30 случайных seed'ах демо давало 8,2 ложной тревоги в сутки. Демо, которое постоянно «кричит волк», выглядит как сломанный продукт.
Причина
Симулятор считал, что при открытой двери датчик стремится к комнатной температуре (24 °C). Так ведёт себя голый датчик воздуха. Датчик внутри холодильника стоит среди продуктов и видит смешанный воздух. Два открывания подряд держали показания выше 5 °C дольше, чем задержка тревоги.
Исправление
Теперь открытая дверь поднимает цель на несколько градусов выше уставки, а компрессор возвращает холод быстрее. Ложных тревог стало 0,13 в сутки вместо 8,2 на тех же seed'ах. Размытый тест превратился в два точных: одно открывание двери никогда не даёт тревогу, а оживлённая неделя — максимум две.
door_tau_s: float = 900.0
cool_tau_s: float = 300.0
elif self.mode is Mode.DOOR_OPEN:
target, tau = p.ambient_c, p.door_tau_sdoor_delta_c: float = 5.0
door_tau_s: float = 300.0
cool_tau_s: float = 180.0
elif self.mode is Mode.DOOR_OPEN:
target, tau = p.setpoint_c + p.door_delta_c, p.door_tau_sРезультат
- 83 автоматических теста и ноль предупреждений линтера; GitHub Actions запускает их при каждом push.
- Работает от начала до конца без оборудования: симулятор создаёт открывания двери и поломки компрессора, а без токена бота тревоги печатаются в консоль.
- В живом запуске демо принудительная поломка компрессора дала ровно одну тревогу, а открывания двери — ни одной.