This guide explains how to build a climate controller using a Sonoff Basic R2 (ESP8266) that reads temperature via MQTT and controls heating through its relay. This approach is used in this project and separates temperature sensing from the controller.
LINE (L) ────┬──────────────> Sonoff Input (L)
│
┌─┴─┐
│ │ Heating System
│ │ (Boiler, etc.)
└─┬─┘
│
NEUTRAL ─────┼──────────────> Sonoff Input (N)
│
└──────────────> Sonoff Output (N)
Sonoff Output (L) ─────────> Heating System Control
IMPORTANT SAFETY:
Disassemble Sonoff Basic
Solder header pins to programming pads:
Connect USB-to-Serial adapter:
USB-Serial Sonoff Basic
3.3V --> 3.3V
GND --> GND
TX --> RX
RX --> TX
Enter flash mode:
Flash with PlatformIO:
pio run --target upload
Once flashed initially, updates can be done via WiFi:
// Already included in main.cpp
#include <ArduinoOTA.h>
Update via PlatformIO:
pio run --target upload --upload-port <IP_ADDRESS>
Or configure in platformio.ini:
upload_protocol = espota
upload_port = 192.168.1.95
Thermostat/
├── platformio.ini # Build configuration
├── include/
│ ├── mqtt_handler.h # MQTT functions
│ ├── thermostat.h # Thermostat logic
│ └── web_config.h # WiFi/MQTT config storage
├── src/
│ ├── main.cpp # Main program
│ ├── mqtt_handler.cpp # MQTT implementation
│ └── thermostat.cpp # Thermostat algorithm
└── data/
└── index.html # Web configuration page
From mqtt_handler.cpp:
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
// Normalize MAC address for comparison
// BLE topics: home/OMG_ESP32_BLE/BTtoMQTT/A4C1389ADA64
// Config may have: A4:C1:38:9A:DA:64
if(strstr(topic, normalized_sensor_id.c_str()) == nullptr) {
return; // Not our sensor
}
// Parse JSON from OpenMQTTGateway
JsonDocument doc;
deserializeJson(doc, payload, length);
if (doc["tempc"].is<float>()) {
currentTemp = doc["tempc"].as<float>();
}
}
How it works:
home/OMG_ESP32_BLE/BTtoMQTT/#{"tempc": 21.5, "hum": 45, ...}currentTemp variablevoid mqtt_publish_state(PubSubClient& client, ThermostatMode mode,
float targetTemp, bool heating) {
JsonDocument doc;
doc["mode"] = "heat"; // or "off"
doc["preset_mode"] = "comfort"; // eco, boost, away
doc["temperature"] = targetTemp;
doc["current_temperature"] = currentTemp;
doc["action"] = heating ? "heating" : "idle";
String topic = "homeassistant/climate/" + device_id + "/state";
client.publish(topic.c_str(), buffer, length);
}
void mqtt_publish_discovery(PubSubClient& client) {
// Publishes to: homeassistant/climate/<device_id>/config
// Contains full entity configuration:
// - Device info
// - Available modes
// - Temperature range
// - State/command topics
// - Templates for parsing JSON
}
Create a configuration structure (in web_config.h):
struct Config {
char wifi_ssid[32];
char wifi_pass[64];
char mqtt_server[64];
int mqtt_port;
char mqtt_user[32];
char mqtt_pass[64];
char mqtt_device_id[32]; // e.g., "livingroom_thermo"
char mqtt_temp_topic[128]; // e.g., "home/OMG_ESP32_BLE/BTtoMQTT"
char temp_sensor_id[18]; // e.g., "A4:C1:38:9A:DA:64"
float preset_confort; // e.g., 20.0°C
float preset_eco; // e.g., 17.0°C
float preset_boost; // e.g., 22.0°C
float preset_hors_gel; // e.g., 8.0°C
};
Store in EEPROM for persistence across reboots.
home/OMG_ESP32_BLE/BTtoMQTT/A4C1389ADA64
Payload example:
{
"id": "A4:C1:38:9A:DA:64",
"name": "LYWSD03MMC",
"rssi": -67,
"brand": "Xiaomi",
"model": "LYWSD03MMC",
"tempc": 21.5,
"tempf": 70.7,
"hum": 45.2,
"batt": 95
}
Flash pvvx firmware for better features:
Flash via web: https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html
void Thermostat::update(float currentTemp) {
if (mode == MODE_OFF) {
heating = false;
digitalWrite(RELAY_PIN, LOW);
return;
}
float target = getTargetTemp();
// Hysteresis: 0.5°C deadband to prevent relay chattering
if (currentTemp < target - 0.5) {
heating = true;
digitalWrite(RELAY_PIN, HIGH); // Turn on heating
} else if (currentTemp > target + 0.5) {
heating = false;
digitalWrite(RELAY_PIN, LOW); // Turn off heating
}
// Within deadband: maintain current state
}
Monitor serial output:
pio device monitor
Check Home Assistant:
Check OMG is receiving BLE:
Check MAC address matching:
A4C1389ADA64A4:C1:38:9A:DA:64normalizeMac())Enable debug logs:
Uncomment Serial.print statements in mqtt_callback()
Check relay GPIO:
#define RELAY_PIN 12Check relay logic:
digitalWrite(RELAY_PIN, HIGH) = ONdigitalWrite(RELAY_PIN, LOW) = OFFTest relay manually:
digitalWrite(RELAY_PIN, HIGH);
delay(2000);
digitalWrite(RELAY_PIN, LOW);
Check broker availability:
mosquitto_sub -h <broker_ip> -t "#" -v
Verify credentials:
Check buffer size:
platformio.ini: -DMQTT_MAX_PACKET_SIZE=1024Check discovery prefix:
homeassistantVerify retained flag:
client.publish(topic, payload, length, true)Check JSON validity:
| Component | Cost | Notes |
|---|---|---|
| Sonoff Basic R2 | $5-8 | Controller |
| ESP32 DevKit | $5 | OMG Gateway |
| Xiaomi LYWSD03MMC | $5 | Temperature sensor |
| Enclosure/wiring | $5 | Installation materials |
| Total | ~$20-25 | Per zone |
Compare to commercial:
Electrical Safety:
Overheating Protection:
Network Reliability:
Sensor Failure:
Replace simple hysteresis with PID for smoother control:
float kp = 2.0, ki = 0.1, kd = 0.5;
float error = target - currentTemp;
float integral += error * dt;
float derivative = (error - lastError) / dt;
float output = kp*error + ki*integral + kd*derivative;
Store weekly schedule in EEPROM:
struct Schedule {
uint8_t hour;
uint8_t minute;
ThermostatMode mode;
};
Integrate with motion sensors for automatic eco mode:
if (no_motion_for_30_minutes) {
setMode(MODE_ECO);
}
Adjust target based on outdoor temperature:
float compensation = (outdoor_temp - 10) * 0.2;
target = base_target - compensation;
Add a simple web interface for configuration (already in data/index.html):
Access via: http://<sonoff-ip>/
Log temperature and state for analysis:
String influx_payload = "thermostat,room=living ";
influx_payload += "temp=" + String(currentTemp) + ",";
influx_payload += "target=" + String(targetTemp) + ",";
influx_payload += "heating=" + String(heating ? 1 : 0);
mqtt.publish("influxdb/write", influx_payload.c_str());
Visualize:
This architecture provides:
The separation of sensing and control via MQTT creates a robust, scalable home heating system.
ESPHome provides a simpler, YAML-based alternative to custom Arduino code. It offers the same functionality with less programming and automatic Home Assistant integration.
Advantages:
Disadvantages:
Install ESPHome in Home Assistant:
Access ESPHome Dashboard:
Click « New Device » and follow wizard
# Install ESPHome
pip install esphome
# Create configuration
esphome wizard sonoff-thermostat.yaml
# Compile and upload
esphome run sonoff-thermostat.yaml
Create sonoff-thermostat.yaml:
esphome:
name: sonoff-thermostat
friendly_name: Living Room Thermostat
platform: ESP8266
board: esp01_1m
# Enable logging
logger:
level: DEBUG
# Enable Home Assistant API
api:
encryption:
key: "your-32-character-encryption-key"
# Enable OTA updates
ota:
password: "your-ota-password"
# WiFi configuration
wifi:
ssid: "YourSSID"
password: "YourPassword"
# Fallback AP if WiFi fails
ap:
ssid: "Sonoff-Thermostat"
password: "fallback-password"
# Web server for status and control
web_server:
port: 80
# MQTT (for receiving temperature from OMG)
mqtt:
broker: 192.168.1.100
username: mqtt_user
password: mqtt_password
discovery: true
discovery_prefix: homeassistant
# Subscribe to temperature sensor
on_message:
- topic: home/OMG_ESP32_BLE/BTtoMQTT/A4C1389ADA64
then:
- lambda: |-
// Parse JSON and update temperature
DynamicJsonDocument doc(512);
deserializeJson(doc, x.c_str());
if (doc.containsKey("tempc")) {
float temp = doc["tempc"];
id(current_temperature).publish_state(temp);
}
# Sonoff Basic GPIO definitions
binary_sensor:
- platform: gpio
pin:
number: GPIO0
mode: INPUT_PULLUP
inverted: true
name: "Button"
on_press:
- climate.control:
id: thermostat
mode: !lambda |-
if (id(thermostat).mode == CLIMATE_MODE_OFF) {
return CLIMATE_MODE_HEAT;
} else {
return CLIMATE_MODE_OFF;
}
- platform: status
name: "Status"
switch:
- platform: gpio
pin: GPIO12
id: relay
name: "Relay"
restore_mode: RESTORE_DEFAULT_OFF
output:
- platform: esp8266_pwm
pin: GPIO13
inverted: true
id: led_output
light:
- platform: monochromatic
output: led_output
id: led
name: "LED"
# Temperature sensor via MQTT
sensor:
- platform: template
name: "Current Temperature"
id: current_temperature
unit_of_measurement: "°C"
accuracy_decimals: 1
device_class: temperature
state_class: measurement
- platform: wifi_signal
name: "WiFi Signal"
update_interval: 60s
- platform: uptime
name: "Uptime"
# Climate component
climate:
- platform: thermostat
name: "Thermostat"
id: thermostat
sensor: current_temperature
# Default target temperature
default_target_temperature_low: 17°C
default_target_temperature_high: 22°C
# Visual settings for HA
visual:
min_temperature: 5°C
max_temperature: 30°C
temperature_step: 0.5°C
# Heating action
heat_action:
- switch.turn_on: relay
- light.turn_on: led
# Idle action
idle_action:
- switch.turn_off: relay
- light.turn_off: led
# Heat mode configuration
heat_mode:
- switch.turn_on: relay
# Off mode configuration
off_mode:
- switch.turn_off: relay
- light.turn_off: led
# Deadband (hysteresis)
heat_deadband: 0.5°C
heat_overrun: 0.5°C
# Preset modes
preset:
- name: comfort
default_target_temperature_low: 19°C
default_target_temperature_high: 20°C
mode: heat
- name: eco
default_target_temperature_low: 16°C
default_target_temperature_high: 17°C
mode: heat
- name: boost
default_target_temperature_low: 21°C
default_target_temperature_high: 22°C
mode: heat
- name: away
default_target_temperature_low: 7°C
default_target_temperature_high: 8°C
mode: heat
- name: sleep
default_target_temperature_low: 16°C
default_target_temperature_high: 18°C
mode: heat
text_sensor:
- platform: version
name: "ESPHome Version"
- platform: wifi_info
ip_address:
name: "IP Address"
ssid:
name: "SSID"
If you want to use Home Assistant to bridge the MQTT temperature to ESPHome:
Step 1: Create MQTT sensor in Home Assistant (configuration.yaml):
mqtt:
sensor:
- name: "BLE Temperature Sensor"
state_topic: "home/OMG_ESP32_BLE/BTtoMQTT/A4C1389ADA64"
unit_of_measurement: "°C"
value_template: "{{ value_json.tempc }}"
device_class: temperature
Step 2: Simplified ESPHome configuration (using HA API instead of MQTT):
esphome:
name: sonoff-thermostat
platform: ESP8266
board: esp01_1m
logger:
api:
encryption:
key: "your-encryption-key"
ota:
password: "your-ota-password"
wifi:
ssid: "YourSSID"
password: "YourPassword"
# Import temperature from Home Assistant
sensor:
- platform: homeassistant
name: "Current Temperature"
id: current_temperature
entity_id: sensor.ble_temperature_sensor
switch:
- platform: gpio
pin: GPIO12
id: relay
name: "Relay"
climate:
- platform: thermostat
name: "Thermostat"
id: thermostat
sensor: current_temperature
default_target_temperature_low: 17°C
default_target_temperature_high: 22°C
visual:
min_temperature: 5°C
max_temperature: 30°C
temperature_step: 0.5°C
heat_action:
- switch.turn_on: relay
idle_action:
- switch.turn_off: relay
heat_deadband: 0.5°C
heat_overrun: 0.5°C
preset:
- name: comfort
default_target_temperature_low: 19°C
default_target_temperature_high: 20°C
- name: eco
default_target_temperature_low: 16°C
default_target_temperature_high: 17°C
- name: boost
default_target_temperature_low: 21°C
default_target_temperature_high: 22°C
- name: away
default_target_temperature_low: 7°C
default_target_temperature_high: 8°C
For more precise temperature control:
climate:
- platform: pid
name: "PID Thermostat"
id: thermostat
sensor: current_temperature
default_target_temperature: 20°C
# PID parameters (tune these for your system)
control_parameters:
kp: 0.5
ki: 0.0005
kd: 0.0
output_averaging_samples: 5
derivative_averaging_samples: 5
# Deadband to prevent relay chattering
deadband_parameters:
threshold_high: 0.2°C
threshold_low: -0.2°C
heat_output: relay_output
output:
- platform: slow_pwm
pin: GPIO12
id: relay_output
period: 300s # 5 minute cycles
If using DS18B20 or DHT22 directly connected:
# For DS18B20
dallas:
- pin: GPIO4
sensor:
- platform: dallas
address: 0x123456789ABCDEF0
name: "Room Temperature"
id: current_temperature
# Or for DHT22
sensor:
- platform: dht
pin: GPIO4
temperature:
name: "Room Temperature"
id: current_temperature
humidity:
name: "Room Humidity"
model: DHT22
update_interval: 30s
Add visual feedback:
i2c:
sda: GPIO4
scl: GPIO5
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
lambda: |-
it.printf(0, 0, id(font), "Temp: %.1f°C", id(current_temperature).state);
it.printf(0, 16, id(font), "Target: %.1f°C", id(thermostat).target_temperature);
it.printf(0, 32, id(font), "Mode: %s", id(thermostat).mode == CLIMATE_MODE_HEAT ? "HEAT" : "OFF");
it.printf(0, 48, id(font), "Action: %s", id(thermostat).action == CLIMATE_ACTION_HEATING ? "ON" : "OFF");
font:
- file: "fonts/arial.ttf"
id: font
size: 12
Prepare Sonoff (same as Arduino method):
Flash from ESPHome Dashboard:
Or via CLI:
esphome run sonoff-thermostat.yaml --device /dev/ttyUSB0
Once flashed initially:
esphome run sonoff-thermostat.yaml --device sonoff-thermostat.local
Or use ESPHome Dashboard: Click « Install » → « Wirelessly »
Pour réaliser cela de manière propre et pérenne, la meilleure méthode consiste à :
Vous devez associer vos topics MQTT et vos payloads dans votre fichier de configuration :
...
mqtt:
climate:
- name: "Thermostat Salon"
modes:
- "off"
- "auto" # On utilise souvent 'auto' pour Eco ou Confort selon vos besoins
- "heat"
preset_modes:
- "Eco"
- "Confort"
- "Boost"
# Topics MQTT (à adapter selon votre équipement)
preset_mode_command_topic: "votre_topic/thermostat/preset/set"
preset_mode_state_topic: "votre_topic/thermostat/preset/state"
mode_command_topic: "votre_topic/thermostat/mode/set"
mode_state_topic: "votre_topic/thermostat/mode/state"
# Si les payloads envoyés par HA doivent correspondre exactement aux vôtres :
payload_off: "Off"
preset_mode_command_template: "{{ value }}"
...
Vous pouvez créer vos règles directement depuis l’interface graphique de Home Assistant :
Allez dans Paramètres > Automatisations et scènes > Automatisations.
Cliquez sur Créer une automatisation (puis Créer une nouvelle automatisation).
Déclencheur (Trigger) :
Deux choix s’offrent à vous :
Option A : Si le thermostat est configuré comme entité climate dans HA
Option B : Si vous préférez envoyer directement le message MQTT
Exemple complet d’automatisation en YAML
Voici à quoi ressemble l’automatisation en code. Vous pouvez l’ajouter directement dans le fichier automations.yaml ou via l’éditeur YAML de l’interface :
alias: "Chauffage : Passage en mode Confort le matin"
description: "Passe le thermostat en mode Confort à 7h00 en semaine"
trigger:
- platform: time
at: "07:00:00"
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
# Exemple via publication MQTT directe :
- action: mqtt.publish
data:
topic: "ma_maison/thermostat/cmd"
payload: "Confort"
mode: single
Astuce : Utiliser le Scheduler / Planning
Si vos horaires varient souvent, plutôt que de créer une automatisation par changement d’heure, vous pouvez installer l’intégration Schedule (Planning) native à Home Assistant (Paramètres > Appareils et services > Entrées > Créer une entrée > Planning) ou la carte HACS Scheduler Card. Cela vous permettra de dessiner visuellement vos plages horaires (Eco / Confort) sur une semaine.
| Feature | Custom Arduino | ESPHome |
|---|---|---|
| Development | C++ code required | YAML only |
| Complexity | High | Low |
| Flexibility | Very high | Moderate |
| HA Integration | MQTT discovery | Native API |
| OTA Updates | Manual setup | Built-in |
| Web Interface | Custom code | Built-in |
| Debugging | Serial only | Web logs + serial |
| Update frequency | As needed | Regular ESPHome updates |
| File size | Smaller (~300KB) | Larger (~500KB) |
| MQTT requirement | Yes | Optional |
| Learning curve | Steep | Gentle |
# Add fallback hotspot
wifi:
ssid: "YourSSID"
password: "YourPassword"
ap:
ssid: "Sonoff-Fallback"
password: "12345678"
# Check logs via web interface
# http://sonoff-thermostat.local/
logger:
level: VERBOSE
logs:
mqtt: DEBUG
json: DEBUG
# Test relay manually via web interface
# Or add a button in HA to toggle switch.relay
# Verify GPIO12 in configuration
switch:
- platform: gpio
pin: GPIO12 # Must be GPIO12 for Sonoff Basic
id: relay
If you have existing Arduino-based thermostats:
Document current settings:
Create ESPHome config matching your setup
Flash one device as a test
Verify operation for 24-48 hours
Migrate remaining devices
Update Home Assistant automations if needed (climate entity names might change)
Use ESPHome if:
Use Custom Arduino if:
For most home automation users, ESPHome is the recommended choice due to its simplicity and tight Home Assistant integration.
Le pilotage d’une résistance de radiateur (charge purement résistive) depuis un ESP32 à l’aide d’un MOC3041 est une excellente solution. La fonction de détection de passage par zéro du MOC3041 est parfaite ici car elle évite de générer des parasites sur le réseau électrique lors des commutations.
L’ESP32 ne peut pas fournir le courant nécessaire pour amorcer directement un gros triac de puissance, et le MOC3041 ne supporte pas la puissance du radiateur. Le MOC3041 sert donc d’intermédiaire.
Attention : Les broches d’un ESP32 fonctionnant en 3.3V, elles ne peuvent fournir en continu qu’environ 12 à 20 mA maximum selon les broches. Pour ne pas surcharger l’ESP32 et garantir la saturation de la LED, il est fortement recommandé d’utiliser un petit transistor de commutation (comme un 2N2222 ou un MOSFET BS170) piloté par l’ESP32, qui alimentera la LED du MOC3041 depuis le rail 5V (VIN) de votre carte.
Le choix du Triac de puissance
Le choix du Triac dépend directement de la puissance de votre radiateur. Pour du 230V alternatif :
| Puissance du radiateur | Courant efficace | Modèle de Triac recommandé |
|---|---|---|
| Jusqu’à 1000W | ~4.4 A | BTA08-600B (8A, 600V) |
| Jusqu’à 2000W | ~8.7 A | BTA16-600B (16A, 600V) |
| Jusqu’à 3000W | ~13 A | BTA24-600B (24A, 600V) |
Conseil de sécurité : Choisissez toujours la série BTA (ex: BTA16) plutôt que BTB. Les versions « BTA » possèdent une semelle métallique isolée électriquement de l’intérieur du composant, ce qui est crucial lorsque vous fixez le triac sur son dissipateur thermique.