Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 159 additions & 11 deletions components/homekit/fan.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,168 @@ namespace esphome
private:
static constexpr const char* TAG = "FanEntity";
fan::Fan* fanPtr;

/**
* @brief Handle incoming HomeKit write requests for a Fan service and apply them to the associated ESPHome fan.
*
* Processes writes for On, Rotation Speed, and Swing Mode characteristics, updates the HomeKit characteristic values and per-write statuses, batches any requested changes into a single fan call, and performs that call.
*
* @param write_data Array of write operations provided by HomeKit.
* @param count Number of entries in `write_data`.
* @param serv_priv Pointer to the associated fan::Fan instance (stored as service private data).
* @param write_priv Unused write-private context supplied by HAP.
* @return int HAP_SUCCESS.
*/
static int fanwrite(hap_write_data_t write_data[], int count, void* serv_priv, void* write_priv) {
fan::Fan* fanPtr = (fan::Fan*)serv_priv;
ESP_LOGD(TAG, "Write called for Accessory %s (%s)", std::to_string(fanPtr->get_object_id_hash()).c_str(), fanPtr->get_name().c_str());
int i, ret = HAP_SUCCESS;
hap_write_data_t* write;
auto call = fanPtr->make_call();
bool update_speed = false;
bool update_state = false;
bool update_oscillating = false;

for (i = 0; i < count; i++) {
write = &write_data[i];
if (!strcmp(hap_char_get_type_uuid(write->hc), HAP_CHAR_UUID_ON)) {
ESP_LOGD(TAG, "Received Write for fan '%s' -> %s", fanPtr->get_name().c_str(), write->val.b ? "On" : "Off");
ESP_LOGD(TAG, "[STATE] CURRENT STATE: %d", fanPtr->state);
fanPtr->make_call().set_state(write->val.b).perform();
call.set_state(write->val.b);
update_state = true;
hap_char_update_val(write->hc, &(write->val));
*(write->status) = HAP_STATUS_SUCCESS;
}
else if (!strcmp(hap_char_get_type_uuid(write->hc), HAP_CHAR_UUID_ROTATION_SPEED)) {
// Only process speed writes if the fan supports speed control
if (fanPtr->get_traits().supports_speed()) {
float speed_percentage = write->val.f;
ESP_LOGD(TAG, "Received Write for fan '%s' speed -> %.1f%%", fanPtr->get_name().c_str(), speed_percentage);

// Direct mapping: HomeKit percentage (0-100) to ESPHome speed (0-100)
int speed_level = static_cast<int>(speed_percentage);

ESP_LOGD(TAG, "Setting fan speed to level: %d", speed_level);
call.set_speed(speed_level);
update_speed = true;
hap_char_update_val(write->hc, &(write->val));
*(write->status) = HAP_STATUS_SUCCESS;
} else {
*(write->status) = HAP_STATUS_RES_ABSENT;
}
}
else if (!strcmp(hap_char_get_type_uuid(write->hc), HAP_CHAR_UUID_SWING_MODE)) {
// Only process oscillation writes if the fan supports oscillation
if (fanPtr->get_traits().supports_oscillation()) {
bool swing_mode = write->val.i;
ESP_LOGD(TAG, "Received Write for fan '%s' oscillation -> %s", fanPtr->get_name().c_str(), swing_mode ? "On" : "Off");

call.set_oscillating(swing_mode);
update_oscillating = true;
hap_char_update_val(write->hc, &(write->val));
*(write->status) = HAP_STATUS_SUCCESS;
} else {
*(write->status) = HAP_STATUS_RES_ABSENT;
}
}
else {
*(write->status) = HAP_STATUS_RES_ABSENT;
}
}

if (update_state || update_speed || update_oscillating) {
call.perform();
}
return ret;
}

/**
* @brief Reflects an ESPHome fan's current state into its HomeKit Fan service characteristics.
*
* Updates the accessory's On, Rotation Speed, and Swing Mode characteristics (if present)
* to match the fan's current on/off state, speed (0–100 mapped directly to HomeKit percentage),
* and oscillating state (1 = enabled, 0 = disabled). If the accessory, service, or any
* characteristic is not found, the function simply returns without error.
*
* @param obj Pointer to the fan whose state will be propagated to HomeKit.
*/
static void on_fanupdate(fan::Fan* obj) {
ESP_LOGD(TAG, "%s state: %s", obj->get_name().c_str(), ONOFF(obj->state));
ESP_LOGD(TAG, "%s state: %s, speed: %d, oscillating: %s",
obj->get_name().c_str(),
ONOFF(obj->state),
obj->speed,
obj->oscillating ? "On" : "Off");

hap_acc_t* acc = hap_acc_get_by_aid(hap_get_unique_aid(std::to_string(obj->get_object_id_hash()).c_str()));
if (acc) {
hap_serv_t* hs = hap_acc_get_serv_by_uuid(acc, HAP_SERV_UUID_FAN);
hap_char_t* on_char = hap_serv_get_char_by_uuid(hs, HAP_CHAR_UUID_ON);
hap_val_t state;
state.b = !!obj->state;
hap_char_update_val(on_char, &state);
if (hs) {
// Update On characteristic
hap_char_t* on_char = hap_serv_get_char_by_uuid(hs, HAP_CHAR_UUID_ON);
if (on_char) {
hap_val_t state;
state.b = !!obj->state;
hap_char_update_val(on_char, &state);
}

// Update Rotation Speed characteristic only if the fan supports speed
if (obj->get_traits().supports_speed()) {
hap_char_t* speed_char = hap_serv_get_char_by_uuid(hs, HAP_CHAR_UUID_ROTATION_SPEED);
if (speed_char) {
hap_val_t speed_val;
// Direct mapping: ESPHome speed (0-100) to HomeKit percentage (0-100)
speed_val.f = static_cast<float>(obj->speed);
hap_char_update_val(speed_char, &speed_val);
}
}

// Update Swing Mode characteristic only if the fan supports oscillation
if (obj->get_traits().supports_oscillation()) {
hap_char_t* swing_char = hap_serv_get_char_by_uuid(hs, HAP_CHAR_UUID_SWING_MODE);
if (swing_char) {
hap_val_t swing_val;
swing_val.i = obj->oscillating ? 1 : 0; // 1 = enabled, 0 = disabled
hap_char_update_val(swing_char, &swing_val);
}
}
}
}
}

/**
* @brief Handle an identify request for the accessory.
*
* Logs that the accessory was identified.
*
* @param ha Pointer to the HomeKit accessory being identified.
* @return int `HAP_SUCCESS` on success.
*/
static int acc_identify(hap_acc_t* ha) {
ESP_LOGI(TAG, "Accessory identified");
return HAP_SUCCESS;
}

public:
FanEntity(fan::Fan* fanPtr) : HAPEntity({{MODEL, "HAP-FAN"}}), fanPtr(fanPtr) {}
/**
* @brief Construct a HomeKit Fan entity for an ESPHome fan.
*
* Initializes a HAP fan accessory wrapper using the model identifier "HAP-FAN"
* and associates it with the provided ESPHome fan instance.
*
* @param fanPtr Pointer to the ESPHome `fan::Fan` instance to expose to HomeKit.
* The pointer must remain valid for the lifetime of the FanEntity.
*/
FanEntity(fan::Fan* fanPtr) : HAPEntity({{MODEL, "HAP-FAN"}}), fanPtr(fanPtr) {}

/**
* @brief Create and register a HomeKit accessory and Fan service for the ESPHome fan.
*
* Creates a HomeKit accessory using the accessory_info values (or sensible defaults),
* exposes a Fan service with On, Rotation Speed, and Swing Mode characteristics
* initialized from the underlying fan state, attaches a write callback so HomeKit
* writes are applied to the fan, registers the accessory with the bridged HomeKit
* database (unique AID derived from the fan object hash), and subscribes to fan
* state updates so changes are reflected back into HomeKit.
*/
void setup() {
hap_acc_cfg_t acc_cfg = {
.model = strdup(accessory_info[MODEL]),
Expand All @@ -66,23 +191,41 @@ namespace esphome
hap_acc_t* accessory = nullptr;
hap_serv_t* service = nullptr;
std::string accessory_name = fanPtr->get_name();

if (accessory_info[NAME] == NULL) {
acc_cfg.name = strdup(accessory_name.c_str());
}
else {
acc_cfg.name = strdup(accessory_info[NAME]);
}

if (accessory_info[SN] == NULL) {
acc_cfg.serial_num = strdup(std::to_string(fanPtr->get_object_id_hash()).c_str());
}
else {
acc_cfg.serial_num = strdup(accessory_info[SN]);
}

/* Create accessory object */
accessory = hap_acc_create(&acc_cfg);
/* Create the fan Service. */

/* Create the fan Service with initial state */
service = hap_serv_fan_create(fanPtr->state);

// Add Rotation Speed characteristic only if the fan supports speed control
if (fanPtr->get_traits().supports_speed()) {
hap_char_t* speed_char = hap_char_rotation_speed_create(static_cast<float>(fanPtr->speed));
hap_serv_add_char(service, speed_char);
ESP_LOGD(TAG, "Added speed control to HomeKit fan");
}

// Add Swing Mode characteristic only if the fan supports oscillation
if (fanPtr->get_traits().supports_oscillation()) {
hap_char_t* swing_char = hap_char_swing_mode_create(fanPtr->oscillating ? 1 : 0);
hap_serv_add_char(service, swing_char);
ESP_LOGD(TAG, "Added oscillation control to HomeKit fan");
}

ESP_LOGD(TAG, "ID HASH: %lu", fanPtr->get_object_id_hash());
hap_serv_set_priv(service, fanPtr);

Expand All @@ -94,11 +237,16 @@ namespace esphome

/* Add the Accessory to the HomeKit Database */
hap_add_bridged_accessory(accessory, hap_get_unique_aid(std::to_string(fanPtr->get_object_id_hash()).c_str()));

if (!fanPtr->is_internal())
fanPtr->add_on_state_callback([this]() { FanEntity::on_fanupdate(fanPtr); });
ESP_LOGI(TAG, "Fan '%s' linked to HomeKit", accessory_name.c_str());

ESP_LOGI(TAG, "Fan '%s' linked to HomeKit%s%s",
accessory_name.c_str(),
fanPtr->get_traits().supports_speed() ? " with speed control" : "",
fanPtr->get_traits().supports_oscillation() ? " with oscillation control" : "");
}
};
}
}
#endif
#endif