Hi all,
Just sharing something I did recently. Since our wifi get switched off during the night (other half demands it!!!), I’m always looking for ways to keep my devices talking to each other using other methods. Ethernet works great (using ESP32-PoE boards), but I don’t have data points everywhere.
I started thinking about using 433MHz as an alternative, and ended up using ESPHome to implement a simple custom protocol that allows my temperature / humidity sensor to broadcast its values over 433MHz. I already have an ESP32-PoE (running ESPHome) that has a 433MHz receiver, and so it listens for these messages, and then forwards them to Home Assistant. The end result is that I now have sensors around the house that are used in Home Assistant, but using 433MHz rather than wifi.
Below is the config for ESPHome running on the ESP8266. As you can see, it has a single DHT temperature / humidity sensor connected. When the values change, the createMessage()
function is called, which creates a custom message that gets sent over 433MHz. The createMessage()
function, along with the IDs for each source / sensor (SALON_TEMPERATURE_NUM
, SALON_HUMIDITY_NUM
, etc.) are defined in the included files. It’s great that ESPHome allows you to reuse the same code files on multiple ESP devices, using includes.
ESPHome config (transmitter)
esphome:
name: esp8266_1
platform: ESP8266
board: nodemcuv2
includes:
- includes/custom_messages.h
- includes/custom_messages.cpp
wifi:
ssid: "MY_WIFI_NETWORK"
password: "MY_WIFI_KEY"
reboot_timeout: 0s # disable reboot
captive_portal:
ota:
api:
reboot_timeout: 0s # disable reboot
logger:
remote_transmitter:
pin: D3
carrier_duty_percent: 100%
sensor:
- platform: dht
pin: D2
model: DHT22
update_interval: 60s
temperature:
id: local_temperature
name: "Local Temperature"
internal: True
unit_of_measurement: "°C"
icon: "mdi:thermometer"
on_value:
then:
- remote_transmitter.transmit_rc_switch_raw:
protocol:
pulse_length: 350
sync: [1, 31]
zero: [3, 1]
one: [1, 3]
repeat:
times: 5
wait_time: 0ms
code: !lambda |-
Message message = createMessage(SALON_TEMPERATURE_NUM, x, false); // inverted = false
return message.binaryText;
humidity:
id: local_humidity
name: "Local Humidity"
internal: True
unit_of_measurement: "%"
icon: "mdi:water-percent"
on_value:
then:
- delay: 2s # wait for temperature sensor to transmit first
- remote_transmitter.transmit_rc_switch_raw:
protocol:
pulse_length: 350
sync: [1, 31]
zero: [3, 1]
one: [1, 3]
repeat:
times: 5
wait_time: 0ms
code: !lambda |-
Message message = createMessage(SALON_HUMIDITY_NUM, x, false); // inverted = false
return message.binaryText;
Below is the config for ESPHome running on the ESP32-PoE, which listens for the messages. As you can see, it calls the parseMessage()
function to determine the source of the message, and the value provided. The corresponding template sensor is updated, which flows on to Home Assistant.
ESPHome config (receiver)
esphome:
name: esp32_poe_3
platform: ESP32
board: esp32-poe
includes:
- includes/custom_messages.h
- includes/custom_messages.cpp
ethernet:
type: LAN8720
mdc_pin: GPIO23
mdio_pin: GPIO18
clk_mode: GPIO17_OUT
phy_addr: 0
power_pin: GPIO12
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
sensor:
- platform: template
id: salon_temperature
name: "Salon Temperature"
unit_of_measurement: "°C"
device_class: "temperature"
accuracy_decimals: 1
- platform: template
id: salon_humidity
name: "Salon Humidity"
unit_of_measurement: "%"
device_class: "humidity"
accuracy_decimals: 1
remote_receiver:
pin:
number: GPIO33
inverted: True
dump: rc_switch
idle: 2ms
on_rc_switch:
- lambda: !lambda |-
Message message = parseMessage(x.code, true); // inverted = true
if (message.header == CUSTOM_HEADER)
{
switch (message.sourceId)
{
case SALON_TEMPERATURE_NUM:
id(salon_temperature).publish_state(message.value);
break;
case SALON_HUMIDITY_NUM:
id(salon_humidity).publish_state(message.value);
break;
}
}
Below are the contents of the included code files. The header file (custom_messages.h
) contains the main definitions of the custom protocol. Basically, the protocol is based around the following 24-bit message structure:
- Bits 1 to 4 - custom header (to distinguish this protocol from others)
- Bits 5 to 8 - source of the message (i.e. which sensor is sending data)
- Bits 9 to 24 - value being sent (i.e. temperature - multiplied by 100 to make it an integer)
homeassistant/esphome/includes/custom_messages.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sstream>
#include <bitset>
#include <map>
const unsigned short CUSTOM_HEADER = 7;
enum SourceId: unsigned short
{
SALON_TEMPERATURE_NUM = 1,
SALON_HUMIDITY_NUM = 2,
};
struct SourceInfo {
SourceId id;
float scale;
};
extern std::map<SourceId, SourceInfo> sourceInfos;
struct Message {
unsigned long long code;
const char* binaryText;
unsigned long long header;
SourceId sourceId;
unsigned long long data;
float value;
};
const char* numberToBinaryString(unsigned long value, short length);
Message parseMessage(unsigned long long value, bool inverted);
Message createMessage(SourceId sourceId, float value, bool inverted);
const unsigned long long MESSAGE_MASK = 0xFFFFFF;
const unsigned long long HEADER_MASK = 0xF00000;
const unsigned short HEADER_START = 0;
const unsigned short HEADER_LENGTH = 4;
const unsigned long long SOURCE_MASK = 0x0F0000;
const unsigned short SOURCE_START = 4;
const unsigned short SOURCE_LENGTH = 4;
const unsigned long long DATA_MASK = 0x00FFFF;
const unsigned short DATA_START = 8;
const unsigned short DATA_LENGTH = 16;
const unsigned short MESSAGE_LENGTH = HEADER_LENGTH + SOURCE_LENGTH + DATA_LENGTH;
Below is the main code file (custom_messages.cpp
) contains the code that creates custom messages, and parses incoming messages.
homeassistant/esphome/includes/custom_messages.cpp
#include "custom_messages.h"
std::map<SourceId, SourceInfo> sourceInfos = {
{ SALON_TEMPERATURE_NUM, { SALON_TEMPERATURE_NUM, 100.0 } },
{ SALON_HUMIDITY_NUM, { SALON_HUMIDITY_NUM, 100.0 } },
};
const char* numberToBinaryString(unsigned long long value, unsigned short length)
{
std::string str = std::bitset<24>(value).to_string();
return str.c_str();
}
unsigned long long binaryStringToNumber(char* binaryString)
{
return std::bitset<8>(binaryString).to_ulong();
}
Message parseMessage(unsigned long long code, bool inverted)
{
Message message;
unsigned long long messageCode = (inverted ? ~code : code) & MESSAGE_MASK;
const char* binaryText = numberToBinaryString(messageCode, MESSAGE_LENGTH);
unsigned long long header = (messageCode & HEADER_MASK) >> (MESSAGE_LENGTH - HEADER_START - HEADER_LENGTH);
SourceId sourceId = static_cast<SourceId>((messageCode & SOURCE_MASK) >> (MESSAGE_LENGTH - SOURCE_START - SOURCE_LENGTH));
unsigned long long data = (messageCode & DATA_MASK) >> (MESSAGE_LENGTH - DATA_START - DATA_LENGTH);
SourceInfo sourceInfo = sourceInfos[sourceId];
message.code = code;
message.binaryText = binaryText;
message.header = header;
message.sourceId = sourceId;
message.data = data;
message.value = (float)(data / sourceInfo.scale);
return message;
}
Message createMessage(SourceId sourceId, float value, bool inverted)
{
SourceInfo sourceInfo = sourceInfos[sourceId];
unsigned long long data = (unsigned long long)(value * sourceInfo.scale);
unsigned long long code = 0;
code = code ^ ((CUSTOM_HEADER << (MESSAGE_LENGTH - HEADER_START - HEADER_LENGTH)) & HEADER_MASK);
code = code ^ ((sourceId << (MESSAGE_LENGTH - SOURCE_START - SOURCE_LENGTH)) & SOURCE_MASK);
code = code ^ ((data << (MESSAGE_LENGTH - DATA_START - DATA_LENGTH)) & DATA_MASK);
Message message = parseMessage(code, inverted);
return message;
}
All in all, a fun exercise where I learnt more about 433MHz, and thanks to ESPHome, I was able to send / receive custom 433MHz messages containing temperature and humidity values.