The SiloCityLabs ESP32 Module for the AC Infinity AirTap T4/T6 already gives Home Assistant users a clean way to upgrade an AirTap vent into a connected smart-home device. The current ESPHome firmware path supports the important hardware pieces: PWM fan control, onboard temperature sensing, OLED display output, physical buttons, panel lockout, and newer IR receiver support on supported builds.
But there is another smart-home audience that could benefit from this hardware: Apple Home users.
If you’re in the Apple ecosystem, using HomePod, Siri on iPhone, Apple TV, scenes, automations, and the Apple Home app, the natural question is: Could the AirTap ESP32 upgrade kit run native Apple Home firmware instead of ESPHome?
Yes it can! HomeSpan provides developers with libraries to create an ESP32 firmware that pairs the AirTap directly with Apple Home over Wi-Fi. That’s native Apple support without Home Assistant, without a Home Assistant HomeKit Bridge, and without a separate server translating ESPHome entities into HomeKit accessories.
This post is not a finished firmware release. It is a technical roadmap for developers, makers, and advanced AirTap owners who want to understand what it would take to build one. It’s enough architecture to start the project, enough code structure to see the direction, and enough caution to avoid the obvious traps.
Why HomeSpan?
Most custom ESP32 smart-home firmware assumes that Home Assistant is the center of the house. That is fine for power users like us - we each have a home server running Home Assistant - and ESPHome is still one of the best ways to ship an ESP32-based IoT device quickly.
But not every smart-home user wants to manage Home Assistant. Apple users want the “house of the future” too: smart vents, quieter airflow, room-level automation, and better HVAC behavior, but they want the control surface to be the Apple Home app. They want to say:
- “Hey Siri, turn on the office vent.”
- “Set the bedroom vent to 40 percent.”
- “Turn off the vent display buttons.”
- “When the room gets warm, increase airflow.”
- “When the Good Night scene runs, lower the vent speed.”
That is a different market from the existing ESPHome/Home Assistant crowd. For SiloCityLabs, this is an opportunity: keep serving the Home Assistant community while opening the door to Apple-first smart-home users who may never search for ESPHome, YAML, or Home Assistant integrations.
The existing AirTap ESP32 hardware is a good fit
Fortunately the AirTap ESP32 upgrade kit already has the right kind of hardware for native Apple Home firmware. The firmware needs to handle:
- ESP32 module running Wi-Fi firmware
- PWM output for AirTap fan speed control
- ADC input for temperature sensing
- SSD1306 128x64 OLED display over I2C
- Local GPIO buttons
- Stored fan speed state
- Panel lockout state
- Optional IR receiver support on supported board versions
- Wi-Fi provisioning (SSID and password setup)
- OTA/update strategy
- A clean end-user pairing/reset story
The current ESPHome firmware proves that the board can already do the important work. HomeSpan just replaces the smart-home protocol layer. HomeSpan will expose HomeKit services directly to Apple Home.
That distinction matters for SEO, positioning, and buyer intent. “ESP32 smart vent for Home Assistant” reaches one audience. “Apple HomeKit smart vent upgrade for AC Infinity AirTap” reaches another.
What HomeSpan brings to the project
HomeSpan is an Arduino library for creating HomeKit accessories on ESP32. It implements Apple’s HomeKit Accessory Protocol for ESP32-class devices and allows the device to pair directly with Apple Home over Wi-Fi.
For this project, HomeSpan is attractive because it can model the AirTap as an actual Apple Home accessory instead of a group of translated Home Assistant entities.
A first Apple Home firmware could expose:
- A Fan service for on/off and speed control
- A Temperature Sensor service for the AirTap temperature reading
- A Switch service for panel lockout
- Accessory Information for manufacturer, model, serial, and firmware version
- Optional support for physical button events or local-only button behavior
From the Apple Home user’s perspective, the AirTap would feel like a native accessory. They would not need to understand ESPHome. They would not need to copy YAML. They would not need to run a local server.
They would flash firmware, pair the device, name the vent, assign it to a room, and automate it.
What we can reuse from the ESPHome firmware
The current ESPHome firmware is the best technical reference because it already describes the board behavior.
The 3-button firmware uses this core pattern:
GPIO2for PWM fan outputGPIO4for ADC temperature inputGPIO6for OLED SDAGPIO7for OLED SCLGPIO8,GPIO9, andGPIO10for local buttons- A stored integer fan speed from
0to10 - A panel lockout boolean
- An NTC temperature conversion pipeline
- An SSD1306 128x64 display at I2C address
0x3C
The 4-button firmware follows the same general structure, but adds the fourth button and IR receiver support:
GPIO2for PWM fan outputGPIO4for ADC temperature inputGPIO6for OLED SDAGPIO7for OLED SCLGPIO20for UpGPIO8for DownGPIO9for ToggleGPIO10for Mode/MenuGPIO3for IR receiver on supported 4-button firmware- Fan speed stored from
0to10 - Panel lockout support
- IR commands that can toggle, increment, or decrement fan speed
The important point is that none of this is ESPHome-specific. ESPHome makes it easy, but the underlying behavior is normal embedded firmware:
- Read GPIO
- Write PWM
- Read ADC
- Calculate temperature
- Update display
- Store state
- Expose controls to a smart-home system
That is exactly the kind of project HomeSpan can handle.
Recommended firmware scope
The first HomeSpan firmware should be intentionally boring. Version 0.1.0 should prove only four things:
- The firmware builds in PlatformIO.
- The ESP32 board boots reliably.
- The accessory pairs with Apple Home.
- Apple Home can set AirTap fan speed through PWM.
A realistic development order:
-
Board bring-up
Compile and flash a basic HomeSpan project on the AirTap ESP32 hardware. -
Apple Home pairing
Expose one Fan accessory and confirm that Apple Home can pair, unpair, and re-pair it. -
PWM fan output
Map Apple Home speed percentage to the existing AirTap 0-10 fan speed model. -
Local buttons
Let the physical buttons change the fan speed and keep Apple Home state in sync. -
Temperature sensor
Read the ADC, convert to temperature, and expose it as a HomeKit Temperature Sensor. -
OLED display
Show fan speed, temperature, Wi-Fi state, and pairing/setup hints. -
Panel lockout
Expose lockout as an Apple Home switch and block local button changes when enabled. -
IR receiver
Add optional IR support for board versions that include the IR sensor. -
Persistence
Store fan speed, lockout state, display preference, and calibration values. -
Release workflow
Add versioning, flashing instructions, recovery instructions, and board-specific builds.
This order delivers value early without trapping the project in menus before HomeKit core is proven.
How the AirTap Should Appear in Apple Home
Use this HomeKit model:
- Accessory:
AirTap Vent - Service:
Fan - Service:
Temperature Sensor - Service:
SwitchnamedPanel Lockout - Service:
Accessory Information
The Fan service exposes on/off state and speed percentage. Internally keep the 0-10 speed model; Apple Home sees 0-100%. Use this mapping:
| Apple Home Speed | Internal AirTap Speed |
|---|---|
| 0% | 0 |
| 1-10% | 1 |
| 11-20% | 2 |
| 21-30% | 3 |
| 31-40% | 4 |
| 41-50% | 5 |
| 51-60% | 6 |
| 61-70% | 7 |
| 71-80% | 8 |
| 81-90% | 9 |
| 91-100% | 10 |
Predictable behavior matters more than smoothness for the first release.
PlatformIO Project Layout
Start with a simple layout:
airtap-homespan/
├── platformio.ini
├── include/
│ ├── AirtapPins.h
│ ├── AirtapState.h
│ ├── AirtapFan.h
│ ├── AirtapButtons.h
│ ├── AirtapTemperature.h
│ ├── AirtapDisplay.h
│ └── AirtapHomeKit.h
├── src/
│ ├── main.cpp
│ ├── AirtapFan.cpp
│ ├── AirtapButtons.cpp
│ ├── AirtapTemperature.cpp
│ ├── AirtapDisplay.cpp
│ └── AirtapHomeKit.cpp
└── README.md
Keep code in main.cpp initially, then split into modules as it grows. Modular design matters for supporting multiple board revisions.
A Starting platformio.ini
HomeSpan targets Arduino on ESP32. Recent HomeSpan releases require a recent Arduino-ESP32 core, and PlatformIO’s default Espressif platform may lag behind the newest Arduino-ESP32 support. A practical option is using the pioarduino Espressif platform package when newer Arduino-ESP32 support is needed.
For the XIAO ESP32-C3 target, a starting platformio.ini could look like this:
[env:airtap_xiao_esp32c3]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
board = seeed_xiao_esp32c3
framework = arduino
monitor_speed = 115200
upload_speed = 921600
lib_deps =
HomeSpan/HomeSpan
adafruit/Adafruit SSD1306
adafruit/Adafruit GFX Library
board_build.partitions = min_spiffs.csv
build_flags =
-D AIRTAP_BOARD_3BTN_REV2
A future ESP32-C6 environment could look like this once the exact PlatformIO board target is confirmed:
[env:airtap_xiao_esp32c6]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
board = seeed_xiao_esp32c6
framework = arduino
monitor_speed = 115200
upload_speed = 921600
lib_deps =
HomeSpan/HomeSpan
adafruit/Adafruit SSD1306
adafruit/Adafruit GFX Library
board_build.partitions = min_spiffs.csv
build_flags =
-D AIRTAP_BOARD_4BTN_REV1
Start with the C3 variant; the existing ESPHome configs use seeed_xiao_esp32c3.
Pin Mapping
Keep pins in one header file, never hardcoded. Create AirtapPins.h:
#pragma once
#if defined(AIRTAP_BOARD_3BTN_REV2)
static constexpr int PIN_FAN_PWM = 2;
static constexpr int PIN_TEMP_ADC = 4;
static constexpr int PIN_OLED_SDA = 6;
static constexpr int PIN_OLED_SCL = 7;
static constexpr int PIN_BUTTON_UP = 8;
static constexpr int PIN_BUTTON_DOWN = 9;
static constexpr int PIN_BUTTON_MODE = 10;
static constexpr int PIN_BUTTON_TOGGLE = -1;
static constexpr int PIN_IR_RX = -1;
#elif defined(AIRTAP_BOARD_4BTN_REV1)
static constexpr int PIN_FAN_PWM = 2;
static constexpr int PIN_TEMP_ADC = 4;
static constexpr int PIN_OLED_SDA = 6;
static constexpr int PIN_OLED_SCL = 7;
static constexpr int PIN_BUTTON_UP = 20;
static constexpr int PIN_BUTTON_DOWN = 8;
static constexpr int PIN_BUTTON_TOGGLE = 9;
static constexpr int PIN_BUTTON_MODE = 10;
static constexpr int PIN_IR_RX = 3;
#else
#error "Select an AirTap board revision with a build flag"
#endif
This gives contributors a clear reference:
- If you have a 3-button board, build with
AIRTAP_BOARD_3BTN_REV2. - If you have a 4-button board, build with
AIRTAP_BOARD_4BTN_REV1. - If a future board revision changes pins, add one block instead of rewriting firmware.
Global State
#pragma once
struct AirtapState {
int fanSpeedStep = 0; // 0-10
bool panelLocked = false;
float temperatureC = NAN;
bool homekitPaired = false;
bool wifiConnected = false;
};
extern AirtapState airtap;
Single source of truth matters: Apple Home, buttons, IR, and display all read from airtap.
PWM Fan Control
The existing ESPHome firmware uses LEDC PWM on GPIO2 at 1000 Hz. It also uses a minimum duty cycle for speed 1 so the fan starts reliably. If the fans stall it can burn out the motors. I’m even considering starting the fan at max power for 1 second, similar to box fans and rooms fans which have the power settings go from 0-3-2-1.
#include <Arduino.h>
#include "AirtapPins.h"
#include "AirtapState.h"
static constexpr int FAN_PWM_CHANNEL = 0;
static constexpr int FAN_PWM_FREQ = 1000;
static constexpr int FAN_PWM_RES_BITS = 10;
static constexpr int FAN_PWM_MAX = (1 << FAN_PWM_RES_BITS) - 1;
float fanStepToDuty(int step) {
if (step <= 0) {
return 0.0f;
}
if (step == 1) {
return 0.38f; // minimum-start behavior based on current ESPHome firmware
}
#if defined(AIRTAP_BOARD_4BTN_REV1)
return (step + 4) / 14.0f;
#else
return (step + 3) / 13.0f;
#endif
}
void setupFanPwm() {
ledcSetup(FAN_PWM_CHANNEL, FAN_PWM_FREQ, FAN_PWM_RES_BITS);
ledcAttachPin(PIN_FAN_PWM, FAN_PWM_CHANNEL);
}
void applyFanSpeed() {
if (airtap.fanSpeedStep < 0) {
airtap.fanSpeedStep = 0;
}
if (airtap.fanSpeedStep > 10) {
airtap.fanSpeedStep = 10;
}
float duty = fanStepToDuty(airtap.fanSpeedStep);
int rawDuty = (int)(duty * FAN_PWM_MAX);
ledcWrite(FAN_PWM_CHANNEL, rawDuty);
}
Mapping HomeKit Speed to AirTap Speed
Apple Home uses percentages while AirTap uses steps. Use converters:
int percentToFanStep(int percent) {
if (percent <= 0) {
return 0;
}
int step = (percent + 9) / 10;
if (step < 1) {
step = 1;
}
if (step > 10) {
step = 10;
}
return step;
}
int fanStepToPercent(int step) {
if (step <= 0) {
return 0;
}
if (step > 10) {
step = 10;
}
return step * 10;
}
This keeps behavior predictable and traceable.
Minimal HomeSpan Fan Service
Expose a Fan service in HomeSpan:
#include <HomeSpan.h>
#include "AirtapState.h"
void applyFanSpeed();
int percentToFanStep(int percent);
int fanStepToPercent(int step);
struct AirtapFanService : Service::Fan {
SpanCharacteristic *active;
SpanCharacteristic *rotationSpeed;
AirtapFanService() : Service::Fan() {
active = new Characteristic::Active(0);
rotationSpeed = new Characteristic::RotationSpeed(0);
}
boolean update() override {
if (active->updated()) {
bool on = active->getNewVal();
if (!on) {
airtap.fanSpeedStep = 0;
} else if (airtap.fanSpeedStep == 0) {
airtap.fanSpeedStep = 5;
}
rotationSpeed->setVal(fanStepToPercent(airtap.fanSpeedStep));
applyFanSpeed();
}
if (rotationSpeed->updated()) {
int percent = rotationSpeed->getNewVal();
airtap.fanSpeedStep = percentToFanStep(percent);
active->setVal(airtap.fanSpeedStep > 0 ? 1 : 0);
applyFanSpeed();
}
return true;
}
void syncFromDeviceState() {
active->setVal(airtap.fanSpeedStep > 0 ? 1 : 0);
rotationSpeed->setVal(fanStepToPercent(airtap.fanSpeedStep));
}
};
syncFromDeviceState() is critical: when physical buttons change the fan, Apple Home stays in sync.
Main Firmware Skeleton
A first main.cpp could look like this:
#include <Arduino.h>
#include <HomeSpan.h>
#include "AirtapPins.h"
#include "AirtapState.h"
AirtapState airtap;
AirtapFanService *fanService = nullptr;
void setupFanPwm();
void applyFanSpeed();
void setupButtons();
void pollButtons();
void setupTemperature();
void pollTemperature();
void setupDisplay();
void updateDisplayIfNeeded();
void setup() {
Serial.begin(115200);
delay(1000);
setupFanPwm();
setupButtons();
setupTemperature();
setupDisplay();
homeSpan.setLogLevel(1);
// Development only. Do not ship every device with the same static setup code.
homeSpan.setPairingCode("46637726");
homeSpan.setQRID("ATAP");
homeSpan.begin(Category::Fans, "AirTap Vent");
new SpanAccessory();
new Service::AccessoryInformation();
new Characteristic::Identify();
new Characteristic::Manufacturer("SiloCityLabs");
new Characteristic::Name("AirTap Vent");
new Characteristic::Model("AirTap ESP32 HomeSpan");
new Characteristic::FirmwareRevision("0.1.0");
fanService = new AirtapFanService();
new AirtapTemperatureService();
new AirtapPanelLockoutService();
}
void loop() {
homeSpan.poll();
pollButtons();
pollTemperature();
updateDisplayIfNeeded();
}
This proves the accessory model. Get pairing and PWM working first.
Local Button Handling
Local buttons are non-negotiable. The device should work offline. Follow existing ESPHome logic:
- Mode or Toggle turns the fan on/off
- Up increases fan speed
- Down decreases fan speed
- Buttons do nothing when panel lockout is enabled
A basic structure:
void setFanStepFromLocalInput(int step) {
if (airtap.panelLocked) {
return;
}
if (step < 0) {
step = 0;
}
if (step > 10) {
step = 10;
}
airtap.fanSpeedStep = step;
applyFanSpeed();
if (fanService) {
fanService->syncFromDeviceState();
}
}
void increaseFanFromButton() {
setFanStepFromLocalInput(airtap.fanSpeedStep + 1);
}
void decreaseFanFromButton() {
setFanStepFromLocalInput(airtap.fanSpeedStep - 1);
}
void toggleFanFromButton() {
if (airtap.fanSpeedStep == 0) {
setFanStepFromLocalInput(10);
} else {
setFanStepFromLocalInput(0);
}
}
I’m skipping over debounce code. There are a million debounce tuts in addition tothe proven “setDebounceTime” in Arduino.
Panel Lockout
Panel lockout is a an existing feautre in my ESPHome firmware. Expose a Switch service for lockout:
struct AirtapPanelLockoutService : Service::Switch {
SpanCharacteristic *on;
AirtapPanelLockoutService() : Service::Switch() {
on = new Characteristic::On(false);
}
boolean update() override {
airtap.panelLocked = on->getNewVal();
return true;
}
};
When enabled:
- Physical buttons should not change fan speed.
- IR remote commands should probably not change fan speed.
- Apple Home should still be allowed to control the fan.
This prevents your toddler from changing the settings.
Temperature Sensor
The AirTap firmware reads temperature through the board’s ADC path. ESPHome currently models it with an NTC/resistance pipeline and calibration points around:
3.389 kOhm -> 0°C10.0 kOhm -> 25°C27.219 kOhm -> 50°C
A HomeSpan version can expose that as a native HomeKit Temperature Sensor.
Start with a rough implementation and tune it later. The development goal is first to get a stable, believable temperature value into Apple Home.
float readAirtapTemperatureC() {
int raw = analogRead(PIN_TEMP_ADC);
// Pseudocode:
// 1. Convert ADC raw value to voltage ratio.
// 2. Convert voltage ratio to thermistor resistance.
// 3. Use the existing calibration curve or Steinhart-Hart approximation.
// 4. Return temperature in Celsius.
return NAN;
}
The HomeSpan service:
struct AirtapTemperatureService : Service::TemperatureSensor {
SpanCharacteristic *currentTemperature;
unsigned long lastReadMs = 0;
AirtapTemperatureService() : Service::TemperatureSensor() {
currentTemperature = new Characteristic::CurrentTemperature(20.0);
}
void loop() override {
if (millis() - lastReadMs < 5000) {
return;
}
lastReadMs = millis();
float t = readAirtapTemperatureC();
if (!isnan(t)) {
airtap.temperatureC = t;
currentTemperature->setVal(t);
}
}
};
The Apple Home user gets a temperature tile they can use in scenes and automations.
OLED Display
The existing firmware uses an SSD1306 128x64 OLED over I2C at address 0x3C. HomeSpan does not manage that display. Arduino libraries can display the status:
AirTap HomeKit- Fan speed
0-10 - Temperature
- Panel locked/unlocked
- Wi-Fi or pairing status
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
void setupDisplay() {
Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 display not found");
return;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.display();
}
void updateDisplay() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("AirTap HomeKit");
display.printf("Fan Speed: %d\n", airtap.fanSpeedStep);
if (!isnan(airtap.temperatureC)) {
float f = airtap.temperatureC * 9.0f / 5.0f + 32.0f;
display.printf("Temp: %.1f F\n", f);
} else {
display.println("Temp: --");
}
display.printf("Panel: %s\n", airtap.panelLocked ? "Locked" : "Enabled");
display.display();
}
Since this in a “MVP”, plain text is fine. Show state first, polish later.
IR Receiver Support
The 4-button ESPHome firmware includes IR receiver support on GPIO3 and uses decoded Pronto data for remote buttons such as power, fan, plus, minus, mode, and refresh. This isn’t vital for the MVP and can be added later. Recommended approach:
- Ship first prototype without IR.
- Log IR codes to confirm remote behavior.
- Map IR commands to existing button functions.
- Respect lockout.
IR should call the same input handlers as buttons, so that changes propagate to HomeKit:
// IR plus/minus/power
increaseFanFromButton();
decreaseFanFromButton();
toggleFanFromButton();
Persistence
The device should remember useful state across reboot:
- Last fan speed
- Panel lockout state
- Display preference
- Temperature calibration / offset
HomeSpan already has storage behavior for HomeKit pairing. For device settings, use ESP32 Preferences/NVS.
#include <Preferences.h>
Preferences prefs;
void loadSettings() {
prefs.begin("airtap", true);
airtap.fanSpeedStep = prefs.getInt("fan", 0);
airtap.panelLocked = prefs.getBool("lock", false);
prefs.end();
}
void saveSettings() {
prefs.begin("airtap", false);
prefs.putInt("fan", airtap.fanSpeedStep);
prefs.putBool("lock", airtap.panelLocked);
prefs.end();
}
Avoid frequent writes; save only on state change, and consider a short delay/debounce before writing.
Provisioning and Reset UX
Customers need a clear setup procedure. Not serial monitors nor “magic phrases” in command-line. Consider common steps like:
- entering pairing mode
- setting or resetting wifi credentials
- what does the display show when not paired?
- what button combination triggers a factory reset?
- how does support tell a user to recover a device?
A reasonable flow:
- On first boot, display
Pair AirTap HomeKit. - Show the HomeKit setup code or direct users to the printed/setup label.
- If already paired, show normal fan status.
- Holding two buttons for 10 seconds resets pairing and Wi-Fi.
- Display confirms
Reset Complete.
OTA and Updates
Use manual USB flashing for now. Automatic updates require infrastructure that can wait.
Testing Checklist
Before calling the firmware usable, test these behaviors:
- Device flashes successfully.
- Serial logs show boot and HomeSpan startup.
- Apple Home can pair the device.
- Apple Home can remove and re-pair the device.
- Fan turns off at
0%. - Fan starts reliably at the first non-zero speed.
- Fan reaches a strong output at
100%. - Physical Up increases the speed.
- Physical Down decreases the speed.
- Physical Toggle/Mode turns the fan on and off.
- Apple Home updates when physical buttons are pressed.
- OLED display matches actual state.
- Panel lockout blocks physical buttons.
- Panel lockout does not block Apple Home control.
- Temperature does not jump wildly.
- Reboot restores expected state.
- Removing power and restoring power does not corrupt pairing.
- Factory reset works without a computer.
- Multiple vents pair with unique names and can be renamed in Apple Home.
Market Positioning
Home Assistant users search for ESPHome + AirTap. Apple users search for HomeKit + AirTap. This post opens a second market: developers and Apple-first smart-home users.
Next Steps for Developers
If you’re interested in Apple Home, HomeKit, ESP32, or smart HVAC, the AirTap ESP32 hardware is a strong foundation. It provides fan PWM, temperature sensing, display, buttons, and proven mechanics. The next step is firmware.
What a First Release Could Include
Ship what works:
platformio.inifor one board targetmain.cppwith PWM fan control- Pin mapping, fan service, lockout service
- README with flashing instructions
- Pairing/reset instructions (for users who receive a completed device)
- Known limitations clearly stated
Be direct about status:
This is experimental HomeSpan firmware for the SiloCityLabs AirTap ESP32 upgrade kit.
Current status:
- Apple Home pairing: working
- Fan speed control: working
- Local buttons: working
- Temperature sensor: experimental
- OLED display: experimental
- IR remote: not implemented
- OTA updates: not implemented
Use ESPHome firmware for production Home Assistant installs.
Use this firmware if you want to test native Apple Home support.
And finally for the repo name: esp32-airtap-homespan- descriptive and searchable.
Summary
The AirTap ESP32 hardware is proven for Home Assistant via ESPHome. HomeSpan offers a parallel path for native Apple Home support.
The best first firmware can be small:
- Pair with Apple Home
- Expose a Fan service
- Control the AirTap fan through PWM
- Keep local buttons working
- Add temperature, display, lockout, persistence, and IR after the core works
This expands the market rather than replacing ESPHome. Developers building for Apple Home now have an entry point.