[Guide] Direct Davis Vantage Receiver 868MHz via ESPHome & CC1101 (No Custom Components!)

Hi everyone,

I wanted to share a project that completely eliminates the need for external software bridges (like OpenMQTTGateway or rtl_433) or complex custom C++ components to read data from a Davis Vantage Pro2 / Vue (868MHz EU Variant) station.

Thanks to recent updates in the official ESPHome core, we can now configure the CC1101 transceiver natively into Packet Mode directly via YAML, catching and decoding the 19200 baud GFSK binary packets flawlessly on an ESP32-WROOM.

Special credits to the community (ventillo and dekay projects) for the binary specifications and layout logic.


:shopping_cart: Shopping List / Hardware Components

To build this receiver, you only need a few inexpensive hardware components and tools:

  1. ESP32 Development Board (WROOM Core): A standard 30-pin or 38-pin ESP32-WROOM board. (Do not use an ESP32-C3 for this specific layout, as it requires a different pin configuration). This is the one i used:

  2. CC1101 868MHz Wireless Module with Ribbon Cable: Ensure you purchase the 868MHz version (often sold with a green or blue PCB). Most modules come with a small copper spiral antenna included, which can be used when distance is small.

  3. Connection Options: You can either use temporary Dupont jumper wires for testing, or use a soldering station and a multi-core cable to solder the ribbon cable directly to the ESP32 pins. A soldered connection is highly recommended for long-term reliability.
    This is my setup:

I still have to design and print a housing, but could not wait to share what i made so far.

:hammer_and_wrench: Hardware Requirements & Wiring (ESP32-WROOM)

Connect your CC1101 module to the hardware pins of your ESP32-WROOM using the pinout reference image below:

CC1101 Pin ESP32 WROOM Pin Function
VCC 3V3 Power (Do NOT use 5V!)
GND GND Ground
MOSI GPIO 23 SPI MOSI
SCLK GPIO 18 SPI Clock
MISO GPIO 19 SPI MISO
GDO2 Not Connected Leave disconnected
GDO0 GPIO 4 Data Interrupt Pin
CSN GPIO 5 SPI Chip Select
ANT Antenna Solder antenna wire here

:warning: Important Range & Antenna Warning

Please note that when using the standard copper spiral antenna that comes with most CC1101 modules, the effective range is relatively short. To avoid excessive packet loss and CRC errors, you should keep the distance between the outdoor ISS unit and the CC1101 to a maximum of 15 meters. Upgrading to a better external antenna can significantly improve results and extend this range.

In my own setup, the distance is about 5 meters. My weather mast is mounted directly on the garage wall, allowing me to place the ESP32 and CC1101 inside the garage. This location was perfect because there is already a Wi-Fi access point present inside the garage to ensure a rock-solid network connection.


:light_bulb: The Passive Channel-Hopping Trick

Davis stations hop across 5 EU channels between ~868.04 and ~868.52 MHz. Instead of programming complex synchronized channel-hopping, we center the CC1101 right in the middle at 868.35MHz and open the filter_bandwidth wide to 325kHz. This broad window captures the transmissions across all hop-channels passively and reliably!


:memo: Step-by-Step Installation Guide

Step 1: Prepare the ESPHome Configuration

Create a new device in ESPHome, open the code editor, and paste the complete YAML configuration provided below. Make sure to update the password and key sections under wifi api and ota with your own credentials.

yaml

substitutions:
  name: davis-vantage-receiver
  friendly_name: Davis Vantage Receiver

esphome:
  name: ${name}
  friendly_name: ${friendly_name}

esp32:
  board: esp32dev       
  framework:
    type: arduino
    advanced:
      minimum_chip_revision: "3.1"
      sram1_as_iram: true

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Davis-Vantage-Receiver"
    password: "CREATEYOUROWN"

captive_portal:
web_server:
  port: 80

api:
  encryption:
    key: "CREATEYOUROWN"

logger:
  level: INFO # Keeps your logs clean, only logs validated station updates

ota:
  - platform: esphome
    password: "CREATEYOUROWN"

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

globals:
  - id: rain_count_prev
    type: int
    initial_value: '-1'
  - id: rain_total_mm
    type: float
    initial_value: '0.0'
  - id: raw_wind_dir
    type: float
    initial_value: '0.0'
  - id: known_unit_id
    type: int
    initial_value: '-1'

cc1101:
  id: my_cc1101
  cs_pin: GPIO5
  gdo0_pin: GPIO4       
  frequency: 868.35MHz     
  modulation_type: GFSK
  symbol_rate: 19200
  fsk_deviation: 9.5kHz
  filter_bandwidth: 325kHz 
  magn_target: 33dB
  packet_mode: true
  packet_length: 8        
  sync_mode: 16/16
  sync1: 0xCB
  sync0: 0x89
  crc_enable: false
  whitening: false
  num_preamble: 2

  on_packet:
    then:
      - lambda: |-
          if (x.size() < 8) return;

          // 1. Bit-reversal (LSB naar MSB mapping)
          std::vector<uint8_t> d(8);
          for (int i = 0; i < 8; i++) {
            uint8_t b = x[i];
            b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
            b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
            b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
            d[i] = b;
          }

          // 2. De robuuste rotatieloop die de bit-shifts feilloos opvangt
          bool crc_ok = false;
          for (int shift = 0; shift < 3; shift++) {
            uint16_t crc = 0;
            for (int i = 0; i < 6; i++) {
              crc ^= (uint16_t)d[i] << 8;
              for (int j = 0; j < 8; j++) {
                crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1;
              }
            }
            
            uint16_t ontvangen_crc_1 = ((uint16_t)d[6] << 8) | d[7];
            uint16_t ontvangen_crc_2 = ((uint16_t)d[7] << 8) | d[6];
            
            if (crc == ontvangen_crc_1 || crc == ontvangen_crc_2) {
              crc_ok = true;
              break;
            }

            uint8_t carry = 0;
            for (int i = 0; i < 8; i++) {
              uint8_t next_carry = (d[i] & 0x01) << 7;
              d[i] = (d[i] >> 1) | carry;
              carry = next_carry;
            }
          }

          if (!crc_ok) return; 

          // 3. Zender-ID lock (ID 1 = intern 0)
          uint8_t unit_id = d[0] & 0x07;
          bool battery_low = (d[0] & 0x08) != 0;

          if (id(known_unit_id) < 0) {
            id(known_unit_id) = unit_id;
            ESP_LOGI("davis", "Station-ID succesvol vastgelegd op: %d", unit_id);
          } else if (unit_id != id(known_unit_id)) {
            return; 
          }

          // Verzend status naar HA
          id(davis_battery_low).publish_state(battery_low);
          id(davis_rssi).publish_state(rssi);
          id(davis_lqi).publish_state(lqi);

          // Windverwerking (bytes 1 en 2)
          float wind_kmh = d[1] * 1.60934f;
          float wind_dir = d[2] * (360.0f / 255.0f);
          
          if (wind_kmh >= 0.0f && wind_kmh < 160.0f) {
            id(davis_wind_speed).publish_state(wind_kmh);
            if (std::isnan(id(davis_windgust).state)) {
              id(davis_windgust).publish_state(wind_kmh);
            }
          }
          if (wind_dir >= 0.0f && wind_dir <= 360.0f) {
            id(raw_wind_dir) = wind_dir;
            id(davis_wind_dir_combined).update();
          }

          // Berichttypes ontleden
          uint8_t ptype = d[0] >> 4;
          if (ptype == 8) {
            // Buitentemperatuur
            uint16_t raw = ((uint16_t)d[3] << 8) | d[4];
            float temp_c = (raw / 160.0f - 32.0f) * 5.0f / 9.0f;
            if (temp_c > -15.0f && temp_c < 45.0f) {
              id(davis_temp).publish_state(temp_c);
              ESP_LOGI("davis", "Weather Update - Temperature: %.1f°C", temp_c);
            }
          } else if (ptype == 9) {
            // Windvlaag
            float gust_kmh = d[3] * 1.60934f;
            if (gust_kmh >= 0.0f && gust_kmh < 200.0f) id(davis_windgust).publish_state(gust_kmh);
          } else if (ptype == 10) {
            // Luchtvochtigheid
            uint16_t raw = (((uint16_t)(d[4] >> 4) & 0x03) << 8) | d[3];
            float hum = raw / 10.0f;
            if (hum > 0.0f && hum <= 100.0f) {
              id(davis_hum).publish_state(hum);
              ESP_LOGI("davis", "Weather Update - Luchtvochtigheid: %.0f%%", hum);
            }
          } else if (ptype == 14) {
            // Regenval
            int tips = ((int)d[3] + (((int)d[4] >> 7) << 8)) & 0x7F;
            if (id(rain_count_prev) >= 0) {
              int delta = tips - id(rain_count_prev);
              if (delta < 0) delta += 128;
              if (delta > 0 && delta < 10) {
                id(rain_total_mm) += delta * 0.2f;
                id(davis_rain).publish_state(id(rain_total_mm));
              }
            }
            id(rain_count_prev) = tips;
          }

binary_sensor:
  - platform: template
    name: "Davis Battery Low"
    id: davis_battery_low
    device_class: battery

sensor:
  - platform: template
    name: "Davis Temperature"
    id: davis_temp
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1

  - platform: template
    name: "Davis Humidity"
    id: davis_hum
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0

  - platform: template
    name: "Davis Wind Speed"
    id: davis_wind_speed
    unit_of_measurement: "km/h"
    device_class: wind_speed
    state_class: measurement
    accuracy_decimals: 1
    filters:
      - sliding_window_moving_average:
          window_size: 5
          send_every: 1

  - platform: template
    name: "Davis Wind Gust"
    id: davis_windgust
    unit_of_measurement: "km/h"
    device_class: wind_speed
    state_class: measurement
    accuracy_decimals: 1

  - platform: template
    name: "Davis RSSI"
    id: davis_rssi
    unit_of_measurement: "dBm"
    state_class: measurement
    device_class: signal_strength
    entity_category: diagnostic
    accuracy_decimals: 1

  - platform: template
    name: "Davis LQI"
    id: davis_lqi
    state_class: measurement
    entity_category: diagnostic
    accuracy_decimals: 0

  - platform: template
    name: "Davis Daily Rain"
    id: davis_rain
    unit_of_measurement: "mm"
    device_class: precipitation
    state_class: total_increasing
    accuracy_decimals: 1

text_sensor:
  - platform: template
    name: "Davis Wind Direction"
    id: davis_wind_dir_combined
    icon: "mdi:compass-outline"
    lambda: |-
      float deg = id(raw_wind_dir);
      int index = int((deg + 11.25f) / 22.5f) % 16;
      
      const char* compassPoints[] = {
        "N", "NNE", "NE", "ENE",
        "E", "ESE", "SE", "SSE",
        "S", "SSW", "SW", "WSW",
        "W", "WNW", "NW", "NNW"
      };
      
      char buffer[32]; 
      snprintf(buffer, sizeof(buffer), "%.0f° %s", deg, compassPoints[index]);
      return std::string(buffer);

Step 2: Compile and Flash

Click Save and then Install. Connect your ESP32 via USB for the initial flash. Once the program compiles and uploads successfully, the device will boot up and automatically connect to your local Wi-Fi network. Subsequent updates can be done completely wireless over-the-air (OTA).

Step 3: Integrate into Home Assistant

Go to your Home Assistant dashboard, navigate to SettingsDevices & Services. You will see a notification stating "Discovered: Davis Vantage Receiver". Click Configure, click Submit, and all the sensors will be added natively to the "overview" dashboard instantly.

Step 4: Understanding Station-ID Configuration

Davis stations can transmit on 8 different channels (Transmitter IDs).

  • Auto-Lock Mode: By default, known_unit_id is set to -1. The ESP32 will automatically lock onto the first valid Davis station it hears in the air and ignore all other neighboring stations from that point forward.
  • Neighbor interference: Once locked, it will silently ignore any packets originating from other transmitter IDs.
  • Manual override: If your station is set to an ID other than 1, or if you want to explicitly hardcode it, change the initial_value: '-1' under id: known_unit_id in the globals: section. Note that Davis IDs map to the code internally as follows: Transmitter ID 1 = 0, ID 2 = 1, ID 3 = 2, up to ID 8 = 7.

Hope this helps anyone looking to run a lightweight, native Davis integration on ESPHome! Enjoy :smiling_face_with_sunglasses: :sun:

Hi Rainx,
Thanks for sharing your project. I followed your instructions, and it woks fine … on the first try !

I had added the UV Index (ptype == 4) :

if (ptype == 4) {
// UV Index
float uv_index = (((uint16_t)d[3] << 8) + (uint8_t)d[4] >> 6) / 50.0;
id(davis_uv_index).publish_state(uv_index);
}

and the Solar Radiation (ptype == 6) :

} else if (ptype == 6) {
// Solar Radiation
float solar_radiation = (((uint16_t)d[3] << 8) + (uint8_t)d[4] >> 6) * 1.757936;
id(davis_solar_radiation).publish_state(solar_radiation);
}

The UV Index works perfectly.
But it’s not the case for the Solar Radiation, because I don’t receive any message 6 (ptype == 6).

That’s very strange because I receive all other data (temperature, humidity, UV Index, daily rain, rain rate …). My Davis console receive and display correctly the Solar Radiation.

My Davis station is a Vantge Pro 2 (EU version).

If you (or someone) have a idea for solve my issue …

Many thankssss !