[GUIDE] Controlling ITHO Daalderop fan with ESP8266 and CC1101

ESPHome 2025.12 now has native support for CC1101 :slight_smile:

My current (work in progress) configuration seems to work without any additional libraries on a esp32 and cc1101 to receive fan speed changes.

# https://esphome.io/components/cc1101/
# Configuration based on IthoCC1101::initReceiveMessage()
# See: https://github.com/letscontrolit/ESPEasy/blob/15830d38268eb28c3fe234413a81f6cf015aeda9/lib/Itho/IthoCC1101.cpp#L238
cc1101:
  cs_pin: GPIO22
  # Base/carrier frequency: 868.299866MHz
  frequency: 868.2999MHz
  # 2-FSK modulation
  modulation_type: 2-FSK
  # Symbol rate: MDMCFG4=0x5A, MDMCFG3=0x83 = 38.3835 kBaud
  symbol_rate: 38383
  # Filter bandwidth: ~203kHz (based on MDMCFG4=0x5A)
  filter_bandwidth: 203kHz
  # Frequency deviation: DEVIATN=0x50 = 50.78125kHz
  fsk_deviation: 50kHz
  # Output power
  output_power: 10
  # Enable packet mode
  gdo0_pin: GPIO16
  packet_mode: true
  # Packet mode configuration
  sync_mode: 16/16  # MDMCFG2=0x02 = 16-bit sync word
  sync1: 0xB3      # SYNC1=179 (0xB3)
  sync0: 0x2A      # SYNC0=42 (0x2A)
  packet_length: 63  # Fixed packet length (sync bytes removed by CC1101)
  crc_enable: false  # CRC disabled in ITHO
  whitening: false   # Data whitening disabled
  on_packet:
    then:
      - lambda: |-
          // ITHO packet decoder based on IthoCC1101::messageDecode()
          // Decode Manchester-like encoding with 1-0 pattern every 8 bits
          // STARTBYTE=2: Skip first 2 bytes after sync word

          if (x.size() != 63) {
            ESP_LOGW("itho", "Invalid packet length: %d", x.size());
            return;
          }

          // Decoded data buffer
          uint8_t decoded[32] = {0};
          uint8_t decoded_len = 0;

          // Decoding parameters - start from byte 2 (STARTBYTE)
          const int STARTBYTE = 2;
          uint8_t out_i = 0;         // byte index
          uint8_t out_j = 4;         // bit index (start at bit 4)
          uint8_t in_bitcounter = 0; // process per 10 input bits

          // Decode the packet starting from STARTBYTE
          for (int i = STARTBYTE; i < x.size(); i++) {
            for (int j = 7; j >= 0; j--) {
              // Select even bits (0, 2, 4, 6) for output
              if (in_bitcounter == 0 || in_bitcounter == 2 ||
                  in_bitcounter == 4 || in_bitcounter == 6) {
                uint8_t bit = (x[i] >> j) & 0x01;
                decoded[out_i] |= (bit << out_j);
                out_j++;
                if (out_j > 7) { out_j = 0; }
                if (out_j == 4) {
                  out_i++;
                  if (out_i >= 32) break;
                }
              }

              in_bitcounter++;
              if (in_bitcounter > 9) { in_bitcounter = 0; }
            }
            if (out_i >= 32) break;
          }
          decoded_len = out_i;

          // Log decoded packet
          std::string decoded_hex = format_hex_pretty(decoded, decoded_len);
          ESP_LOGI("itho", "Decoded (%d bytes): %s", decoded_len, decoded_hex.c_str());

          // Check for valid ITHO packet (min 12 bytes)
          if (decoded_len < 12) {
            ESP_LOGW("itho", "Decoded packet too short");
            return;
          }

          // Extract command bytes - check bytes 5-10 against command patterns
          // Based on IthoCC1101::checkIthoCommand() and command byte arrays
          std::string command = "Unknown";

          // Command bytes are at decoded[5..10]
          // Low:    22 F1 03 00 02 04 (0x22, 0xF1, 0x03, 0x00, 0x02, 0x04)
          // Medium: 22 F1 03 00 03 04 (0x22, 0xF1, 0x03, 0x00, 0x03, 0x04)
          // High:   22 F1 03 00 04 04 (0x22, 0xF1, 0x03, 0x00, 0x04, 0x04)
          // Timer:  22 F3 03 00 00 0A (0x22, 0xF3, 0x03, 0x00, 0x00, 0x0A/14/1E)

          if (decoded_len >= 11) {
            // Check common bytes: decoded[5]==0x22 and decoded[7]==0x03
            if (decoded[5] == 0x22 && decoded[7] == 0x03) {
              // Timer: byte[6]==0xF3, byte[9]==0x00, byte[10]==0x0A/0x14/0x1E
              if (decoded[6] == 0xF3 && decoded[9] == 0x00) {
                command = "Timer";
                id(itho_command).publish_state("Timer");
                id(itho_last_command).publish_state("Timer");
              }
              // Low/Medium/High: byte[6]==0xF1, byte[10]==0x04
              else if (decoded[6] == 0xF1 && decoded[10] == 0x04) {
                // Distinguish by byte[9]
                if (decoded[9] == 0x02) {
                  command = "Low";
                  id(itho_command).publish_state("Low");
                  id(itho_last_command).publish_state("Low");
                } else if (decoded[9] == 0x03) {
                  command = "Medium";
                  id(itho_command).publish_state("Medium");
                  id(itho_last_command).publish_state("Medium");
                } else if (decoded[9] == 0x04) {
                  command = "High";
                  id(itho_command).publish_state("High");
                  id(itho_last_command).publish_state("High");
                }
              }
            }
          }

          ESP_LOGI("itho", "Command: %s", command.c_str());

          // Clear command state after 2 seconds
          id(clear_command_timer).execute();

spi:
  clk_pin: GPIO18 # CC1101 SCK
  miso_pin: GPIO19 # CC1101 MISO/GDO1
  mosi_pin: GPIO23 # CC1101 MOSI

# Text sensors for ITHO commands
text_sensor:
  - platform: template
    name: "ITHO Command"
    id: itho_command
    icon: "mdi:fan"

  - platform: template
    name: "ITHO Last Command"
    id: itho_last_command
    icon: "mdi:fan-clock"

# Script to clear command state after 2 seconds
script:
  - id: clear_command_timer
    mode: restart
    then:
      - delay: 2s
      - lambda: |-
          id(itho_command).publish_state("");

todo: add pairing button and buttons to set speed/timer

2 Likes