ESPHome temperature / humidity sensor over 433MHz (instead of wifi)

Tags: #<Tag:0x00007fc411f20be8> #<Tag:0x00007fc411f20b20>

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.

Aside from the fact that I love you shared this, I can’t stop but thinking as well, man your other half is making it hard for you.

You could setup your network that way that the VLAN where your IoT stuff is in, can keep communicating, even at night. And all other devices are blocked from having any internet traffic.

Or create a seperate SSID for your IoT stuff (one your other half doesn’t know about) and maybe even hide the broadcasting of SSID. Lol

Aside from that I appreciate you sharing. I like the idea to have alternatives. One question though, any government regulations limit duty cycles for this band?

I have friends that turn their wifi off every night because of the “rays”. They are medical professionals.

If your wife is concerned about the rays, don’t tell he about the RF433 will you?

1 Like

Low frequency, better penetration, i dunno whats worse indeed 2.4ghz or 433 mhz. Also more power transmitted over 433 mhz indeed. Good catch @nickrout haha

Off topic: i don’t know if turning off wifi will be much of a difference for me when neighbours all around me have wifi and strong enough to connect to from my house. We’re exposed to radiation 24/7 anyway.

  • TV radiation
  • Cellular and Sft
  • Satelite
  • drinking water
  • smoking
  • cosmos radiation
  • Wifi
  • Zigbee
  • zwave
  • all kind of radio stations

Etc etc. Your home wifi is just like a drop on a hotplate