mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Use SmartThings custom setpoint bounds for air conditioners (#180304)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d1e33f99a7
commit
9145c009f8
@@ -415,6 +415,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
Capability.THERMOSTAT_COOLING_SETPOINT,
|
||||
Capability.TEMPERATURE_MEASUREMENT,
|
||||
Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE,
|
||||
Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL,
|
||||
Capability.DEMAND_RESPONSE_LOAD_CONTROL,
|
||||
},
|
||||
)
|
||||
@@ -633,28 +634,55 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
step, self._setpoint_range_unit, self.temperature_unit
|
||||
)
|
||||
|
||||
def _get_custom_setpoint(self, attribute: Attribute) -> float | None:
|
||||
"""Return a setpoint bound from the custom setpoint control capability."""
|
||||
if not self.supports_capability(Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL):
|
||||
return None
|
||||
setpoint = self.get_attribute_value(
|
||||
Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, attribute
|
||||
)
|
||||
# Devices report -1000 when the bound is not available
|
||||
if setpoint is None or setpoint == -1000:
|
||||
return None
|
||||
unit = self._internal_state[Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL][
|
||||
attribute
|
||||
].unit
|
||||
return TemperatureConverter.convert(
|
||||
setpoint,
|
||||
UNIT_MAP[unit] if unit else self.temperature_unit,
|
||||
self.temperature_unit,
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature."""
|
||||
if (minimum := self._get_setpoint_range_value("minimum")) is None:
|
||||
if (minimum := self._get_setpoint_range_value("minimum")) is not None:
|
||||
return TemperatureConverter.convert(
|
||||
DEFAULT_MIN_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unit
|
||||
minimum, self._setpoint_range_unit, self.temperature_unit
|
||||
)
|
||||
if (
|
||||
minimum := self._get_custom_setpoint(Attribute.MINIMUM_SETPOINT)
|
||||
) is not None:
|
||||
return minimum
|
||||
return TemperatureConverter.convert(
|
||||
minimum, self._setpoint_range_unit, self.temperature_unit
|
||||
DEFAULT_MIN_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unit
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum temperature."""
|
||||
if (maximum := self._get_setpoint_range_value("maximum")) is None:
|
||||
if (maximum := self._get_setpoint_range_value("maximum")) is not None:
|
||||
return TemperatureConverter.convert(
|
||||
DEFAULT_MAX_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unit
|
||||
maximum, self._setpoint_range_unit, self.temperature_unit
|
||||
)
|
||||
if (
|
||||
maximum := self._get_custom_setpoint(Attribute.MAXIMUM_SETPOINT)
|
||||
) is not None:
|
||||
return maximum
|
||||
return TemperatureConverter.convert(
|
||||
maximum, self._setpoint_range_unit, self.temperature_unit
|
||||
DEFAULT_MAX_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unit
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -349,8 +349,8 @@
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
|
||||
'none',
|
||||
'wind_free',
|
||||
@@ -412,8 +412,8 @@
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'wind_free',
|
||||
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
|
||||
'none',
|
||||
@@ -454,8 +454,8 @@
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
|
||||
'none',
|
||||
'sleep',
|
||||
@@ -529,8 +529,8 @@
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
<ClimateEntityStateAttribute.PRESET_MODE: 'preset_mode'>: 'none',
|
||||
<ClimateEntityCapabilityAttribute.PRESET_MODES: 'preset_modes'>: list([
|
||||
'none',
|
||||
@@ -709,8 +709,8 @@
|
||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -762,8 +762,8 @@
|
||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
]),
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 35,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 7,
|
||||
<ClimateEntityCapabilityAttribute.MAX_TEMP: 'max_temp'>: 30,
|
||||
<ClimateEntityCapabilityAttribute.MIN_TEMP: 'min_temp'>: 16,
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <ClimateEntityFeature: 393>,
|
||||
<ClimateEntityStateAttribute.TARGET_TEMPERATURE: 'temperature'>: 18,
|
||||
}),
|
||||
|
||||
@@ -824,9 +824,10 @@ async def test_ac_setpoint_range_update(
|
||||
"""Test the setpoint range is used when the device reports one."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Without a setpoint range the custom setpoint bounds are used
|
||||
state = hass.states.get("climate.theater_ac_office_granit")
|
||||
assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP
|
||||
assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 16
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 30
|
||||
assert ATTR_TARGET_TEMP_STEP not in state.attributes
|
||||
|
||||
await trigger_update(
|
||||
@@ -883,6 +884,69 @@ async def test_ac_setpoint_range_converted_to_device_unit(
|
||||
assert state.attributes[ATTR_TARGET_TEMP_STEP] == 1.8
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000003"])
|
||||
async def test_ac_custom_setpoint_bounds(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the custom setpoint bounds are used when there is no setpoint range."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("climate.clim_salon")
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 16
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 30
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000003"])
|
||||
@pytest.mark.parametrize("value", [None, -1000])
|
||||
async def test_ac_custom_setpoint_bounds_unavailable(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
value: int | None,
|
||||
) -> None:
|
||||
"""Test we fall back to the defaults when the custom bounds are unavailable."""
|
||||
set_attribute_value(
|
||||
devices,
|
||||
Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL,
|
||||
Attribute.MINIMUM_SETPOINT,
|
||||
value,
|
||||
)
|
||||
set_attribute_value(
|
||||
devices,
|
||||
Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL,
|
||||
Attribute.MAXIMUM_SETPOINT,
|
||||
value,
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("climate.clim_salon")
|
||||
assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP
|
||||
assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_01001"])
|
||||
async def test_ac_setpoint_range_takes_precedence(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the setpoint range wins over the custom setpoint bounds."""
|
||||
set_attribute_value(
|
||||
devices,
|
||||
Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL,
|
||||
Attribute.MINIMUM_SETPOINT,
|
||||
5,
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("climate.theater_aire_dormitorio_principal")
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 16
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["virtual_thermostat"])
|
||||
async def test_thermostat_set_fan_mode(
|
||||
hass: HomeAssistant,
|
||||
|
||||
Reference in New Issue
Block a user