@@ -16,43 +16,154 @@ namespace esphome
1616 private:
1717 static constexpr const char * TAG = " FanEntity" ;
1818 fan::Fan* fanPtr;
19+
20+ /* *
21+ * @brief Handle incoming HomeKit write requests for a Fan service and apply them to the associated ESPHome fan.
22+ *
23+ * 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.
24+ *
25+ * @param write_data Array of write operations provided by HomeKit.
26+ * @param count Number of entries in `write_data`.
27+ * @param serv_priv Pointer to the associated fan::Fan instance (stored as service private data).
28+ * @param write_priv Unused write-private context supplied by HAP.
29+ * @return int HAP_SUCCESS.
30+ */
1931 static int fanwrite (hap_write_data_t write_data[], int count, void * serv_priv, void * write_priv) {
2032 fan::Fan* fanPtr = (fan::Fan*)serv_priv;
2133 ESP_LOGD (TAG, " Write called for Accessory %s (%s)" , std::to_string (fanPtr->get_object_id_hash ()).c_str (), fanPtr->get_name ().c_str ());
2234 int i, ret = HAP_SUCCESS;
2335 hap_write_data_t * write;
36+ auto call = fanPtr->make_call ();
37+ bool update_speed = false ;
38+ bool update_state = false ;
39+ bool update_oscillating = false ;
40+
2441 for (i = 0 ; i < count; i++) {
2542 write = &write_data[i];
2643 if (!strcmp (hap_char_get_type_uuid (write->hc ), HAP_CHAR_UUID_ON)) {
2744 ESP_LOGD (TAG, " Received Write for fan '%s' -> %s" , fanPtr->get_name ().c_str (), write->val .b ? " On" : " Off" );
28- ESP_LOGD (TAG, " [STATE] CURRENT STATE: %d" , fanPtr->state );
29- fanPtr->make_call ().set_state (write->val .b ).perform ();
45+ call.set_state (write->val .b );
46+ update_state = true ;
47+ hap_char_update_val (write->hc , &(write->val ));
48+ *(write->status ) = HAP_STATUS_SUCCESS;
49+ }
50+ else if (!strcmp (hap_char_get_type_uuid (write->hc ), HAP_CHAR_UUID_ROTATION_SPEED)) {
51+ float speed_percentage = write->val .f ;
52+ ESP_LOGD (TAG, " Received Write for fan '%s' speed -> %.1f%%" , fanPtr->get_name ().c_str (), speed_percentage);
53+
54+ // Direct mapping: HomeKit percentage (0-100) to ESPHome speed (0-100)
55+ int speed_level = static_cast <int >(speed_percentage);
56+
57+ ESP_LOGD (TAG, " Setting fan speed to level: %d" , speed_level);
58+ call.set_speed (speed_level);
59+ update_speed = true ;
60+ hap_char_update_val (write->hc , &(write->val ));
61+ *(write->status ) = HAP_STATUS_SUCCESS;
62+ }
63+ else if (!strcmp (hap_char_get_type_uuid (write->hc ), HAP_CHAR_UUID_SWING_MODE)) {
64+ bool swing_mode = write->val .i ;
65+ ESP_LOGD (TAG, " Received Write for fan '%s' oscillation -> %s" , fanPtr->get_name ().c_str (), swing_mode ? " On" : " Off" );
66+
67+ call.set_oscillating (swing_mode);
68+ update_oscillating = true ;
3069 hap_char_update_val (write->hc , &(write->val ));
3170 *(write->status ) = HAP_STATUS_SUCCESS;
3271 }
3372 else {
3473 *(write->status ) = HAP_STATUS_RES_ABSENT;
3574 }
3675 }
76+
77+ if (update_state || update_speed || update_oscillating) {
78+ call.perform ();
79+ }
3780 return ret;
3881 }
82+
83+ /* *
84+ * @brief Reflects an ESPHome fan's current state into its HomeKit Fan service characteristics.
85+ *
86+ * Updates the accessory's On, Rotation Speed, and Swing Mode characteristics (if present)
87+ * to match the fan's current on/off state, speed (0–100 mapped directly to HomeKit percentage),
88+ * and oscillating state (1 = enabled, 0 = disabled). If the accessory, service, or any
89+ * characteristic is not found, the function simply returns without error.
90+ *
91+ * @param obj Pointer to the fan whose state will be propagated to HomeKit.
92+ */
3993 static void on_fanupdate (fan::Fan* obj) {
40- ESP_LOGD (TAG, " %s state: %s" , obj->get_name ().c_str (), ONOFF (obj->state ));
94+ ESP_LOGD (TAG, " %s state: %s, speed: %d, oscillating: %s" ,
95+ obj->get_name ().c_str (),
96+ ONOFF (obj->state ),
97+ obj->speed ,
98+ obj->oscillating ? " On" : " Off" );
99+
41100 hap_acc_t * acc = hap_acc_get_by_aid (hap_get_unique_aid (std::to_string (obj->get_object_id_hash ()).c_str ()));
42101 if (acc) {
43102 hap_serv_t * hs = hap_acc_get_serv_by_uuid (acc, HAP_SERV_UUID_FAN);
44- hap_char_t * on_char = hap_serv_get_char_by_uuid (hs, HAP_CHAR_UUID_ON);
45- hap_val_t state;
46- state.b = !!obj->state ;
47- hap_char_update_val (on_char, &state);
103+ if (hs) {
104+ // Update On characteristic
105+ hap_char_t * on_char = hap_serv_get_char_by_uuid (hs, HAP_CHAR_UUID_ON);
106+ if (on_char) {
107+ hap_val_t state;
108+ state.b = !!obj->state ;
109+ hap_char_update_val (on_char, &state);
110+ }
111+
112+ // Update Rotation Speed characteristic
113+ hap_char_t * speed_char = hap_serv_get_char_by_uuid (hs, HAP_CHAR_UUID_ROTATION_SPEED);
114+ if (speed_char) {
115+ hap_val_t speed_val;
116+ // Direct mapping: ESPHome speed (0-100) to HomeKit percentage (0-100)
117+ speed_val.f = static_cast <float >(obj->speed );
118+ hap_char_update_val (speed_char, &speed_val);
119+ }
120+
121+ // Update Swing Mode characteristic
122+ hap_char_t * swing_char = hap_serv_get_char_by_uuid (hs, HAP_CHAR_UUID_SWING_MODE);
123+ if (swing_char) {
124+ hap_val_t swing_val;
125+ swing_val.i = obj->oscillating ? 1 : 0 ; // 1 = enabled, 0 = disabled
126+ hap_char_update_val (swing_char, &swing_val);
127+ }
128+ }
48129 }
49130 }
131+
132+ /* *
133+ * @brief Handle an identify request for the accessory.
134+ *
135+ * Logs that the accessory was identified.
136+ *
137+ * @param ha Pointer to the HomeKit accessory being identified.
138+ * @return int `HAP_SUCCESS` on success.
139+ */
50140 static int acc_identify (hap_acc_t * ha) {
51141 ESP_LOGI (TAG, " Accessory identified" );
52142 return HAP_SUCCESS;
53143 }
144+
54145 public:
55- FanEntity (fan::Fan* fanPtr) : HAPEntity({{MODEL, " HAP-FAN" }}), fanPtr(fanPtr) {}
146+ /* *
147+ * @brief Construct a HomeKit Fan entity for an ESPHome fan.
148+ *
149+ * Initializes a HAP fan accessory wrapper using the model identifier "HAP-FAN"
150+ * and associates it with the provided ESPHome fan instance.
151+ *
152+ * @param fanPtr Pointer to the ESPHome `fan::Fan` instance to expose to HomeKit.
153+ * The pointer must remain valid for the lifetime of the FanEntity.
154+ */
155+ FanEntity (fan::Fan* fanPtr) : HAPEntity({{MODEL, " HAP-FAN" }}), fanPtr(fanPtr) {}
156+
157+ /* *
158+ * @brief Create and register a HomeKit accessory and Fan service for the ESPHome fan.
159+ *
160+ * Creates a HomeKit accessory using the accessory_info values (or sensible defaults),
161+ * exposes a Fan service with On, Rotation Speed, and Swing Mode characteristics
162+ * initialized from the underlying fan state, attaches a write callback so HomeKit
163+ * writes are applied to the fan, registers the accessory with the bridged HomeKit
164+ * database (unique AID derived from the fan object hash), and subscribes to fan
165+ * state updates so changes are reflected back into HomeKit.
166+ */
56167 void setup () {
57168 hap_acc_cfg_t acc_cfg = {
58169 .model = strdup (accessory_info[MODEL]),
@@ -66,23 +177,35 @@ namespace esphome
66177 hap_acc_t * accessory = nullptr ;
67178 hap_serv_t * service = nullptr ;
68179 std::string accessory_name = fanPtr->get_name ();
180+
69181 if (accessory_info[NAME] == NULL ) {
70182 acc_cfg.name = strdup (accessory_name.c_str ());
71183 }
72184 else {
73185 acc_cfg.name = strdup (accessory_info[NAME]);
74186 }
187+
75188 if (accessory_info[SN] == NULL ) {
76189 acc_cfg.serial_num = strdup (std::to_string (fanPtr->get_object_id_hash ()).c_str ());
77190 }
78191 else {
79192 acc_cfg.serial_num = strdup (accessory_info[SN]);
80193 }
194+
81195 /* Create accessory object */
82196 accessory = hap_acc_create (&acc_cfg);
83- /* Create the fan Service. */
197+
198+ /* Create the fan Service with initial state */
84199 service = hap_serv_fan_create (fanPtr->state );
85200
201+ // Add Rotation Speed characteristic
202+ hap_char_t * speed_char = hap_char_rotation_speed_create (static_cast <float >(fanPtr->speed ));
203+ hap_serv_add_char (service, speed_char);
204+
205+ // Add Swing Mode characteristic
206+ hap_char_t * swing_char = hap_char_swing_mode_create (fanPtr->oscillating ? 1 : 0 );
207+ hap_serv_add_char (service, swing_char);
208+
86209 ESP_LOGD (TAG, " ID HASH: %lu" , fanPtr->get_object_id_hash ());
87210 hap_serv_set_priv (service, fanPtr);
88211
@@ -94,9 +217,11 @@ namespace esphome
94217
95218 /* Add the Accessory to the HomeKit Database */
96219 hap_add_bridged_accessory (accessory, hap_get_unique_aid (std::to_string (fanPtr->get_object_id_hash ()).c_str ()));
220+
97221 if (!fanPtr->is_internal ())
98222 fanPtr->add_on_state_callback ([this ]() { FanEntity::on_fanupdate (fanPtr); });
99- ESP_LOGI (TAG, " Fan '%s' linked to HomeKit" , accessory_name.c_str ());
223+
224+ ESP_LOGI (TAG, " Fan '%s' linked to HomeKit with speed and oscillation control" , accessory_name.c_str ());
100225 }
101226 };
102227 }
0 commit comments