devices.esphome.io

Tuya Smart Weather Station

Tuya Smart Weather Station

Device Type: misc
Electrical Standard: global
Board: rtl87xx
Difficulty: Chip needs replacement, 5/5

General Notes

These devices are sold under various brand names but typically share the same internal hardware and TuyaMCU protocol.

The stock firmware relies on the Tuya cloud for weather data. This ESPHome configuration liberates the device by allowing it to connect directly to Home Assistant. It can then display weather data from any source you have configured in Home Assistant, such as the OpenWeatherMap integration, a personal weather station, or any other weather service.

The configuration is built on the rtl87xx platform using the LibreTiny framework, we use a custom branch for this since rtl8720c is not yet fully supported. We also use a custom tuya component in order to correctly handle the weather commands from TuyaMCU.

Tuya Smart Weather Station

Flashing Instuctions

This device is based on the WBR3 Tuya Module, you can check for flashing setup in here. With the WBR3 removed and with the probe pins soldered you can use ltchiptool to flash it via UART. We strongly recommend using an external power source for the 3v3 power supply. Make sure to connect the GND of the supply and the serial converter together, so they share the same GND reference.

Features

Home Assistant Integration

Pulls and displays real-time weather data (temperature, humidity, pressure, wind speed, UV index, real feel, and weather condition) directly from your Home Assistant sensors.

Local and RF Sensor Support

Displays and exposes data from its built-in sensor and up to three additional wireless RF sub-sensors.

GPIO Pinout

PinFunction
PA13UART RX (Connects to TuyaMCU TX)
PA14UART TX (Connects to TuyaMCU RX)
PA12Connected to the board, unknown functionality

TuyaMCU DP IDs

DP IDFunctionTypeValues / Notes
10224 Hour FormatBoolean0: 12h, 1: 24h
103Weather ConditionRawUnknown format
105Temperature UnitEnum0: Celsius, 1: Fahrenheit
106Panel BrightnessEnum0: Off, 1: 30%, 2: 60%, 3: 100%
108Panel Display ConfigRaw11-byte bitmask to show/hide screen elements
109Wind Speed UnitEnum0: mph, 1: km/h
110Pressure UnitEnum0: hPa, 1: mbar
129Night ModeBoolean0: Off, 1: On
130Night Mode DurationRaw4 bytes: [StartH] [StartM] [EndH] [EndM]
131Local TemperatureValueInteger, Celsius * 10
132Local HumidityValueInteger
133Sub1 TemperatureValueInteger, Celsius * 10
134Sub1 HumidityValueInteger
135Sub2 TemperatureValueInteger, Celsius * 10
136Sub2 HumidityValueInteger
137Sub3 TemperatureValueInteger, Celsius * 10
138Sub3 HumidityValueInteger

Example ESPHome Configuration

# WBR3 based Tuya Smart Weather Station
esphome:
name: weather-station
friendly_name: weather-station
substitutions:
temperature_entity: sensor.openweathermap_temperature
humidity_entity: sensor.openweathermap_humidity
pressure_entity: sensor.openweathermap_pressure
realfeel_entity: sensor.openweathermap_feels_like_temperature
uvi_entity: sensor.openweathermap_uv_index
windspeed_entity: sensor.openweathermap_wind_speed
# weathercode_entity will be translated to tuya code, if not
# using openweather modify the translation table.
weathercode_entity: sensor.openweathermap_weather_code
rtl87xx:
board: generic-rtl8720cf-2mb-992k
framework:
version: 0.0.0
# Adds OTA fixes from https://github.com/prokoma/libretiny
# as well as fixing UART pins so pins 13 and 14 don't
# fall into SW UART case.
source: https://github.com/vitoralb/libretiny#1a3f8ce
# implements a PoC weather service to handle TuyaMCU weather
# commands
external_components:
- source: github://vitoralb/[email protected]
components: [ tuya ]
logger:
baud_rate: 0
captive_portal:
mdns:
api:
password: ""
reboot_timeout: 0s
on_client_connected:
then:
- script.execute: retry_initial_weather_sync
ota:
platform: esphome
password: ""
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap: {}
time:
- platform: homeassistant
id: ha_time
datetime:
- platform: template
entity_category: CONFIG
name: "Night Mode Start Time"
id: night_mode_start
type: time
optimistic: true
set_action:
then:
- lambda: |-
std::vector<uint8_t> data;
data.push_back(x.hour);
data.push_back(x.minute);
data.push_back(id(night_mode_end).hour);
data.push_back(id(night_mode_end).minute);
id(tuya_mcu).set_raw_datapoint_value(130, data);
- platform: template
entity_category: CONFIG
name: "Night Mode End Time"
id: night_mode_end
type: time
optimistic: true
set_action:
then:
- lambda: |-
std::vector<uint8_t> data;
data.push_back(id(night_mode_start).hour);
data.push_back(id(night_mode_start).minute);
data.push_back(x.hour);
data.push_back(x.minute);
id(tuya_mcu).set_raw_datapoint_value(130, data);
uart:
rx_pin: GPIO13
tx_pin: GPIO14
baud_rate: 9600
tuya:
id: tuya_mcu
time_id: ha_time
on_datapoint_update:
- sensor_datapoint: 130
datapoint_type: raw
then:
- lambda: |-
ESP_LOGD("main", "Received Night Mode update from MCU: %s", format_hex_pretty(x).c_str());
if (x.size() == 4) { // [StartH][StartM][EndH][EndM]
TimeEntityRestoreState start;
TimeEntityRestoreState end;
start.hour = x[0];
start.minute = x[1];
start.second = 0;
end.hour = x[2];
end.minute = x[3];
end.second = 0;
start.apply(id(night_mode_start));
end.apply(id(night_mode_end));
}
sensor:
- platform: homeassistant
id: current_temperature
entity_id: ${temperature_entity}
internal: true
filters:
round: 0
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
weather_service->set_weather_data_int("w.temp", x);
weather_service->send_weather_data();
}
- platform: homeassistant
id: current_humidity
entity_id: ${humidity_entity}
internal: true
filters:
round: 0
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
weather_service->set_weather_data_int("w.humidity", x);
weather_service->send_weather_data();
}
- platform: homeassistant
id: current_pressure
entity_id: ${pressure_entity}
internal: true
filters:
round: 0
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
weather_service->set_weather_data_int("w.pressure", x);
weather_service->send_weather_data();
}
- platform: homeassistant
id: current_realfeel
entity_id: ${realfeel_entity}
internal: true
filters:
round: 0
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
weather_service->set_weather_data_int("w.realFeel", x);
weather_service->send_weather_data();
}
- platform: homeassistant
id: current_uvi
entity_id: ${uvi_entity}
internal: true
filters:
round: 0
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
weather_service->set_weather_data_int("w.uvi", x);
weather_service->send_weather_data();
}
- platform: homeassistant
id: current_windspeed
entity_id: ${windspeed_entity}
internal: true
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
char buffer[8];
snprintf(buffer, sizeof(buffer), "%.1f", (x / 3.6f));
weather_service->set_weather_data_string("w.windSpeed", buffer);
weather_service->send_weather_data();
}
- platform: homeassistant
id: raw_weathercode
entity_id: ${weathercode_entity}
internal: true
on_value:
then:
- component.update: current_weathercode
- platform: tuya
name: "Local Temperature"
sensor_datapoint: 131
device_class: "temperature"
unit_of_measurement: "°C"
accuracy_decimals: 1
filters:
multiply: 0.1
- platform: tuya
name: "Local Humidity"
sensor_datapoint: 132
device_class: "humidity"
unit_of_measurement: "%"
accuracy_decimals: 0
- platform: tuya
name: "Sub1 Temperature"
sensor_datapoint: 133
device_class: "temperature"
unit_of_measurement: "°C"
accuracy_decimals: 1
filters:
multiply: 0.1
- platform: tuya
name: "Sub1 Humidity"
sensor_datapoint: 134
device_class: "humidity"
unit_of_measurement: "%"
accuracy_decimals: 0
- platform: tuya
name: "Sub2 Temperature"
sensor_datapoint: 135
device_class: "temperature"
unit_of_measurement: "°C"
accuracy_decimals: 1
filters:
multiply: 0.1
- platform: tuya
name: "Sub2 Humidity"
sensor_datapoint: 136
device_class: "humidity"
unit_of_measurement: "%"
accuracy_decimals: 0
- platform: tuya
name: "Sub3 Temperature"
sensor_datapoint: 137
device_class: "temperature"
unit_of_measurement: "°C"
accuracy_decimals: 1
filters:
multiply: 0.1
- platform: tuya
name: "Sub3 Humidity"
sensor_datapoint: 138
device_class: "humidity"
unit_of_measurement: "%"
accuracy_decimals: 0
select:
- platform: tuya
name: "Display Brightness"
entity_category: CONFIG
enum_datapoint: 106
options:
"0": "Off"
"1": "30%"
"2": "60%"
"3": "100%"
- platform: tuya
name: "Unit Temperature"
entity_category: CONFIG
enum_datapoint: 105
options:
"0": "Celsius"
"1": "Fahrenheit"
- platform: tuya
name: "Unit Wind Speed"
entity_category: CONFIG
enum_datapoint: 109
options:
"0": "mph"
"1": "km/h"
- platform: tuya
name: "Unit Pressure"
entity_category: CONFIG
enum_datapoint: 110
options:
"0": "hPa"
"1": "mbar"
switch:
- platform: tuya
name: "Night Mode Enable"
entity_category: CONFIG
switch_datapoint: 129
- platform: tuya
name: "Display 24h Format"
entity_category: CONFIG
switch_datapoint: 102
- platform: template
entity_category: CONFIG
name: "Display UV Index"
id: show_uv
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
name: "Display Wind Speed"
entity_category: CONFIG
id: show_wind
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Pressure"
id: show_pressure
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Temperature"
id: show_temp
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Humidity"
id: show_hum
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Feels Like"
id: show_feels
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Local T&H"
id: show_local
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Sub T&H"
id: show_sub
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Date"
id: show_date
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Week"
id: show_week
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
- platform: template
entity_category: CONFIG
name: "Display Weather"
id: show_weather
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- script.execute: assemble_and_send_display_config
on_turn_off:
- script.execute: assemble_and_send_display_config
text_sensor:
- platform: template
id: current_weathercode
internal: true
update_interval: never
lambda: |-
std::string tuya_code = "0";
if (!isnan(id(raw_weathercode).state)) {
int owm_code = (int)id(raw_weathercode).state;
switch (owm_code) {
case 800: tuya_code = "146"; break; // Clear
case 801: tuya_code = "119"; break; // Few clouds
case 802: tuya_code = "129"; break; // Scattered clouds
case 803: tuya_code = "142"; break; // Broken clouds
case 804: tuya_code = "132"; break; // Overcast
case 701: case 741: tuya_code = "121"; break; // Mist, Fog
case 711: case 721: case 731: case 761: case 762: tuya_code = "140"; break; // Smoke, Haze, Dust, Ash
case 751: tuya_code = "103"; break; // Sand
case 771: tuya_code = "102"; break; // Squall
case 781: tuya_code = "116"; break; // Tornado
}
if (owm_code >= 200 && owm_code <= 232) tuya_code = "143"; // Thunderstorm
if (owm_code >= 300 && owm_code <= 321) tuya_code = "139"; // Drizzle
if (owm_code == 500 || owm_code == 520) tuya_code = "139"; // Light Rain
if (owm_code == 501 || owm_code == 521) tuya_code = "141"; // Moderate Rain
if ((owm_code >= 502 && owm_code <= 504) || (owm_code >= 522 && owm_code <= 531)) tuya_code = "101"; // Heavy Rain
if (owm_code == 511) tuya_code = "137"; // Freezing Rain
if (owm_code == 600 || owm_code == 615 || owm_code == 620) tuya_code = "130"; // Light Snow
if (owm_code == 601) tuya_code = "131"; // Snow
if (owm_code == 602 || owm_code == 622) tuya_code = "124"; // Heavy Snow
if ((owm_code >= 611 && owm_code <= 613) || owm_code == 616) tuya_code = "113"; // Sleet
}
return {tuya_code.c_str()};
on_value:
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
weather_service->set_weather_data_string("w.conditionNum", x);
weather_service->send_weather_data();
}
script:
- id: assemble_and_send_display_config
then:
- lambda: |-
std::vector<uint8_t> data;
data.push_back(id(show_uv).state ? 1 : 0);
data.push_back(id(show_wind).state ? 1 : 0);
data.push_back(id(show_pressure).state ? 1 : 0);
data.push_back(id(show_temp).state ? 1 : 0);
data.push_back(id(show_hum).state ? 1 : 0);
data.push_back(id(show_feels).state ? 1 : 0);
data.push_back(id(show_local).state ? 1 : 0);
data.push_back(id(show_sub).state ? 1 : 0);
data.push_back(id(show_date).state ? 1 : 0);
data.push_back(id(show_week).state ? 1 : 0);
data.push_back(id(show_weather).state ? 1 : 0);
id(tuya_mcu).set_raw_datapoint_value(108, data);
- id: retry_initial_weather_sync
mode: queued
then:
- lambda: |-
auto* weather_service = id(tuya_mcu).get_weather_service();
if (weather_service != nullptr) {
// Temperature
if (!isnan(id(current_temperature).state)) {
weather_service->set_weather_data_int("w.temp", id(current_temperature).state);
} else {
weather_service->set_weather_data_int("w.temp", 0);
}
// Humidity
if (!isnan(id(current_humidity).state)) {
weather_service->set_weather_data_int("w.humidity", id(current_humidity).state);
} else {
weather_service->set_weather_data_int("w.humidity", 0);
}
// Pressure
if (!isnan(id(current_pressure).state)) {
weather_service->set_weather_data_int("w.pressure", id(current_pressure).state);
} else {
weather_service->set_weather_data_int("w.pressure", 0);
}
// Real Feel
if (!isnan(id(current_realfeel).state)) {
weather_service->set_weather_data_int("w.realFeel", id(current_realfeel).state);
} else {
weather_service->set_weather_data_int("w.realFeel", 0);
}
// UV Index
if (!isnan(id(current_uvi).state)) {
weather_service->set_weather_data_int("w.uvi", id(current_uvi).state);
} else {
weather_service->set_weather_data_int("w.uvi", 0);
}
// Wind Speed
if (!isnan(id(current_windspeed).state)) {
char buffer[8];
snprintf(buffer, sizeof(buffer), "%.1f", (id(current_windspeed).state / 3.6f));
weather_service->set_weather_data_string("w.windSpeed", buffer);
} else {
weather_service->set_weather_data_string("w.windSpeed", "0");
}
weather_service->set_weather_data_string("w.conditionNum", id(current_weathercode).state);
weather_service->send_weather_data();
id(retry_initial_weather_sync).stop();
}
- delay: 10s
- script.execute: retry_initial_weather_sync
Edit this page on GitHub