Restructure to modern ESPHome component with native Fan integration
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::Faninterface with 4 speeds + 3 timer presets
Configuration
- Replaced 7 custom switch platforms with single native fan entity
- Removed manual
includes:andlibraries: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 withincludes:andlibraries: - 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.