From dd47f7dac06c2f00ef11680a3e70dbeb6120bfd2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:17:10 +0200 Subject: [PATCH] Use platform entity attribute enums for reads in HomeKit (#180016) --- .../components/homekit/climate_base.py | 32 ++++---- .../components/homekit/climate_util.py | 25 ++++--- .../components/homekit/type_air_purifiers.py | 7 +- .../components/homekit/type_covers.py | 11 ++- homeassistant/components/homekit/type_fans.py | 28 ++++--- .../components/homekit/type_heater_coolers.py | 54 +++++++------ .../components/homekit/type_humidifiers.py | 31 +++++--- .../components/homekit/type_lights.py | 42 ++++++----- .../components/homekit/type_media_players.py | 19 +++-- .../components/homekit/type_remotes.py | 7 +- .../components/homekit/type_switches.py | 5 +- .../components/homekit/type_thermostats.py | 75 +++++++++++-------- 12 files changed, 200 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/homekit/climate_base.py b/homeassistant/components/homekit/climate_base.py index cb985545b165..1ac05bd4c0c3 100644 --- a/homeassistant/components/homekit/climate_base.py +++ b/homeassistant/components/homekit/climate_base.py @@ -9,15 +9,8 @@ from pyhap.const import CATEGORY_THERMOSTAT from pyhap.service import Service from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, - ATTR_FAN_MODES, - ATTR_HVAC_ACTION, - ATTR_HVAC_MODES, - ATTR_MAX_TEMP, - ATTR_MIN_TEMP, ATTR_SWING_MODE, - ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, @@ -29,7 +22,9 @@ from homeassistant.components.climate import ( SERVICE_SET_FAN_MODE, SERVICE_SET_SWING_MODE, SWING_OFF, + ClimateEntityCapabilityAttribute, ClimateEntityFeature, + ClimateEntityStateAttribute, HVACAction, HVACMode, ) @@ -141,11 +136,11 @@ class HomeKitClimateAccessory(HomeAccessory): # reload the accessory when any of them change. self._reload_on_change_attrs.extend( ( - ATTR_MIN_TEMP, - ATTR_MAX_TEMP, - ATTR_FAN_MODES, - ATTR_SWING_MODES, - ATTR_HVAC_MODES, + ClimateEntityCapabilityAttribute.MIN_TEMP, + ClimateEntityCapabilityAttribute.MAX_TEMP, + ClimateEntityCapabilityAttribute.FAN_MODES, + ClimateEntityCapabilityAttribute.SWING_MODES, + ClimateEntityCapabilityAttribute.HVAC_MODES, ) ) @@ -204,7 +199,9 @@ class HomeKitClimateAccessory(HomeAccessory): def _update_current_temperature_char(self, state: State) -> None: """Update the current temperature characteristic from the entity state.""" self._update_temperature_char( - self.char_current_temp, state, ATTR_CURRENT_TEMPERATURE + self.char_current_temp, + state, + ClimateEntityStateAttribute.CURRENT_TEMPERATURE, ) def _dual_setpoint_params( @@ -275,7 +272,8 @@ class HomeKitClimateAccessory(HomeAccessory): self.ordered_fan_speeds and ( speed := fan_mode_to_speed( - self.ordered_fan_speeds, attributes.get(ATTR_FAN_MODE) + self.ordered_fan_speeds, + attributes.get(ClimateEntityStateAttribute.FAN_MODE), ) ) is not None @@ -286,7 +284,7 @@ class HomeKitClimateAccessory(HomeAccessory): """Update the swing characteristic from the current swing mode.""" # An absent swing mode keeps the last value; there is nothing to show. if self.swing_on_mode is not None and ( - swing_mode := attributes.get(ATTR_SWING_MODE) + swing_mode := attributes.get(ClimateEntityStateAttribute.SWING_MODE) ): self.char_swing.set_value(1 if is_swing_on(swing_mode) else 0) @@ -363,13 +361,13 @@ class HomeKitClimateAccessory(HomeAccessory): self._update_swing_char(attributes) self._update_fan_speed_char(attributes) - fan_mode = attributes.get(ATTR_FAN_MODE) + fan_mode = attributes.get(ClimateEntityStateAttribute.FAN_MODE) fan_mode_lower = fan_mode.lower() if isinstance(fan_mode, str) else None if CHAR_TARGET_FAN_STATE in self.fan_chars: self.char_target_fan_state.set_value(1 if fan_mode_lower == FAN_AUTO else 0) if CHAR_CURRENT_FAN_STATE in self.fan_chars and ( - hvac_action := attributes.get(ATTR_HVAC_ACTION) + hvac_action := attributes.get(ClimateEntityStateAttribute.HVAC_ACTION) ): self.char_current_fan_state.set_value( HC_HASS_TO_HOMEKIT_FAN_STATE[hvac_action] diff --git a/homeassistant/components/homekit/climate_util.py b/homeassistant/components/homekit/climate_util.py index 92a5bfce06b8..aa4e8f46f8d9 100644 --- a/homeassistant/components/homekit/climate_util.py +++ b/homeassistant/components/homekit/climate_util.py @@ -5,10 +5,6 @@ import math from typing import Any from homeassistant.components.climate import ( - ATTR_FAN_MODES, - ATTR_MAX_TEMP, - ATTR_MIN_TEMP, - ATTR_SWING_MODES, FAN_HIGH, FAN_LOW, FAN_MEDIUM, @@ -18,6 +14,7 @@ from homeassistant.components.climate import ( SWING_OFF, SWING_ON, SWING_VERTICAL, + ClimateEntityCapabilityAttribute, ) from homeassistant.core import State from homeassistant.util.percentage import ( @@ -51,7 +48,9 @@ def get_fan_modes_and_speeds( exposes, in HomeKit rotation-speed order; it is empty when the entity only advertises custom fan mode names. """ - fan_modes = _lower_to_original(attributes.get(ATTR_FAN_MODES) or []) + fan_modes = _lower_to_original( + attributes.get(ClimateEntityCapabilityAttribute.FAN_MODES) or [] + ) ordered_fan_speeds: list[str] = [] if PRE_DEFINED_FAN_MODES.intersection(fan_modes): ordered_fan_speeds = [ @@ -67,7 +66,9 @@ def get_swing_on_mode(attributes: dict[str, Any]) -> str | None: returned so it can be sent back to the service. Returns ``None`` when the entity exposes no predefined swing modes. """ - if not (swing_modes := attributes.get(ATTR_SWING_MODES)): + if not ( + swing_modes := attributes.get(ClimateEntityCapabilityAttribute.SWING_MODES) + ): return None lower_to_original = _lower_to_original(swing_modes) return next( @@ -82,13 +83,13 @@ def get_swing_on_mode(attributes: dict[str, Any]) -> str | None: def get_swing_off_mode(attributes: dict[str, Any]) -> str: """Return the entity's off swing mode, preserving its original casing.""" - swing_modes = attributes.get(ATTR_SWING_MODES) or [] + swing_modes = attributes.get(ClimateEntityCapabilityAttribute.SWING_MODES) or [] return _lower_to_original(swing_modes).get(SWING_OFF, SWING_OFF) def has_swing_off_mode(attributes: dict[str, Any]) -> bool: """Return whether the entity advertises a swing off mode.""" - swing_modes = attributes.get(ATTR_SWING_MODES) or [] + swing_modes = attributes.get(ClimateEntityCapabilityAttribute.SWING_MODES) or [] return SWING_OFF in _lower_to_original(swing_modes) @@ -131,12 +132,16 @@ def get_temperature_range_from_state( defaults are already Celsius and used as-is. The minimum is clamped to zero because the Home app crashes on negative bounds. """ - if (min_temp := state.attributes.get(ATTR_MIN_TEMP)) is not None: + if ( + min_temp := state.attributes.get(ClimateEntityCapabilityAttribute.MIN_TEMP) + ) is not None: min_temp = temperature_to_homekit(min_temp, unit) else: min_temp = default_min - if (max_temp := state.attributes.get(ATTR_MAX_TEMP)) is not None: + if ( + max_temp := state.attributes.get(ClimateEntityCapabilityAttribute.MAX_TEMP) + ) is not None: max_temp = temperature_to_homekit(max_temp, unit) else: max_temp = default_max diff --git a/homeassistant/components/homekit/type_air_purifiers.py b/homeassistant/components/homekit/type_air_purifiers.py index bbeb63f0056b..5e5ffc514ae6 100644 --- a/homeassistant/components/homekit/type_air_purifiers.py +++ b/homeassistant/components/homekit/type_air_purifiers.py @@ -8,6 +8,7 @@ from pyhap.const import CATEGORY_AIR_PURIFIER from pyhap.service import Service from pyhap.util import callback as pyhap_callback +from homeassistant.components.fan import FanEntityStateAttribute from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, @@ -48,7 +49,7 @@ from .const import ( SERV_TEMPERATURE_SENSOR, THRESHOLD_FILTER_CHANGE_NEEDED, ) -from .type_fans import ATTR_PRESET_MODE, CHAR_ROTATION_SPEED, Fan +from .type_fans import CHAR_ROTATION_SPEED, Fan from .util import ( cleanup_name_for_homekit, convert_to_float, @@ -468,8 +469,8 @@ class AirPurifier(Fan): # Automatic mode is represented in HASS by a preset called Auto or auto attributes = new_state.attributes - if ATTR_PRESET_MODE in attributes: - current_preset_mode = attributes.get(ATTR_PRESET_MODE) + if FanEntityStateAttribute.PRESET_MODE in attributes: + current_preset_mode = attributes.get(FanEntityStateAttribute.PRESET_MODE) self.char_target_air_purifier_state.set_value( TARGET_STATE_AUTO if current_preset_mode and current_preset_mode.lower() == "auto" diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 74da92d988ae..b1a41e395169 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -13,12 +13,11 @@ from pyhap.service import Service from pyhap.util import callback as pyhap_callback from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, - ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, CoverEntityFeature, + CoverEntityStateAttribute, CoverState, ) from homeassistant.const import ( @@ -273,7 +272,9 @@ class OpeningDeviceBase(HomeAccessory): # update tilt if not self._supports_tilt: return - current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + current_tilt = new_state.attributes.get( + CoverEntityStateAttribute.CURRENT_TILT_POSITION + ) if not isinstance(current_tilt, (float, int)): return # HomeKit sends values between -90 and 90. @@ -332,7 +333,9 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): @override def async_update_state(self, new_state: State) -> None: """Update cover position and tilt after state changed.""" - current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) + current_position = new_state.attributes.get( + CoverEntityStateAttribute.CURRENT_POSITION + ) if isinstance(current_position, (float, int)): current_position = int(current_position) self.char_current_position.set_value(current_position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 88ca72826772..a1b900c011b7 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -10,9 +10,7 @@ from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_PERCENTAGE, - ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, - ATTR_PRESET_MODES, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, @@ -20,7 +18,9 @@ from homeassistant.components.fan import ( SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, + FanEntityCapabilityAttribute, FanEntityFeature, + FanEntityStateAttribute, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -66,14 +66,18 @@ class Fan(HomeAccessory): assert state self._reload_on_change_attrs.extend( ( - ATTR_PERCENTAGE_STEP, - ATTR_PRESET_MODES, + FanEntityStateAttribute.PERCENTAGE_STEP, + FanEntityCapabilityAttribute.PRESET_MODES, ) ) features = state.attributes.get(EntityStateAttribute.SUPPORTED_FEATURES, 0) - percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1) - self.preset_modes: list[str] | None = state.attributes.get(ATTR_PRESET_MODES) + percentage_step = state.attributes.get( + FanEntityStateAttribute.PERCENTAGE_STEP, 1 + ) + self.preset_modes: list[str] | None = state.attributes.get( + FanEntityCapabilityAttribute.PRESET_MODES + ) if features & FanEntityFeature.DIRECTION: self.chars.append(CHAR_ROTATION_DIRECTION) @@ -210,7 +214,9 @@ class Fan(HomeAccessory): params[ATTR_PRESET_MODE] = self.preset_modes[0] self.async_call_service(FAN_DOMAIN, SERVICE_SET_PRESET_MODE, params) elif current_state := self.hass.states.get(self.entity_id): - percentage: float = current_state.attributes.get(ATTR_PERCENTAGE) or 50.0 + percentage: float = ( + current_state.attributes.get(FanEntityStateAttribute.PERCENTAGE) or 50.0 + ) params[ATTR_PERCENTAGE] = percentage _LOGGER.debug("%s: Set auto to 0", self.entity_id) self.async_call_service(FAN_DOMAIN, SERVICE_TURN_ON, params) @@ -267,7 +273,7 @@ class Fan(HomeAccessory): # Handle Direction if self.char_direction is not None: - direction = new_state.attributes.get(ATTR_DIRECTION) + direction = new_state.attributes.get(FanEntityStateAttribute.DIRECTION) if direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): hk_direction = 1 if direction == DIRECTION_REVERSE else 0 self.char_direction.set_value(hk_direction) @@ -276,7 +282,7 @@ class Fan(HomeAccessory): if self.char_speed is not None and state != STATE_OFF: # We do not change the homekit speed when turning off # as it will clear the restore state - percentage = attributes.get(ATTR_PERCENTAGE) + percentage = attributes.get(FanEntityStateAttribute.PERCENTAGE) # If the homeassistant component reports its speed as the first entry # in its speed list but is not off, the hk_speed_value is 0. But 0 # is a special value in homekit. When you turn on a homekit accessory @@ -295,12 +301,12 @@ class Fan(HomeAccessory): # Handle Oscillating if self.char_swing is not None: - oscillating = attributes.get(ATTR_OSCILLATING) + oscillating = attributes.get(FanEntityStateAttribute.OSCILLATING) if isinstance(oscillating, bool): hk_oscillating = 1 if oscillating else 0 self.char_swing.set_value(hk_oscillating) - current_preset_mode = attributes.get(ATTR_PRESET_MODE) + current_preset_mode = attributes.get(FanEntityStateAttribute.PRESET_MODE) if self.char_target_fan_state is not None: # Handle single preset mode self.char_target_fan_state.set_value(int(current_preset_mode is not None)) diff --git a/homeassistant/components/homekit/type_heater_coolers.py b/homeassistant/components/homekit/type_heater_coolers.py index d50169b7eb54..7bd8a0eb7c41 100644 --- a/homeassistant/components/homekit/type_heater_coolers.py +++ b/homeassistant/components/homekit/type_heater_coolers.py @@ -10,11 +10,7 @@ from pyhap.characteristic import Characteristic from pyhap.const import CATEGORY_AIR_CONDITIONER, CATEGORY_HEATER from homeassistant.components.climate import ( - ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, - ATTR_HVAC_ACTION, ATTR_HVAC_MODE, - ATTR_HVAC_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, @@ -25,7 +21,9 @@ from homeassistant.components.climate import ( SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + ClimateEntityCapabilityAttribute, ClimateEntityFeature, + ClimateEntityStateAttribute, HVACAction, HVACMode, ) @@ -151,7 +149,7 @@ class HeaterCooler(HomeKitClimateAccessory): ) ) - hvac_modes = attributes.get(ATTR_HVAC_MODES, []) + hvac_modes = attributes.get(ClimateEntityCapabilityAttribute.HVAC_MODES, []) current_mode = try_parse_enum(HVACMode, state.state) self._supports_off = HVACMode.OFF in hvac_modes @@ -229,7 +227,7 @@ class HeaterCooler(HomeKitClimateAccessory): self.fan_chars.append(CHAR_TARGET_FAN_STATE) if self.ordered_fan_speeds: self.fan_chars.append(CHAR_ROTATION_SPEED) - if attributes.get(ATTR_HVAC_ACTION) is not None: + if attributes.get(ClimateEntityStateAttribute.HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) # Fan/swing modes are detected in the base class; only advertise the @@ -304,7 +302,7 @@ class HeaterCooler(HomeKitClimateAccessory): # Thermostat, this is decided once at setup and not a reload attribute: # current humidity changes on every update, so reloading on it would # thrash the accessory. - self._has_humidity = ATTR_CURRENT_HUMIDITY in attributes + self._has_humidity = ClimateEntityStateAttribute.CURRENT_HUMIDITY in attributes if self._has_humidity: humidity_serv = self.add_preload_service(SERV_HUMIDITY_SENSOR, CHAR_NAME) serv.add_linked_service(humidity_serv) @@ -534,10 +532,13 @@ class HeaterCooler(HomeKitClimateAccessory): self._has_cool_threshold and self._has_heat_threshold and ( - ATTR_TARGET_TEMP_HIGH in attributes - or ATTR_TARGET_TEMP_LOW in attributes + ClimateEntityStateAttribute.TARGET_TEMP_HIGH in attributes + or ClimateEntityStateAttribute.TARGET_TEMP_LOW in attributes + ) + and ( + effective_mode in RANGE_MODES + or ClimateEntityStateAttribute.TARGET_TEMPERATURE not in attributes ) - and (effective_mode in RANGE_MODES or ATTR_TEMPERATURE not in attributes) ) if use_range: @@ -584,7 +585,9 @@ class HeaterCooler(HomeKitClimateAccessory): # Pick whichever threshold moved further from the entity's existing # target setpoint. The thresholds are in HomeKit units, so convert # the target setpoint before comparing. - target_temp = current_state.attributes.get(ATTR_TEMPERATURE) + target_temp = current_state.attributes.get( + ClimateEntityStateAttribute.TARGET_TEMPERATURE + ) if target_temp is None: selected_temp = heating_temp else: @@ -643,9 +646,9 @@ class HeaterCooler(HomeKitClimateAccessory): self.char_current_state.set_value(HC_INACTIVE) else: self.char_active.set_value(1) - action = attributes.get(ATTR_HVAC_ACTION) or self._derive_action( - new_state, current_mode - ) + action = attributes.get( + ClimateEntityStateAttribute.HVAC_ACTION + ) or self._derive_action(new_state, current_mode) self.char_current_state.set_value( HC_HASS_TO_HOMEKIT_ACTION.get(action, HC_INACTIVE) ) @@ -653,7 +656,8 @@ class HeaterCooler(HomeKitClimateAccessory): self._update_current_temperature_char(new_state) self._update_temperature_thresholds(new_state) if self._has_humidity and isinstance( - (humidity := attributes.get(ATTR_CURRENT_HUMIDITY)), (int, float) + (humidity := attributes.get(ClimateEntityStateAttribute.CURRENT_HUMIDITY)), + (int, float), ): self.char_current_humidity.set_value(humidity) if self.fan_chars: @@ -671,8 +675,8 @@ class HeaterCooler(HomeKitClimateAccessory): # Dual capable entities publish the range keys even in single # setpoint modes, so only values decide what is displayed. supports_dual_temp = ( - attributes.get(ATTR_TARGET_TEMP_HIGH) is not None - or attributes.get(ATTR_TARGET_TEMP_LOW) is not None + attributes.get(ClimateEntityStateAttribute.TARGET_TEMP_HIGH) is not None + or attributes.get(ClimateEntityStateAttribute.TARGET_TEMP_LOW) is not None ) if supports_dual_temp: @@ -686,7 +690,7 @@ class HeaterCooler(HomeKitClimateAccessory): ) elif ( target_temp := temperature_attribute_to_homekit( - state, ATTR_TEMPERATURE, self._unit + state, ClimateEntityStateAttribute.TARGET_TEMPERATURE, self._unit ) ) is not None: if self._has_cool_threshold: @@ -697,7 +701,7 @@ class HeaterCooler(HomeKitClimateAccessory): def _derive_action(self, state: State, mode: HVACMode | None) -> HVACAction: """Infer heating / cooling when integration omits hvac_action.""" attributes = state.attributes - cur = attributes.get(ATTR_CURRENT_TEMPERATURE) + cur = attributes.get(ClimateEntityStateAttribute.CURRENT_TEMPERATURE) if cur is None or mode is None: return HVACAction.IDLE @@ -705,17 +709,19 @@ class HeaterCooler(HomeKitClimateAccessory): # Range modes have independent thresholds; single-target modes only # drive one side. Any other mode (e.g. dry, fan_only) stays idle. if mode in RANGE_MODES: - cool_above = attributes.get(ATTR_TARGET_TEMP_HIGH) - heat_below = attributes.get(ATTR_TARGET_TEMP_LOW) + cool_above = attributes.get(ClimateEntityStateAttribute.TARGET_TEMP_HIGH) + heat_below = attributes.get(ClimateEntityStateAttribute.TARGET_TEMP_LOW) if cool_above is None and heat_below is None: # Some integrations run auto from a single setpoint. - cool_above = heat_below = attributes.get(ATTR_TEMPERATURE) + cool_above = heat_below = attributes.get( + ClimateEntityStateAttribute.TARGET_TEMPERATURE + ) elif mode == HVACMode.COOL: - cool_above = attributes.get(ATTR_TEMPERATURE) + cool_above = attributes.get(ClimateEntityStateAttribute.TARGET_TEMPERATURE) heat_below = None elif mode == HVACMode.HEAT: cool_above = None - heat_below = attributes.get(ATTR_TEMPERATURE) + heat_below = attributes.get(ClimateEntityStateAttribute.TARGET_TEMPERATURE) else: return HVACAction.IDLE diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 8ad8a2aacd69..6b3af4df30bf 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -7,15 +7,14 @@ from pyhap.const import CATEGORY_HUMIDIFIER from pyhap.util import callback as pyhap_callback from homeassistant.components.humidifier import ( - ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, - ATTR_MAX_HUMIDITY, - ATTR_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, DOMAIN as HUMIDIFIER_DOMAIN, SERVICE_SET_HUMIDITY, HumidifierDeviceClass, + HumidifierEntityCapabilityAttribute, + HumidifierEntityStateAttribute, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -101,8 +100,8 @@ class HumidifierDehumidifier(HomeAccessory): super().__init__(*args, category=CATEGORY_HUMIDIFIER) self._reload_on_change_attrs.extend( ( - ATTR_MAX_HUMIDITY, - ATTR_MIN_HUMIDITY, + HumidifierEntityCapabilityAttribute.MAX_HUMIDITY, + HumidifierEntityCapabilityAttribute.MIN_HUMIDITY, ) ) @@ -287,10 +286,22 @@ class HumidifierDehumidifier(HomeAccessory): """Return min and max humidity range.""" attributes = state.attributes min_humidity = max( - round(attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY)), 0 + round( + attributes.get( + HumidifierEntityCapabilityAttribute.MIN_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + ) + ), + 0, ) max_humidity = min( - round(attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY)), 100 + round( + attributes.get( + HumidifierEntityCapabilityAttribute.MAX_HUMIDITY, + DEFAULT_MAX_HUMIDITY, + ) + ), + 100, ) return min_humidity, max_humidity @@ -315,9 +326,11 @@ class HumidifierDehumidifier(HomeAccessory): self.char_current_humidifier_dehumidifier.set_value(current_state) # Update target humidity - target_humidity = attributes.get(ATTR_HUMIDITY) + target_humidity = attributes.get(HumidifierEntityStateAttribute.HUMIDITY) if isinstance(target_humidity, (int, float)): self.char_target_humidity.set_value(target_humidity) - current_humidity = attributes.get(ATTR_CURRENT_HUMIDITY) + current_humidity = attributes.get( + HumidifierEntityStateAttribute.CURRENT_HUMIDITY + ) if isinstance(current_humidity, (int, float)): self.char_current_humidity.set_value(current_humidity) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index bfac12a17833..080cff5c0230 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -7,19 +7,16 @@ from typing import Any, override from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, - ATTR_MAX_COLOR_TEMP_KELVIN, - ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, - ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, ColorMode, + LightEntityCapabilityAttribute, + LightEntityStateAttribute, brightness_supported, color_supported, color_temp_supported, @@ -75,9 +72,9 @@ class Light(HomeAccessory): super().__init__(*args, category=CATEGORY_LIGHTBULB) self._reload_on_change_attrs.extend( ( - ATTR_SUPPORTED_COLOR_MODES, - ATTR_MAX_COLOR_TEMP_KELVIN, - ATTR_MIN_COLOR_TEMP_KELVIN, + LightEntityCapabilityAttribute.SUPPORTED_COLOR_MODES, + LightEntityCapabilityAttribute.MAX_COLOR_TEMP_KELVIN, + LightEntityCapabilityAttribute.MIN_COLOR_TEMP_KELVIN, ) ) self.chars = [] @@ -88,9 +85,9 @@ class Light(HomeAccessory): assert state attributes = state.attributes self.color_modes = color_modes = ( - attributes.get(ATTR_SUPPORTED_COLOR_MODES) or [] + attributes.get(LightEntityCapabilityAttribute.SUPPORTED_COLOR_MODES) or [] ) - self._previous_color_mode = attributes.get(ATTR_COLOR_MODE) + self._previous_color_mode = attributes.get(LightEntityStateAttribute.COLOR_MODE) self.color_supported = color_supported(color_modes) self.color_temp_supported = color_temp_supported(color_modes) self.rgbw_supported = ColorMode.RGBW in color_modes @@ -121,10 +118,16 @@ class Light(HomeAccessory): if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = color_temperature_kelvin_to_mired( - attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN, DEFAULT_MAX_COLOR_TEMP) + attributes.get( + LightEntityCapabilityAttribute.MAX_COLOR_TEMP_KELVIN, + DEFAULT_MAX_COLOR_TEMP, + ) ) max_mireds = color_temperature_kelvin_to_mired( - attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN, DEFAULT_MIN_COLOR_TEMP) + attributes.get( + LightEntityCapabilityAttribute.MIN_COLOR_TEMP_KELVIN, + DEFAULT_MIN_COLOR_TEMP, + ) ) # Ensure min is less than max self.min_mireds, self.max_mireds = get_min_max(min_mireds, max_mireds) @@ -250,7 +253,7 @@ class Light(HomeAccessory): # Handle State state = new_state.state attributes = new_state.attributes - color_mode = attributes.get(ATTR_COLOR_MODE) + color_mode = attributes.get(LightEntityStateAttribute.COLOR_MODE) self.char_on.set_value(int(state == STATE_ON)) color_mode_changed = self._previous_color_mode != color_mode self._previous_color_mode = color_mode @@ -258,7 +261,8 @@ class Light(HomeAccessory): # Handle Brightness if ( self.brightness_supported - and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None + and (brightness := attributes.get(LightEntityStateAttribute.BRIGHTNESS)) + is not None and isinstance(brightness, (int, float)) ): brightness = round(brightness / 255 * 100, 0) @@ -281,12 +285,14 @@ class Light(HomeAccessory): # Handle Color - color must always be set before color temperature # or the iOS UI will not display it correctly. if self.color_supported: - if color_temp := attributes.get(ATTR_COLOR_TEMP_KELVIN): + if color_temp := attributes.get( + LightEntityStateAttribute.COLOR_TEMP_KELVIN + ): hue, saturation = color_temperature_to_hs(color_temp) elif color_mode == ColorMode.WHITE: hue, saturation = 0, 0 elif ( - (hue_sat := attributes.get(ATTR_HS_COLOR)) + (hue_sat := attributes.get(LightEntityStateAttribute.HS_COLOR)) and isinstance(hue_sat, (list, tuple)) and len(hue_sat) == 2 ): @@ -306,7 +312,9 @@ class Light(HomeAccessory): if CHAR_COLOR_TEMPERATURE in self.chars: color_temp = None if self.color_temp_supported: - color_temp_kelvin = attributes.get(ATTR_COLOR_TEMP_KELVIN) + color_temp_kelvin = attributes.get( + LightEntityStateAttribute.COLOR_TEMP_KELVIN + ) if color_temp_kelvin is not None: color_temp = color_temperature_kelvin_to_mired(color_temp_kelvin) elif color_mode == ColorMode.WHITE: diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index e0e3ad96cc99..644b33b0afbb 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -8,12 +8,13 @@ from pyhap.const import CATEGORY_SWITCH from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, - ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, + MediaPlayerEntityCapabilityAttribute, MediaPlayerEntityFeature, + MediaPlayerEntityStateAttribute, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -242,7 +243,11 @@ class MediaPlayer(HomeAccessory): play_stop_char.set_value(hk_state) if toggle_mute_char := self.chars[FEATURE_TOGGLE_MUTE]: - mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) + mute_state = bool( + new_state.attributes.get( + MediaPlayerEntityStateAttribute.MEDIA_VOLUME_MUTED + ) + ) _LOGGER.debug( '%s: Set current state for "toggle_mute" to %s', self.entity_id, @@ -259,8 +264,8 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): """Initialize a Television Media Player accessory object.""" super().__init__( MediaPlayerEntityFeature.SELECT_SOURCE, - ATTR_INPUT_SOURCE, - ATTR_INPUT_SOURCE_LIST, + MediaPlayerEntityStateAttribute.INPUT_SOURCE, + MediaPlayerEntityCapabilityAttribute.INPUT_SOURCE_LIST, *args, **kwargs, ) @@ -394,7 +399,11 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): # Set mute state if CHAR_VOLUME_SELECTOR in self.chars_speaker: - current_mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) + current_mute_state = bool( + new_state.attributes.get( + MediaPlayerEntityStateAttribute.MEDIA_VOLUME_MUTED + ) + ) _LOGGER.debug( "%s: Set current mute state to %s", self.entity_id, diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 19e48a4017e4..003d9cd6ee01 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -8,10 +8,9 @@ from pyhap.const import CATEGORY_TELEVISION from homeassistant.components.remote import ( ATTR_ACTIVITY, - ATTR_ACTIVITY_LIST, - ATTR_CURRENT_ACTIVITY, DOMAIN as REMOTE_DOMAIN, RemoteEntityFeature, + RemoteEntityStateAttribute, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -224,8 +223,8 @@ class ActivityRemote(RemoteInputSelectAccessory): """Initialize a Activity Remote accessory object.""" super().__init__( RemoteEntityFeature.ACTIVITY, - ATTR_CURRENT_ACTIVITY, - ATTR_ACTIVITY_LIST, + RemoteEntityStateAttribute.CURRENT_ACTIVITY, + RemoteEntityStateAttribute.ACTIVITY_LIST, *args, ) state = self.hass.states.get(self.entity_id) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 5d7e48003be9..ab852d7c29ed 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -23,13 +23,14 @@ from homeassistant.components.input_number import ( DOMAIN as INPUT_NUMBER_DOMAIN, SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE, ) -from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION +from homeassistant.components.input_select import SERVICE_SELECT_OPTION from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, SERVICE_DOCK, SERVICE_START_MOWING, LawnMowerActivity, ) +from homeassistant.components.select import SelectEntityCapabilityAttribute from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, @@ -527,7 +528,7 @@ class SelectSwitch(HomeAccessory): assert state self.select_chars: dict[str, Characteristic] = {} - options = state.attributes[ATTR_OPTIONS] + options = state.attributes[SelectEntityCapabilityAttribute.OPTIONS] for option in options: serv_option = self.add_preload_service( SERV_OUTLET, diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 8a2aaafc278a..bb025bd602c4 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -6,16 +6,8 @@ from typing import Any, override from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, ATTR_HUMIDITY, - ATTR_HVAC_ACTION, ATTR_HVAC_MODE, - ATTR_HVAC_MODES, - ATTR_MAX_HUMIDITY, - ATTR_MAX_TEMP, - ATTR_MIN_HUMIDITY, - ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_HUMIDITY, @@ -26,17 +18,20 @@ from homeassistant.components.climate import ( SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, + ClimateEntityCapabilityAttribute, ClimateEntityFeature, + ClimateEntityStateAttribute, HVACAction, HVACMode, ) from homeassistant.components.water_heater import ( - ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, DOMAIN as WATER_HEATER_DOMAIN, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER, + WaterHeaterCapabilityAttribute, WaterHeaterEntityFeature, + WaterHeaterStateAttribute, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -162,15 +157,21 @@ class Thermostat(HomeKitClimateAccessory): assert state hc_min_temp, hc_max_temp = self.get_temperature_range(state) # The common climate reload attributes are added by the base class. - self._reload_on_change_attrs.append(ATTR_MIN_HUMIDITY) + self._reload_on_change_attrs.append( + ClimateEntityCapabilityAttribute.MIN_HUMIDITY + ) # Add additional characteristics if auto mode is supported self.chars: list[str] = [] attributes = state.attributes min_humidity, _ = get_min_max( - attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY), - attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY), + attributes.get( + ClimateEntityCapabilityAttribute.MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY + ), + attributes.get( + ClimateEntityCapabilityAttribute.MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY + ), ) features = attributes.get(EntityStateAttribute.SUPPORTED_FEATURES, 0) @@ -180,7 +181,7 @@ class Thermostat(HomeKitClimateAccessory): ) if ( - ATTR_CURRENT_HUMIDITY in attributes + ClimateEntityStateAttribute.CURRENT_HUMIDITY in attributes or features & ClimateEntityFeature.TARGET_HUMIDITY ): self.chars.append(CHAR_CURRENT_HUMIDITY) @@ -271,7 +272,7 @@ class Thermostat(HomeKitClimateAccessory): self.fan_chars.append(CHAR_SWING_MODE) if self.fan_chars: - if attributes.get(ATTR_HVAC_ACTION) is not None: + if attributes.get(ClimateEntityStateAttribute.HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) self._configure_fan_service(serv_thermostat) @@ -307,7 +308,9 @@ class Thermostat(HomeKitClimateAccessory): # and hope for the best. hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE) hc_current_temp = temperature_attribute_to_homekit( - state, ATTR_CURRENT_TEMPERATURE, self._unit + state, + ClimateEntityStateAttribute.CURRENT_TEMPERATURE, + self._unit, ) hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT if ( @@ -411,7 +414,10 @@ class Thermostat(HomeKitClimateAccessory): def _configure_hvac_modes(self, state: State) -> None: """Configure target mode characteristics.""" # This cannot be none OR an empty list - hc_modes = state.attributes.get(ATTR_HVAC_MODES) or DEFAULT_HVAC_MODES + hc_modes = ( + state.attributes.get(ClimateEntityCapabilityAttribute.HVAC_MODES) + or DEFAULT_HVAC_MODES + ) # Determine available modes for this entity, # Prefer HEAT_COOL over AUTO and COOL over FAN_ONLY, DRY # @@ -466,7 +472,7 @@ class Thermostat(HomeKitClimateAccessory): ) # Set current operation mode for supported thermostats - if hvac_action := attributes.get(ATTR_HVAC_ACTION): + if hvac_action := attributes.get(ClimateEntityStateAttribute.HVAC_ACTION): self.char_current_heat_cool.set_value( HC_HASS_TO_HOMEKIT_ACTION.get(hvac_action, HC_HEAT_COOL_OFF) ) @@ -476,14 +482,16 @@ class Thermostat(HomeKitClimateAccessory): # Update current humidity if CHAR_CURRENT_HUMIDITY in self.chars: assert self.char_current_humidity - current_humdity = attributes.get(ATTR_CURRENT_HUMIDITY) + current_humdity = attributes.get( + ClimateEntityStateAttribute.CURRENT_HUMIDITY + ) if isinstance(current_humdity, (int, float)): self.char_current_humidity.set_value(current_humdity) # Update target humidity if CHAR_TARGET_HUMIDITY in self.chars: assert self.char_target_humidity - target_humdity = attributes.get(ATTR_HUMIDITY) + target_humdity = attributes.get(ClimateEntityStateAttribute.TARGET_HUMIDITY) if isinstance(target_humdity, (int, float)): self.char_target_humidity.set_value(target_humdity) @@ -499,7 +507,7 @@ class Thermostat(HomeKitClimateAccessory): # Update target temperature target_temp = temperature_attribute_to_homekit( - new_state, ATTR_TEMPERATURE, self._unit + new_state, ClimateEntityStateAttribute.TARGET_TEMPERATURE, self._unit ) if ( target_temp is None @@ -509,11 +517,11 @@ class Thermostat(HomeKitClimateAccessory): # even if the device does not support it hc_hvac_mode = self.char_target_heat_cool.value if hc_hvac_mode == HC_HEAT_COOL_HEAT: - temp_low = attributes.get(ATTR_TARGET_TEMP_LOW) + temp_low = attributes.get(ClimateEntityStateAttribute.TARGET_TEMP_LOW) if isinstance(temp_low, (int, float)): target_temp = self._temperature_to_homekit(temp_low) elif hc_hvac_mode == HC_HEAT_COOL_COOL: - temp_high = attributes.get(ATTR_TARGET_TEMP_HIGH) + temp_high = attributes.get(ClimateEntityStateAttribute.TARGET_TEMP_HIGH) if isinstance(temp_high, (int, float)): target_temp = self._temperature_to_homekit(temp_high) if target_temp: @@ -537,9 +545,9 @@ class WaterHeater(HomeAccessory): super().__init__(*args, category=CATEGORY_THERMOSTAT) self._reload_on_change_attrs.extend( ( - ATTR_MAX_TEMP, - ATTR_MIN_TEMP, - ATTR_OPERATION_LIST, + WaterHeaterCapabilityAttribute.MAX_TEMP, + WaterHeaterCapabilityAttribute.MIN_TEMP, + WaterHeaterCapabilityAttribute.OPERATION_LIST, ) ) self._unit = self.hass.config.units.temperature_unit @@ -548,7 +556,9 @@ class WaterHeater(HomeAccessory): min_temp, max_temp = self.get_temperature_range(state) features = state.attributes.get(EntityStateAttribute.SUPPORTED_FEATURES, 0) - operation_list = state.attributes.get(ATTR_OPERATION_LIST) or [] + operation_list = ( + state.attributes.get(WaterHeaterCapabilityAttribute.OPERATION_LIST) or [] + ) self._supports_on_off = bool(features & WaterHeaterEntityFeature.ON_OFF) self._supports_operation_mode = bool( features & WaterHeaterEntityFeature.OPERATION_MODE @@ -629,11 +639,16 @@ class WaterHeater(HomeAccessory): state = self.hass.states.get(self.entity_id) if not state: return - current_operation_mode = state.attributes.get(ATTR_OPERATION_MODE) + current_operation_mode = state.attributes.get( + WaterHeaterStateAttribute.OPERATION_MODE + ) if current_operation_mode and current_operation_mode != STATE_OFF: # Already in a non-off operation mode; do not change it. return - operation_list = state.attributes.get(ATTR_OPERATION_LIST) or [] + operation_list = ( + state.attributes.get(WaterHeaterCapabilityAttribute.OPERATION_LIST) + or [] + ) for mode in operation_list: if mode != STATE_OFF: params[ATTR_OPERATION_MODE] = mode @@ -665,13 +680,13 @@ class WaterHeater(HomeAccessory): """Update water_heater state after state change.""" # Update current and target temperature target_temperature = temperature_attribute_to_homekit( - new_state, ATTR_TEMPERATURE, self._unit + new_state, WaterHeaterStateAttribute.TEMPERATURE, self._unit ) if target_temperature is not None: self.char_target_temp.set_value(target_temperature) current_temperature = temperature_attribute_to_homekit( - new_state, ATTR_CURRENT_TEMPERATURE, self._unit + new_state, WaterHeaterStateAttribute.CURRENT_TEMPERATURE, self._unit ) if current_temperature is not None: self.char_current_temp.set_value(current_temperature)