Fridge Alert Bot
Telegram alerts when a restaurant fridge stays too warm — and silence when someone just opened the door.
- Role
- Solo project — design, code, tests
- Year
- 2026
- Category
- Bots · Infrastructure
- Stack
- Python 3.12, FastAPI, aiogram 3, SQLite

The problem
A fridge that warms up overnight means spoiled food in the morning. A gap in the temperature log means trouble at a hygiene inspection. Staff check thermometers a few times a day, which misses most failures.
Cheap temperature probes exist. What is missing is something that watches them and calls a person at the right moment — without crying wolf every time someone opens the door. At Steady Control I work with fridge temperature sensors in hotels and restaurants, so I built the part that decides when to wake someone up.
The solution
Sensors send readings to an HTTP API. A small state machine decides what each reading means:
- A fridge must stay above its limit for 5 minutes before an alert. A door open for a minute never alerts.
- One alert per incident, then a "back to normal" message with the duration and the peak temperature.
- Recovery needs the temperature 0.5 °C below the limit, so a fridge hovering at the limit does not flood the chat.
- Late or repeated readings from a sensor that was offline go into the history without moving the state backwards. Incidents older than 15 minutes are recorded but not announced.
In Telegram the kitchen team uses /status, /history <sensor> (a PNG chart), /setlimit
and /report, and a daily summary arrives every morning.
Demo
Screenshots

Tech stack
- Python 3.12
- FastAPI
- aiogram 3
- SQLite
- matplotlib
- pytest
- Docker
Python 3.12 with type hints, FastAPI for the API, aiogram 3 for the bot, SQLite for storage and matplotlib for the charts. The API, the bot and the report scheduler run in one asyncio process and share one service object, so they can never disagree about a fridge's state.
Code highlight
The alert rule is a pure function: no database, no Telegram, just "state + reading → new state and maybe an event". That turns every edge case into a three-line test.
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), NoneBug I found and fixed
The simulator raised false alarms on ordinary door openings
What broke
A test runs a simulated day of door openings through the real alert rules. It failed: 18 alert and recovery messages without a single equipment failure. Over 30 random seeds the demo raised 8.2 false alarms per day. A demo that cries wolf makes the product look broken.
Root cause
The simulator treated an open door as "the probe moves towards room temperature (24 °C)". That is how a bare air sensor behaves. A probe inside a fridge sits among the food and sees mixed air. Two openings close together kept the reading above 5 °C for longer than the alert delay.
The fix
An open door now raises the target a few degrees above the setpoint, and the compressor recovers faster. False alarms fell from 8.2 to 0.13 per day on the same seeds. The vague test became two precise ones: a single opening must never alert, and a busy week may give at most two.
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_sResult
- 83 automated tests and zero linter warnings; GitHub Actions runs them on every push.
- Runs end to end without hardware: the simulator produces door openings and compressor failures, and without a bot token the alerts print to the console.
- In a live demo run, a forced compressor failure produced exactly one alert, and the door openings produced none.