Use platform entity attribute enums for reads in HomeKit (#180016)

This commit is contained in:
epenet
2026-08-24 17:17:10 +02:00
committed by GitHub
parent c1952cddad
commit dd47f7dac0
12 changed files with 200 additions and 136 deletions
@@ -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]
@@ -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
@@ -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"
@@ -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)
+17 -11
View File
@@ -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))
@@ -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
@@ -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)
+25 -17
View File
@@ -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:
@@ -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,
@@ -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)
@@ -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,
@@ -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)