Skip to content

About

A climate sensor logger and room climate control client.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PlatformIO CI Lines of Code Coverage Duplicated Lines (%) Maintainability Rating Quality Gate Status

Klima-Control

ESP32-based temperature and humidity controller with web interface for monitoring and controlling environmental conditions. Runs on the Adafruit QT Py ESP32-S2 and supports a wide range of I2C sensors for temperature, humidity, pressure, CO2, air quality, light, and VOC monitoring.

Features

  • Real-time monitoring - Temperature and humidity readings from a wide range of I2C sensors
  • Temperature control - PID-based temperature control with configurable target
  • Web interface - Full control and monitoring from any device on your network
  • MQTT integration - Publish sensor data and control commands via MQTT
  • Remote syslog - Forward device logs to a remote syslog server (UDP, RFC 3164)
  • Data logging - Historical sensor data for trend analysis
  • Timers & Alarms - Schedule temperature control actions or notifications
  • OTA updates - Update firmware over WiFi from GitHub releases
  • Easy setup - Captive portal for WiFi configuration
  • mDNS discovery - Access device at klima-XXXXXX.local
  • Visual feedback - Status LED indicates device state (solid=on, blink=startup, flash=MQTT transmit)

Hardware

Primary board: Adafruit QT Py ESP32-S2 (~320 KB internal SRAM + ~2 MB PSRAM)

Matching Housing: 3D-printable housing on MakerWorld

Specification Value
MCU ESP32-S2 (single-core @ 240MHz)
Internal RAM ~320 KB (task stacks, FreeRTOS, WiFi, mbedTLS, DMA — all internal-only)
PSRAM ~2 MB on board (verified on-device as psram_size 2094735; available for non-real-time allocations)
Flash 4MB
Sensors Multiple I2C sensors (per-address assignment in settings)
I2C pins SDA: GPIO 8, SCL: GPIO 9
Optional display Waveshare 1.54" e-paper (200×200, SSD1681) over SPI

An optional e-paper display can show the current temperature and humidity. It is disabled by default; see E-Paper Display Wiring for the module to buy, the pin-by-pin wiring, and troubleshooting.

Use Cases

Klima-Control ships as a single ESP32-S2 board that talks to a configurable chain of I2C sensors, which lets the same firmware cover a range of deployments by changing only the housing and the sensor chain.

Flush-Mounted Room Thermostat

Mounted in a standard 55 mm wall box with an optional e-paper display, the device reads the room's current climate and drives a temperature setpoint that is published over MQTT and exposed in the web UI.

Klima-Control flush-mounted as a room thermostat, with e-paper display showing the current temperature, humidity and setpoint

Modular Sensor Measurement Device

A bare module with a STEMMA QT / Qwiic port can be paired with a chain of plug-and-play sensors via Qwiic / STEMMA QT cables — temperature, humidity, CO2, air quality, light, pressure — without rewiring. Attach the sensors that match the location, then map each response from the I2C scan to a sensor type in the Sensors settings page (see "Sensor configuration" below).

Bare Klima-Control module wired to a STEMMA QT sensor breakout, with the Qwiic/STEMMA QT cable chain visible

Supported Sensors

All sensors connect via the Stemma QT / I2C port. Multiple sensors can be connected simultaneously. The same address can carry several sensor modules — how the firmware picks which one is the topic of the next section, "Sensor Configuration".

Sensor Category Measurements Calculated Values I2C Address(es)
SHT4x Temperature & Humidity Temperature, Relative Humidity Dew Point 0x44, 0x45
BME680 Environmental Temperature, Relative Humidity, Pressure Dew Point, Sea Level Pressure 0x76, 0x77
BMP3xx Pressure Pressure Sea Level Pressure 0x76, 0x77
DPS310 Pressure Pressure Sea Level Pressure 0x76, 0x77
SCD4x CO2 CO2 (ppm) 0x62
SGP40 Air Quality VOC Index — 0x59
BH1750 Light Illuminance (lux) — 0x23, 0x5C
TSL2591 Light Illuminance (lux) — 0x29
VEML7700 Light Illuminance (lux) — 0x10
PM25 AQI Air Quality PM1.0 / PM2.5 / PM10 concentration (µg/m³), particle counts — 0x12

Note: The SGP40 VOC sensor requires temperature and relative humidity readings from another sensor (e.g. SHT4x) to compute a calibrated VOC index.

Sensor Configuration

Sensors are not auto-assigned at startup. The firmware holds a per-address assignment string in NVS (44=SHT4x,77=BME680); at boot each entry is instantiated as a driver of the given type at the given address.

The Sensors settings page is the place to manage this list:

  • Discover addresses: an I2C scan reports which addresses currently respond.
  • Pick a type per address: the same address can carry several sensor modules (e.g. 0x76 / 0x77 is shared by BME680, BMP3xx and DPS310). The scan only knows an address responded — it cannot tell which module is on the other end, so the user picks the type in settings.
  • Deactivate a module: simply omit its address from the assignment list (or leave the type blank). The module stays wired, but no driver is instantiated for it and it produces no measurements.

Changes are persisted to NVS and take effect on the next boot (the API triggers a restart).

Getting Started

Build & Upload

# Build for ESP32-S2
pio run -e adafruit_qtpy_esp32s2

# Upload to device
pio run -e adafruit_qtpy_esp32s2 -t upload

# Monitor serial output (with exception decoder)
pio device monitor

Exception Decoder: The serial monitor runs with the esp32_exception_decoder filter enabled (configured via monitor_filters in platformio.ini). This automatically decodes ESP32 crash stack traces — addresses in exception/backtrace output are resolved to function names and source file line numbers using the debug ELF file. The firmware is built with build_type = debug to include debug symbols required for decoding.

Initial Setup

  1. Power on the device
  2. The device enters AP mode and broadcasts a WPA2-PSK-protected WiFi network Klima XXXXXX (where XXXXXX is the device id, derived from the last 3 bytes of the MAC address). Read the WPA2 passphrase from the captive portal page itself or from the device's boot log over USB serial
  3. Connect to this network using the passphrase (default IP: 192.168.4.1)
  4. A captive portal opens automatically - enter your WiFi credentials
  5. The device restarts and connects to your network
  6. Access the web interface at klima-XXXXXX.local or check your router for the IP
  7. Configure temperature control settings in the web interface

Web Interface

Page Description
Control Monitor current temperature/humidity and set temperature control
Settings Configure WiFi, device name, MQTT, target temperature, and control parameters
Timers Set countdown timers or daily alarms for temperature control actions
About Device information and diagnostics

REST API

The project includes an OpenAPI specification documenting the primary API endpoints with request/response schemas. Interactive HTML documentation is deployed to GitHub Pages.

Note: The OpenAPI spec currently covers the primary endpoints. Additional endpoints (MQTT, OTA, sensors, settings) are documented in the source code.

Key Endpoints

Endpoint Method Description
/api/status GET Device status, current temperature, humidity, and control state
/api/temperature/target POST Set target temperature
/api/control GET Live controller state, gains in force, and control interval
/api/control/enable POST Enable/disable temperature control
/api/control/tuning POST Set PID gains and the control interval
/api/sensors GET Detailed sensor information

Configuration

All settings are persisted in ESP32 NVS (Non-Volatile Storage):

  • WiFi Config: SSID, password, connection status
  • Device Config: Device name, device ID, sensor I2C address, PID gains and control interval
  • Temperature Config: Target temperature, control enabled flag
  • MQTT Config: Broker address, port, topics (if enabled)
  • Syslog Config: Syslog server address, port, enabled flag

Project Structure

src/
  main.cpp                 # Entry point and device initialization
  Config.h/cpp             # NVS persistence and configuration management
  Network.h/cpp            # WiFi, mDNS, NTP, and webserver
  SensorController.h/cpp   # Sensor management and temperature control
  SensorDataLogger.h/cpp   # Historical data logging
  WebServerManager.h/cpp   # Web interface and REST API
  MqttClient.h/cpp         # MQTT connectivity
  SyslogOutput.h/cpp       # UDP syslog log forwarding
  Log.h                    # Project-level logging macros
  StatusLed.h/cpp          # Status LED control
  TimerScheduler.h/cpp     # Timer and alarm management
  TouchController.h/cpp    # Capacitive touch input
  OTAUpdater.h/cpp         # Over-the-air firmware updates
  CaptivePortal.h/cpp      # WiFi setup captive portal
  sensor/
    Sensor.h               # Sensor base class and measurement types
    SHT4x.h/cpp           # Temperature & humidity sensor
    BME680.h/cpp          # Environmental sensor (temp, humidity, pressure)
    BMP3xx.h/cpp          # Pressure sensor
    DPS310.h/cpp          # Pressure sensor
    SCD4x.h/cpp           # CO2, temperature & humidity sensor
    SGP40.h/cpp           # VOC index sensor
    BH1750.h/cpp          # Ambient light sensor
    TSL2591.h/cpp         # High-dynamic-range light sensor
    VEML7700.h/cpp        # Ambient light sensor
    PM25.h/cpp            # Particulate matter / air quality sensor
    DeviceSensor.h/cpp    # Device sensor abstraction
  task/
    SensorMonitor.h/cpp   # Sensor reading FreeRTOS task
data/
  control.html             # Main control page
  settings.html            # Settings page
  timers.html              # Timer management
  about.html               # Device information
tests/
  test_status_led/        # Status LED tests

Architecture

  • Single-core design: Optimized for ESP32-S2's single core
  • FreeRTOS tasks:
    • Network Task - handles WiFi, HTTP, mDNS, NTP, and webserver
    • Sensor Monitor Task - reads sensors and updates temperature control
  • Thread-safe: Direct method calls and shared data with proper synchronization
  • Persistent storage: All settings stored in ESP32 NVS
  • Real-time operation: Sensors read every second; the PID recomputes on the configured control interval (60 s by default), while the over-temperature shutoff is evaluated on every sensor cycle

Temperature Control

The temperature control system uses:

  • PID algorithm - Calculates control output based on temperature error
  • Per-driver range validation - Each sensor driver rejects implausible readings before they reach the controller (averaging multiple sensors is intentionally not used: a faulty reading diluted by a healthy one would still contaminate the loop)
  • Data logging - All measurements and control states logged for analysis
  • Configurable targets - Set any temperature within sensor range

Tuning

The gains and the control interval are stored in NVS and editable under Settings → Tuning or via POST /api/control/tuning. Values are validated on load and fall back per field; the API rejects an out-of-range value rather than clamping it.

Field Range Default
kp 0.01–100 0.5
ki 0–0.05 0.0001
kd 0–600 0
control_interval_s 1–600 60

ki and kd may be zero; kp may not, because a zero proportional gain disables control while control still reports itself as enabled. The defaults are derived arithmetically rather than measured, and are consistent with the autotuner's own Tyreus–Luyben derivation, which produces a PI controller with kd = 0 — on a floor whose time constant is hours, derivative action mostly amplifies sensor noise. Expect to revise them after the first converged autotune run on a real plant.

The control interval decimates only the PID computation. Sensors still read every second, and both the over-temperature shutoff and the autotuner's sampling are evaluated on every sensor cycle regardless — a safety limit noticed a minute late is a weaker guarantee, and coarser autotune sampling would bias its amplitude measurement. Because elapsed time is measured rather than assumed, the interval does not change what ki means.

Accepting an autotune result persists the derived gains. The controller adopts them on its next control tick rather than immediately, so GET /api/control reports the gains actually in force and is how a change is confirmed.

MQTT Integration

Optional MQTT support for:

  • Publishing current temperature and humidity
  • Publishing control state and target temperature
  • Subscribing to commands to enable/disable control
  • Remote monitoring and integration with home automation systems

Enable it under Settings → MQTT or via POST /api/mqtt.

Topics

With the configured prefix {prefix} (default sensors):

Topic Payload Retained
{prefix}/temperature, {prefix}/humidity, {prefix}/… Measurement JSON (time, value, unit, sensor, calculated) no
{prefix}/target_temperature Setpoint in °C, e.g. 21.5 yes
{prefix}/control_enabled ON / OFF yes
{prefix}/control_output PID output as a whole-number percent (0–100) no
{prefix}/heating Confirmed heating, ON / OFF no
{prefix}/status online while connected, offline otherwise yes

Every control-state topic is published as soon as its value changes, once on every (re)connect, and again on every control-loop computation — once per configured control interval (default 60 s) — independently of the measurement publish interval. A subscriber therefore sees the setpoint and demand of each control run even when nothing changed.

Commands

Publish to these to change the device (no retain — a retained command would be re-applied on every connect):

Topic Payload
{prefix}/target_temperature/set A number in [10.0, 30.0] °C
{prefix}/control_enabled/set ON / OFF

A setpoint outside that range, or a non-numeric payload, is rejected rather than clamped, so a mistyped value never replaces the stored one. Unknown control payloads are ignored.

Availability

The device registers an MQTT Last Will (retained offline on {prefix}/status), publishes a retained online when it connects and a retained offline before a clean disconnect. A broker or subscriber therefore sees offline both when the device is switched off deliberately and when it drops without warning.

Home Assistant

Turn on Settings → MQTT → Home Assistant Discovery (or POST /api/mqtt {"ha_discovery": true}) to have the device announce itself. It publishes one retained Home Assistant device-discovery message on homeassistant/device/<device_id>/config describing:

  • a climate entity (current temperature, target temperature, off/heat),
  • humidity and dew-point sensors,
  • a PID-output sensor, and
  • a heating binary sensor.

The message is republished after every reconnect, so the entities come back after a broker restart without re-toggling the setting. Turning the toggle off publishes an empty retained payload to the discovery topic, which removes the device from Home Assistant.

MQTT must be enabled for discovery to be published. Retained discovery outlives a firmware downgrade: disable discovery (or clear the topic by hand) before downgrading.

Syslog

Device logs can be forwarded to a remote syslog server via UDP (RFC 3164). Configure the syslog target in the web interface under Settings or via the /api/syslog endpoint.

  • Protocol: UDP syslog (RFC 3164)
  • Facility: user (1)
  • Hostname: uses the device's mDNS hostname (e.g. klima-aabbcc)
  • Severity mapping: E=error(3), W=warning(4), I=info(6), D=debug(7)
  • Zero overhead when disabled: no UDP socket allocated, inline isActive() check

E-Paper Display

An optional Waveshare 1.54" e-paper module (200×200, SSD1681) shows the current temperature and relative humidity. Disabled by default; enable it under Settings → E-Paper Display or via the /api/display endpoint.

The layout is a header band in a small font (KlimaControl top left, the running firmware version top right), the temperature and humidity, then a two-line footer carrying the device name, the wall clock, the setpoint, the controller demand bar and the control-state symbol.

Mock Display Examples

The following mock-ups are rendered pixel-exactly by the firmware's own drawing code via scripts/render_display_mock.py — they show each panel state, including the warning icon that appears in the margin:

Example Description
Normal operation on the e-paper mock: 21.4 °C, 45 %RH, heating active with demand bar at 3 segments Normal — current readings, heating active, demand bar and control-state symbol in the footer
Frost warning on the e-paper mock: 2.8 °C with the frost icon in the left margin Frost — temperature at or below the configured frost threshold; warning icon in the margin
Overheat warning on the e-paper mock: 31.6 °C with the overheat icon Overheat — the over-temperature safety shutoff has engaged
Sensor warning on the e-paper mock: value placeholders --.- and -- with the sensor icon Sensor — the sensor snapshot is invalid, so value placeholders are shown
Actuator warning on the e-paper mock: control state uncertain, empty demand bar Actuator — control state UNCERTAIN: the device cannot vouch for the valve, demand bar is empty
Humidity warning on the e-paper mock: 78 %RH with the humidity icon Humid — humidity at or above the configured limit

When several conditions are active at once, the highest-priority warning wins the single icon slot (overheat → frost → sensor → actuator → humid).

  • Zero idle power: e-paper keeps its image with no power between refreshes
  • Paged rendering: 625-byte page buffer rather than a 5 KB framebuffer, to keep internal SRAM above the OTA pre-flight gate
  • Refresh policy: value hysteresis (±0.1 °C / ±1 %RH), a configurable minimum interval (default 60 s), and a full refresh every 12 partials to clear ghosting
  • Version at a glance: the header band sits above the partial-refresh window, so it is repainted only by a full refresh — and therefore holds only compile-time-constant content. After an OTA update the first paint on the new firmware is a full refresh, so the version shown is always the one running
  • Fails safe: a disconnected panel is detected after three timed-out refreshes and stops being driven, without touching the saved configuration

See E-Paper Display Wiring for hardware setup.

Over-the-Air (OTA) Updates

Firmware updates from GitHub releases:

  • Checks for new releases automatically
  • Downloads .bin files directly from GitHub
  • Safe automatic rollback if update fails
  • Zero downtime updates (except brief restart)

See OTA Documentation for detailed setup.

Development

Specifications

The project includes two types of specifications:

  1. OpenSpec Documents in openspec/ - spec-driven capability specifications
  2. OpenAPI Specifications in docs/api/ - API interface definitions

OpenSpec Validation

The OpenSpec documents in the openspec/ directory define the system architecture, requirements, and behavior, and the spec-driven change workflow used to evolve them. See AGENTS.md for the full workflow. These are validated using the official OpenSpec CLI:

  • GitHub Actions automatically validates all OpenSpec documents on push/pull request
  • HTML documentation is generated from the Markdown specs
  • Validation includes proper formatting, cross-references, and requirement syntax

To validate locally (run from the repo root, where openspec/ lives):

openspec validate --all --strict   # or: scripts/validate-openspec.sh

API Documentation

The project includes OpenAPI specification files in the docs/api/ directory. When you push changes to these files:

  1. GitHub Actions automatically validates the OpenAPI specs
  2. HTML documentation is generated using Redoc
  3. The documentation is deployed to GitHub Pages

To validate OpenAPI specs locally:

spectral lint docs/api/*.yaml

To generate HTML documentation locally:

redoc-cli bundle docs/api/klimacontrol-api.yaml -o docs/api/index.html

API Documentation

The project includes OpenAPI specification files in the docs/api/ directory. When you push changes to these files:

  1. GitHub Actions automatically validates the OpenAPI specs
  2. HTML documentation is generated using Redoc
  3. The documentation is available as a downloadable artifact

To view the API documentation:

  1. Go to the Actions tab in GitHub
  2. Find the latest "OpenAPI Validation and Documentation" workflow run
  3. Download the "api-documentation" artifact
  4. Open the HTML files in your browser

You can also generate the documentation locally:

# Install required tools
npm install -g redoc-cli

# Generate HTML from OpenAPI spec
redoc-cli bundle docs/api/klimacontrol-api.yaml -o docs/api/index.html

Run Tests

# Run all tests
pio test -e native

# Run specific test suite
pio test -e native -f test_status_led

# Verbose output
pio test -e native -v

Test Coverage

To track test coverage, install lcov:

# macOS
brew install lcov

# Ubuntu
sudo apt-get install lcov

Coverage report is automatically generated after running tests.

Memory Management

  • No raw pointers: All dynamic memory managed via std::unique_ptr
  • RAII pattern: Resources automatically released when out of scope
  • RAM budget: ~320KB internal SRAM + ~2MB PSRAM, currently using ~56KB internal (17.2%)
  • Flash budget: 4MB available, currently using ~1MB (25%)

Constraints

  • PSRAM: the board exposes ~2 MB of PSRAM, but task stacks, FreeRTOS / lwIP / WiFi / mbedTLS / DMA allocations remain internal-SRAM-only — the OTA gate and the network-task low-heap restart guard both use heap_caps_get_free_size(MALLOC_CAP_INTERNAL).
  • Single-core: All tasks run on one core, optimize for responsiveness
  • Stack size: Sensor Monitor task has 6KB stack, Network task has 8KB stack (both HWM-driven; periodic "stack HWM" lines log the live measurement)
  • JSON buffer: 512 bytes for API responses

License

This project is licensed under the Apache License, Version 2.0. See the LICENSE file for the full license text.

About

A climate sensor logger and room climate control client.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages