Automatic chicken coop door controller β ESP32-C3 Β· MicroPython Β· asyncio Β· Web UI
Opens and closes a chicken coop door automatically based on ambient light and time-of-day windows. Built around an ESP32-C3, a self-braking DC motor, and a local-only WiFi access point for configuration β no cloud, no router dependency, no always-on WiFi.
- Dual decision mechanism β ambient light sensor (5-sample unanimity) + configurable time windows. Sensor is primary; time is a safety net.
- Absolute backstop β independently configurable hard open/close times for when the sensor alone isn't enough (lamp, full moon, sensor failure).
- Manual override with state memory β physical buttons or web UI. Manual state persists until the next backstop trigger.
- Self-braking motor β JGY-370 worm gear DC motor draws 0 mA at rest. No stall current, no heat, dramatically lower idle power vs the v1 servo.
- Safety stop β motor timeout (21 s) and DRV8833 nFAULT monitoring. Latched state, physical recovery required.
- On-demand WiFi AP β double-click either button to start the access point. Auto-shuts down after 10 minutes of inactivity.
- Live web dashboard β SSE-driven status, 30-day forecast SVG chart, config editor, event log.
- Persistent binary log β 365-record circular buffer on LittleFS (8 bytes/day). Zero pre-allocation.
- Astronomical time windows β sunrise/sunset approximation for TarnΓ³w (~50Β°N, 21Β°E), Β±15 min accuracy, pure cosine model.
- DST-transparent RTC β DS3231 always stores CET (UTC+1). Daylight saving is a logic layer concern only.
- Fully testable on CPython β dependency injection throughout. Complete pytest suite runs without hardware.
- Hardware at a Glance
- Architecture
- Getting Started
- Configuration
- Web Interface
- LED Status Reference
- Documentation
- Project History
- License
| Component | Part | Purpose |
|---|---|---|
| MCU | ESP32-C3 Super Mini | WiFi, USB, 3.3 V logic |
| Motor driver | DRV8833 | 1.5 A peak, nSLEEP power gating |
| DC motor | JGY-370 (6 V, worm gear) | Self-braking β 0 mA holding current |
| GPIO expander | PCF8574 (I2C 0x20) | LEDs + motor IN1/IN2 |
| RTC | DS3231 (ZS-042) | TCXO Β±2 ppm, CR2032 backup |
| Light sensor | BH1750 (GY-302) | Lux measurement, I2C 0x23 |
| Limit switches | 2Γ microswitch | Door-open / door-closed detection |
| Push buttons | 2Γ momentary | Manual open/close + WiFi AP trigger |
Full pinout, wiring diagram and BOM: docs/hardware.md
graph TD
BH1750["BH1750\nLight Sensor"] -->|lux every 300ms| LSL["light_sensor_loop\n(asyncio task)"]
LSL --> LBuf["lux_buffer[5]\nlux_ready flag"]
DS3231["DS3231\nRTC"] -->|datetime every 2s| CL["control_loop\n(asyncio task)"]
CFG["config.json"] -->|load at boot| CTRL
LBuf --> CTRL["CoopController\ntick() β synchronous"]
CL --> CTRL
CTRL -->|MOVING_OPEN/CLOSE| RM["_run_move\n(asyncio coroutine)"]
RM -->|IN1/IN2 via PCF8574| DRV["DRV8833\nMotor Driver"]
DRV --> JGY["JGY-370\nDC Motor"]
JGY --> LS["Limit Switches\nGPIO 0, 1"]
LS -->|limit hit| RM
CTRL --> LED["led_loop\n(asyncio task)"]
LED --> LEDS["LED red/yellow/green\nGPIO6 + PCF P0/P1"]
BTN["Buttons\nGPIO 7, 10"] --> BM["button_monitor Γ 2\n(asyncio tasks)"]
BM -->|1 click| CTRL
BM -->|β₯2 clicks| AP["start_ap_session\nWiFi AP + microdot"]
AP --> WEB["Web Server\n192.168.4.1"]
CTRL --> WEB
WEB --> BROWSER["Browser\n(status / config / logs / debug)"]
The system runs entirely in a single uasyncio event loop. tick() is a plain synchronous function β no I/O, no await β which makes the state machine trivially testable with mock objects on any Python 3.13+ environment.
Requires Python 3.13+ and uv.
# Clone and install
git clone https://github.com/albertlis/Automatic-door-opening-system.git
cd Automatic-door-opening-system
uv sync --extra dev
# Run the full test suite
uv run pytest tests/ -v
# Lint and format
uv run ruff check src/ tests/
uv run ruff format src/ tests/
# Start the web UI locally (no hardware needed)
python run_local.py
# β open http://localhost:5000The local server uses mock hardware objects. All web pages, the SVG forecast chart, config editor, and log viewer work without an ESP32.
| Suite | What it covers |
|---|---|
tests/test_astro.py |
Sunrise/sunset approximation, DST boundary detection |
tests/test_config.py |
Config load/save, validation invariant, helper functions |
tests/test_state.py |
All state machine transitions, safety stop, I2C error handling |
tests/test_async.py |
_run_move, light_sensor_loop, control_loop, button_monitor |
tests/test_api.py |
All REST endpoints, time sync, config POST validation |
tests/test_logs.py |
Binary log write/read, circular buffer wrap, sentinel values |
TDD protocol: write the test suite first (all fail), then implement until all pass.
Download the latest ESP32_GENERIC_C3 firmware from micropython.org/download.
esptool --chip esp32c3 --port COM<N> erase_flash
esptool --chip esp32c3 --port COM<N> --baud 460800 write_flash -z 0x0 ESP32_GENERIC_C3-*.bin
β οΈ Do this before installing the CR2032 battery.The ZS-042 board has a charging circuit for rechargeable cells. CR2032 is non-rechargeable.
Desolder the power LED and the charging resistor before fitting the battery.
See docs/hardware.md for details.
Upload order matters β main.py triggers boot on upload, so it must go last.
mpremote connect COM<N> fs mkdir /www
mpremote connect COM<N> fs cp src/compat.py :compat.py
mpremote connect COM<N> fs cp src/astro.py :astro.py
mpremote connect COM<N> fs cp src/config.py :config.py
mpremote connect COM<N> fs cp config.default.json :config.json
mpremote connect COM<N> fs cp src/logs.py :logs.py
mpremote connect COM<N> fs cp src/state.py :state.py
mpremote connect COM<N> fs cp src/hardware.py :hardware.py
mpremote connect COM<N> fs cp src/web.py :web.py
mpremote connect COM<N> fs cp src/www/index.html :/www/index.html
mpremote connect COM<N> fs cp src/www/config.html :/www/config.html
mpremote connect COM<N> fs cp src/www/logs.html :/www/logs.html
mpremote connect COM<N> fs cp src/www/debug.html :/www/debug.html
mpremote connect COM<N> fs cp src/boot.py :boot.py
mpremote connect COM<N> fs cp src/main.py :main.py- Power on.
boot.pydetects the DS3231 default date (2000-01-01) and starts the WiFi AP automatically. - Connect to
Coop_Control(password:coop123). - Open
http://192.168.4.1β click Synchronizuj czas z przeglΔ darki to set the RTC. - Automatic open/close logic activates immediately once the year is β₯ 2020.
- On subsequent boots, WiFi is OFF by default. Double-click either button to start the AP.
Three independently configurable sections β each has its own mode:
| Section | Controls | Modes |
|---|---|---|
window |
When is the light sensor active? | sun_position (dynamic) Β· legacy (fixed hours) |
override_open |
Backstop: latest allowed open time | dynamic (N min after sunrise) Β· fixed (clock hour) |
override_close |
Backstop: latest allowed close time | fixed (clock hour) Β· dynamic (N min after sunset) |
Additional parameters:
| Parameter | Default | Description |
|---|---|---|
light.lux_open |
8.0 lx | Open threshold β all 5 samples must exceed this |
light.lux_close |
3.0 lx | Close threshold β all 5 samples must be below this |
safety.move_timeout_s |
21 s | Motor movement timeout before safety stop |
window_open β€ abs_open < abs_close β₯ window_close
The backstop values must be outside the sensor window β they are the last-resort boundary, not the first trigger. Violating this invariant returns a 400 error on the config page with a specific message.
Full configuration reference and seasonal behaviour: State Machine & Control Logic
Activated by double-clicking either physical button. Auto-shuts off after 10 minutes of idle.
| URL | Page | Description |
|---|---|---|
/ |
Status | Live gate state badge, lux readings, RTC time, today's schedule, 30-day forecast SVG, manual open/close buttons |
/config |
Configuration | All config parameters with mode toggles, browser time sync button |
/logs |
Event Log | Daily records: first open, last close, manual interventions, safety stop incidents |
/debug |
Debug | Raw sensor values, firmware version, WebREPL link, reboot button |
The status page uses Server-Sent Events (HTMX SSE extension) for live updates β no page refresh needed. Falls back to 2-second polling if SSE is unavailable.
The 30-day forecast SVG is generated server-side from the astronomical model β shows how the sensor window and backstop times shift with sunrise/sunset over the coming month.
| State | LED | Pattern | Meaning |
|---|---|---|---|
IDLE_OPEN Β· MANUAL_HOLD_OPEN |
Green | Solid | Door fully open |
IDLE_CLOSED Β· MANUAL_HOLD_CLOSED |
Red | Solid | Door fully closed |
MOVING_OPEN Β· MOVING_CLOSE |
Green | 1 Hz blink | Door in motion |
SAFETY_STOP |
Red | 1 Hz blink | Motor stopped β physical button to recover |
ERROR |
Red | 4 Hz blink | I2C failure or hardware fault β reboot required |
| Low RTC battery (any state) | Yellow | 1 Hz blink | CR2032 below 2.7 V β replace battery |
ERROR is distinguishable from SAFETY_STOP by blink speed. LED_RED (GPIO6) is wired directly to the MCU β it works even if the I2C bus and PCF8574 are dead.
| Document | Description |
|---|---|
| docs/hardware.md | Complete BOM, power topology, pin tables, PCF8574 wiring, DS3231 modification, DRV8833 notes |
| docs/state-machine.md | State diagram (Mermaid), full transition table, decision logic, time system, DST handling |
| docs/api.md | Complete REST API reference with request/response examples for all 9 endpoints |
| migraton.md | Full architecture specification and design decisions for V2 |
V1 (ATmega328P / Arduino / C++) β still in the src/ history. Replaced due to:
- DS1307 RTC drift required nightly correction hack
- MG995 servo drew 400β600 mA stall current 24/7 to hold the door
- No USB, no remote visibility, no OTA updates
V2 (this codebase) β ESP32-C3, MicroPython, self-braking worm gear motor, WiFi AP, web UI.
This project β including source code, documentation, and wiring diagrams β is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).
Commercial use is strictly prohibited. This includes selling the software, offering paid installation services, incorporating it into a commercial product, or any internal business use. Commercial licensing is available β contact the author for written permission.
See the LICENSE file for the full license text, or visit creativecommons.org/licenses/by-nc-sa/4.0.