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.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ BLE Temp │ BLE │ MQTT Gateway │ MQTT │ Sonoff Basic │
│ Sensor │────────>│ (OMG ESP32) │────────>│ Thermostat │
│ (Xiaomi) │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ MQTT │ MQTT
▼ ▼
┌────────────────────────────────────────┐
│ MQTT Broker (Mosquitto) │
└────────────────────────────────────────┘
│
│ MQTT Discovery
▼
┌──────────────────┐
│ Home Assistant │
└──────────────────┘
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
}
Once MQTT discovery is published, Home Assistant automatically creates:
climate.livingroom_thermo:
state: heat
temperature: 20.0
current_temperature: 19.5
preset_mode: comfort
action: heating
In configuration.yaml:
climate:
- platform: mqtt
name: "Living Room Thermostat"
modes:
- "off"
- "heat"
preset_modes:
- comfort
- eco
- boost
- away
mode_state_topic: "homeassistant/climate/livingroom_thermo/state"
mode_state_template: "{{ value_json.mode }}"
temperature_state_topic: "homeassistant/climate/livingroom_thermo/state"
temperature_state_template: "{{ value_json.temperature }}"
current_temperature_topic: "homeassistant/climate/livingroom_thermo/state"
current_temperature_template: "{{ value_json.current_temperature }}"
action_topic: "homeassistant/climate/livingroom_thermo/state"
action_template: "{{ value_json.action }}"
preset_mode_state_topic: "homeassistant/climate/livingroom_thermo/state"
preset_mode_value_template: "{{ value_json.preset_mode }}"
mode_command_topic: "homeassistant/climate/livingroom_thermo/mode/set"
temperature_command_topic: "homeassistant/climate/livingroom_thermo/temperature/set"
preset_mode_command_topic: "homeassistant/climate/livingroom_thermo/preset/set"
min_temp: 5
max_temp: 30
temp_step: 0.5
temperature_unit: C
automation:
- alias: "Eco mode at night"
trigger:
platform: time
at: "22:00:00"
action:
service: climate.set_preset_mode
target:
entity_id: climate.livingroom_thermo
data:
preset_mode: eco
- alias: "Comfort mode in morning"
trigger:
platform: time
at: "06:00:00"
condition:
condition: state
entity_id: binary_sensor.workday
state: 'on'
action:
service: climate.set_preset_mode
target:
entity_id: climate.livingroom_thermo
data:
preset_mode: comfort
temp_sensor_id = "A4:C1:38:9A:DA:64"mqtt_temp_topic = "home/OMG_ESP32_BLE/BTtoMQTT"Set your preferred temperatures:
Check MQTT messages:
mosquitto_sub -h localhost -t "homeassistant/climate/+/state" -v
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 »
ESPHome devices auto-discover in Home Assistant:
climate.thermostatsensor.current_temperatureswitch.relaylight.ledsensor.wifi_signalautomation:
- alias: "Morning Comfort Mode"
trigger:
- platform: time
at: "06:00:00"
action:
- service: climate.set_preset_mode
target:
entity_id: climate.thermostat
data:
preset_mode: comfort
- alias: "Night Eco Mode"
trigger:
- platform: time
at: "22:00:00"
action:
- service: climate.set_preset_mode
target:
entity_id: climate.thermostat
data:
preset_mode: eco
- alias: "Away Mode when leaving"
trigger:
- platform: state
entity_id: person.john
to: "not_home"
for: "00:30:00"
action:
- service: climate.set_preset_mode
target:
entity_id: climate.thermostat
data:
preset_mode: away
| 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.