ESPHOME-ITHO icon indicating copy to clipboard operation
ESPHOME-ITHO copied to clipboard

Restructure to modern ESPHome component with native Fan integration

Open Copilot opened this issue 2 months ago • 10 comments

Migrates from legacy inline custom component (single itho.h with 7 binary switches) to modern ESPHome component structure with native Fan platform integration.

Changes

Component Structure

  • Created components/itho_fan/ with proper Python codegen layer (__init__.py, fan.py, const.py) and C++ implementation (itho_fan.h, itho_fan.cpp)
  • Hub component manages RF communication, interrupt handling, and state
  • Fan component implements ESPHome's fan::Fan interface with 4 speeds + 3 timer presets

Configuration

  • Replaced 7 custom switch platforms with single native fan entity
  • Removed manual includes: and libraries: directives (now handled by component)
  • Added external_components: for local component loading
  • Simplified from 131 to 79 lines (-40%)

Fan Integration

  • Speed control: Low (1), Medium (2), High (3), Full (4)
  • Preset modes: "Timer 10min", "Timer 20min", "Timer 30min"
  • Timer countdown with automatic return to Low on expiration
  • State synchronization between RF events and fan entity

Before

switch:
  - platform: custom
    lambda: |-
      auto fansendlow = new FanSendLow();
      return {fansendlow};
    switches:
      name: "FanSendLow"
# ... 6 more switches

After

external_components:
  - source: {type: local, path: components}

itho_fan:
  id: itho_hub
  device_id: "10,87,81"
  interrupt_pin: D1
  remote_ids:
    - remote_id: "51,40,61"
      room_name: "Bathroom"

fan:
  - platform: itho_fan
    name: "Itho Ventilation"

Preserved Functionality

  • ESP8266 compatibility
  • CC1101 RF send/receive via ITHO-Lib
  • Interrupt-driven packet reception
  • Remote tracking with room identification
  • OTA interrupt safety
Original prompt

Problem Statement

The current ESPHOME-ITHO project uses the legacy inline custom component structure with a single itho.h header file and multiple binary switches. This needs to be restructured to follow the modern ESPHome custom component directory structure and use the native Fan component for better Home Assistant integration.

Current Structure

The project currently has:

  • itho.h - Single header file containing all component classes
  • itho.yaml - ESPHome configuration using inline custom components with includes: and libraries:
  • 7 separate switches for fan control (Low, Medium, High, Full, Timer1-3, Join)
  • Custom text sensors

Required Changes

Restructure the project to use the modern ESPHome custom component format with native Fan integration:

1. Create Component Directory Structure

Create a components/itho_fan/ directory with the following structure:

components/
└── itho_fan/
    ├── __init__.py
    ├── fan.py
    ├── itho_fan.h
    ├── itho_fan.cpp
    └── const.py

2. Component Files Implementation

components/itho_fan/__init__.py

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi
from esphome.const import CONF_ID

CODEOWNERS = ["@jodur"]
DEPENDENCIES = []
AUTO_LOAD = ["fan"]

CONF_DEVICE_ID = "device_id"
CONF_REMOTE_IDS = "remote_ids"
CONF_REMOTE_ID = "remote_id"
CONF_ROOM_NAME = "room_name"
CONF_PIN_INTERRUPT = "interrupt_pin"

itho_fan_ns = cg.esphome_ns.namespace("itho_fan")
IthoFanHub = itho_fan_ns.class_("IthoFanHub", cg.Component)

REMOTE_SCHEMA = cv.Schema(
    {
        cv.Required(CONF_REMOTE_ID): cv.string,
        cv.Required(CONF_ROOM_NAME): cv.string,
    }
)

CONFIG_SCHEMA = cv.Schema(
    {
        cv.GenerateID(): cv.declare_id(IthoFanHub),
        cv.Required(CONF_DEVICE_ID): cv.string,
        cv.Optional(CONF_REMOTE_IDS, default=[]): cv.ensure_list(REMOTE_SCHEMA),
        cv.Required(CONF_PIN_INTERRUPT): cv.int_,
    }
).extend(cv.COMPONENT_SCHEMA)


async def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])
    await cg.register_component(var, config)

    # Set device ID
    device_id_parts = config[CONF_DEVICE_ID].split(",")
    if len(device_id_parts) == 3:
        cg.add(
            var.set_device_id(
                int(device_id_parts[0]),
                int(device_id_parts[1]),
                int(device_id_parts[2]),
            )
        )

    # Set remote IDs
    for remote in config[CONF_REMOTE_IDS]:
        cg.add(var.add_remote_id(remote[CONF_REMOTE_ID], remote[CONF_ROOM_NAME]))

    # Set interrupt pin
    cg.add(var.set_interrupt_pin(config[CONF_PIN_INTERRUPT]))

    # Add library dependency
    cg.add_library("SPI", None)
    cg.add_library("https://github.com/jodur/ITHO-Lib#NewLib", None)

components/itho_fan/fan.py

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import fan
from esphome.const import CONF_OUTPUT_ID, CONF_ID
from . import itho_fan_ns, IthoFanHub, CONF_DEVICE_ID

DEPENDENCIES = ["itho_fan"]

IthoFan = itho_fan_ns.class_("IthoFan", cg.Component, fan.Fan)

CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
    {
        cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(IthoFan),
        cv.GenerateID(CONF_ID): cv.use_id(IthoFanHub),
    }
).extend(cv.COMPONENT_SCHEMA)


async def to_code(config):
    var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
    await cg.register_component(var, config)
    await fan.register_fan(var, config)

    hub = await cg.get_variable(config[CONF_ID])
    cg.add(var.set_hub(hub))

components/itho_fan/const.py

"""Constants for Itho Fan component."""

# Fan speeds
SPEED_LOW = 1
SPEED_MEDIUM = 2
SPEED_HIGH = 3
SPEED_FULL = 4

# Timer states
TIMER_10MIN = 13
TIMER_20MIN = 23
TIMER_30MIN = 33

# Timer durations in seconds
TIME_10MIN = 10 * 60
TIME_20MIN = 20 * 60
TIME_30MIN = 30 * 60

components/itho_fan/itho_fan.h

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/fan/fan.h"
#include "esphome/core/hal.h"
#include "IthoCC1101.h"
#include <vector>
#include <string>

namespace esphome {
namespace itho_fan {

struct RemoteInfo {
  std::string id;
  std::string room_name;
};

class IthoFanHub : public Component {
 public:
  void setup() override;
  void loop() override;
  void dump_config() override;
  
  void set_device_id(uint8_t id1, uint8_t id2, uint8_t id3);
  void add_remote_id(const std::string &id, const std::string &room_name);
  void set_interrupt_pin(uint8_t pin);
  
  void send_command(uint8_t command);
  
  int get_state() const { return state_; }
  int get_timer() const { return timer_; }
  std::string get_last_id() const { return last_id_; }
  
  void register_fan(class IthoFan *fan) { fan_ = fan; }

 protected:
  friend void IRAM_ATTR itho_interrupt_handler();
  
  void process_packet();
  int get_remote_index(const std::string &id);
  void set_state(int state, int timer, const ...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot avatar Dec 25 '25 11:12 Copilot