Compare commits

..

1 Commits

Author SHA1 Message Date
Gunjan Jaswal f1b2b67b57 Fix HomeKit thermostat sending lowercased fan_mode to climate (#175180) 2026-06-30 18:01:03 -05:00
3 changed files with 82 additions and 16 deletions
@@ -399,13 +399,15 @@ class Thermostat(HomeAccessory):
def _set_fan_speed(self, speed: int) -> None:
_LOGGER.debug("%s: Set fan speed to %s", self.entity_id, speed)
mode = percentage_to_ordered_list_item(self.ordered_fan_speeds, speed - 1)
speed_key = percentage_to_ordered_list_item(self.ordered_fan_speeds, speed - 1)
mode = self.fan_modes[speed_key]
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode}
self.async_call_service(CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, params)
def _get_on_mode(self) -> str:
if self.ordered_fan_speeds:
return percentage_to_ordered_list_item(self.ordered_fan_speeds, 50)
speed_key = percentage_to_ordered_list_item(self.ordered_fan_speeds, 50)
return self.fan_modes[speed_key]
return self.fan_modes[FAN_ON]
def _set_fan_active(self, active: int) -> None:
+6 -14
View File
@@ -229,17 +229,6 @@ class TodoItem:
"""The date and time that a to-do item was marked completed."""
def _serialize_todo_item(item: TodoItem) -> dict[str, Any]:
"""Serialize a To-do item for websocket subscribers.
Avoids dataclasses.asdict(), which recursively deepcopies every field value
(including the status StrEnum via __deepcopy__) on every subscriber update.
TodoItem is a flat dataclass of immutable values, so a shallow dict is
equivalent and far cheaper.
"""
return {field.name: getattr(item, field.name) for field in dataclasses.fields(item)}
CACHED_PROPERTIES_WITH_ATTR_ = {
"todo_items",
}
@@ -312,8 +301,11 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if not self._update_listeners:
return
items = self.todo_items
todo_items = [copy.copy(item) for item in items] if items is not None else None
todo_items = (
[copy.copy(item) for item in self.todo_items]
if self.todo_items is not None
else None
)
for listener in self._update_listeners:
listener(todo_items)
@@ -350,7 +342,7 @@ async def websocket_handle_subscribe_todo_items(
@callback
def todo_item_listener(todo_items: list[TodoItem] | None) -> None:
"""Push updated To-do list items to websocket."""
items = [_serialize_todo_item(item) for item in todo_items or []]
items = [dataclasses.asdict(item) for item in todo_items or []]
connection.send_message(
websocket_api.event_message(
msg["id"],
@@ -2896,3 +2896,75 @@ async def test_thermostat_reversed_min_max(hass: HomeAssistant, hk_driver) -> No
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
async def test_thermostat_with_capitalized_fan_modes(
hass: HomeAssistant, hk_driver
) -> None:
"""Test fan modes are sent back with their original casing."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVACMode.OFF,
{
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE,
ATTR_FAN_MODES: ["Auto", "Low", "Medium", "High"],
ATTR_HVAC_ACTION: HVACAction.IDLE,
ATTR_FAN_MODE: "Auto",
ATTR_HVAC_MODES: [
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.OFF,
HVACMode.AUTO,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
acc.run()
await hass.async_block_till_done()
# ordered_fan_speeds tracks the lowercased keys, but the value sent back to
# the climate entity must use the entity's original casing.
assert acc.ordered_fan_speeds == [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
call_set_fan_mode = async_mock_service(hass, CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE)
char_rotation_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_rotation_speed_iid,
HAP_REPR_VALUE: 100,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 1
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == "High"
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_rotation_speed_iid,
HAP_REPR_VALUE: 100 / 3,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 2
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == "Low"