mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Watts: add HVAC action + preset mode (#169546)
Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
co-authored by
Copilot
Joostlek
parent
261ca2dd9a
commit
7ba7700d5e
@@ -3,11 +3,12 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from visionpluspython.models import ThermostatDevice
|
||||
from visionpluspython.models import ThermostatDevice, ThermostatMode
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
@@ -17,7 +18,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import WattsVisionConfigEntry
|
||||
from .const import DOMAIN, HVAC_MODE_TO_THERMOSTAT, THERMOSTAT_MODE_TO_HVAC
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
HVAC_ACTION_TO_HA,
|
||||
HVAC_MODE_TO_THERMOSTAT,
|
||||
PRESET_MODE_TO_THERMOSTAT,
|
||||
PRESET_MODES,
|
||||
THERMOSTAT_MODE_TO_HVAC,
|
||||
THERMOSTAT_MODE_TO_PRESET,
|
||||
)
|
||||
from .coordinator import WattsVisionDeviceCoordinator
|
||||
from .entity import WattsVisionEntity
|
||||
|
||||
@@ -26,6 +35,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def _parse_thermostat_mode(mode: str) -> ThermostatMode:
|
||||
return ThermostatMode[mode.upper()]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: WattsVisionConfigEntry,
|
||||
@@ -79,9 +92,13 @@ async def async_setup_entry(
|
||||
class WattsVisionClimate(WattsVisionEntity[ThermostatDevice], ClimateEntity):
|
||||
"""Representation of a Watts Vision heater as a climate entity."""
|
||||
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO]
|
||||
_attr_preset_modes = PRESET_MODES
|
||||
_attr_name = None
|
||||
_attr_translation_key = "thermostat"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -112,7 +129,44 @@ class WattsVisionClimate(WattsVisionEntity[ThermostatDevice], ClimateEntity):
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return hvac mode."""
|
||||
return THERMOSTAT_MODE_TO_HVAC.get(self.device.thermostat_mode)
|
||||
return THERMOSTAT_MODE_TO_HVAC.get(
|
||||
_parse_thermostat_mode(self.device.thermostat_mode)
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current HVAC action."""
|
||||
return HVAC_ACTION_TO_HA.get(self.device.hvac_action)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
return THERMOSTAT_MODE_TO_PRESET.get(
|
||||
_parse_thermostat_mode(self.device.thermostat_mode)
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
mode = PRESET_MODE_TO_THERMOSTAT[preset_mode]
|
||||
|
||||
try:
|
||||
await self.coordinator.client.set_thermostat_mode(self.device_id, mode)
|
||||
except (ValueError, RuntimeError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_preset_mode_error",
|
||||
) from err
|
||||
|
||||
_LOGGER.debug(
|
||||
"Successfully set preset mode to %s (ThermostatMode.%s) for %s",
|
||||
preset_mode,
|
||||
mode.name,
|
||||
self.device_id,
|
||||
)
|
||||
|
||||
self.coordinator.trigger_fast_polling()
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
from visionpluspython.models import SwitchDevice, ThermostatDevice, ThermostatMode
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
|
||||
DOMAIN = "watts"
|
||||
|
||||
@@ -20,20 +25,45 @@ UPDATE_INTERVAL_SECONDS = 30
|
||||
FAST_POLLING_INTERVAL_SECONDS = 5
|
||||
DISCOVERY_INTERVAL_MINUTES = 15
|
||||
|
||||
# Mapping from Watts Vision + modes to Home Assistant HVAC modes
|
||||
|
||||
THERMOSTAT_MODE_TO_HVAC = {
|
||||
"Program": HVACMode.AUTO,
|
||||
"Eco": HVACMode.HEAT,
|
||||
"Comfort": HVACMode.HEAT,
|
||||
"Off": HVACMode.OFF,
|
||||
# Mapping from Watts Vision+ modes to Home Assistant HVAC modes
|
||||
THERMOSTAT_MODE_TO_HVAC: dict[ThermostatMode, HVACMode] = {
|
||||
ThermostatMode.PROGRAM: HVACMode.AUTO,
|
||||
ThermostatMode.ECO: HVACMode.HEAT,
|
||||
ThermostatMode.COMFORT: HVACMode.HEAT,
|
||||
ThermostatMode.DEFROST: HVACMode.HEAT,
|
||||
ThermostatMode.TIMER: HVACMode.HEAT,
|
||||
ThermostatMode.OFF: HVACMode.OFF,
|
||||
}
|
||||
|
||||
# Mapping from Home Assistant HVAC modes to Watts Vision + modes
|
||||
HVAC_MODE_TO_THERMOSTAT = {
|
||||
# Mapping from Home Assistant HVAC modes to Watts Vision+ modes
|
||||
HVAC_MODE_TO_THERMOSTAT: dict[HVACMode, ThermostatMode] = {
|
||||
HVACMode.HEAT: ThermostatMode.COMFORT,
|
||||
HVACMode.OFF: ThermostatMode.OFF,
|
||||
HVACMode.AUTO: ThermostatMode.PROGRAM,
|
||||
}
|
||||
|
||||
# Preset modes available on all Watts Vision+ thermostats.
|
||||
PRESET_MODES: list[str] = [PRESET_COMFORT, PRESET_ECO, "defrost", "timer"]
|
||||
|
||||
# Mapping from Watts Vision+ mode to HA preset mode string
|
||||
THERMOSTAT_MODE_TO_PRESET: dict[ThermostatMode, str] = {
|
||||
ThermostatMode.COMFORT: PRESET_COMFORT,
|
||||
ThermostatMode.ECO: PRESET_ECO,
|
||||
ThermostatMode.DEFROST: "defrost",
|
||||
ThermostatMode.TIMER: "timer",
|
||||
}
|
||||
|
||||
# Mapping from HA preset mode string to Watts Vision+ ThermostatMode
|
||||
PRESET_MODE_TO_THERMOSTAT: dict[str, ThermostatMode] = {
|
||||
v: k for k, v in THERMOSTAT_MODE_TO_PRESET.items()
|
||||
}
|
||||
|
||||
# Mapping from Watts Vision+ HVAC actions to Home Assistant HVACAction
|
||||
HVAC_ACTION_TO_HA: dict[str, HVACAction] = {
|
||||
"Heating": HVACAction.HEATING,
|
||||
"Cooling": HVACAction.COOLING,
|
||||
"Idle": HVACAction.IDLE,
|
||||
"Off": HVACAction.OFF,
|
||||
}
|
||||
|
||||
SUPPORTED_DEVICE_TYPES = (ThermostatDevice, SwitchDevice)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"comfort": "mdi:weather-sunny",
|
||||
"defrost": "mdi:snowflake",
|
||||
"eco": "mdi:moon-waning-crescent",
|
||||
"timer": "mdi:timer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,9 @@ rules:
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: No entity required translations.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: Thermostat entities use standard HA Climate entity.
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
||||
@@ -30,6 +30,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"defrost": "Defrost",
|
||||
"timer": "Timer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"authentication_failed": {
|
||||
"message": "Authentication failed"
|
||||
@@ -58,6 +72,9 @@
|
||||
"set_hvac_mode_error": {
|
||||
"message": "An error occurred while setting the HVAC mode"
|
||||
},
|
||||
"set_preset_mode_error": {
|
||||
"message": "An error occurred while setting the preset mode"
|
||||
},
|
||||
"set_switch_state_error": {
|
||||
"message": "An error occurred while setting the switch state"
|
||||
},
|
||||
|
||||
@@ -83,9 +83,7 @@ def mock_watts_client() -> Generator[AsyncMock]:
|
||||
switch_detail_data["deviceId"]: switch_detail,
|
||||
}
|
||||
|
||||
async def get_device_side_effect(
|
||||
device_id: str, refresh: bool = False
|
||||
) -> Device:
|
||||
async def get_device_side_effect(device_id: str) -> Device:
|
||||
"""Return the appropriate device based on device_id."""
|
||||
return device_details.get(device_id, device_detail)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"currentTemperature": 21.0,
|
||||
"setpoint": 23.5,
|
||||
"thermostatMode": "Comfort",
|
||||
"hvacAction": "Heating",
|
||||
"minAllowedTemperature": 5.0,
|
||||
"maxAllowedTemperature": 30.0,
|
||||
"temperatureUnit": "C",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"currentTemperature": 20.8,
|
||||
"setpoint": 22.0,
|
||||
"thermostatMode": "Comfort",
|
||||
"hvacAction": "Heating",
|
||||
"minAllowedTemperature": 5.0,
|
||||
"maxAllowedTemperature": 30.0,
|
||||
"temperatureUnit": "C",
|
||||
@@ -31,6 +32,7 @@
|
||||
"currentTemperature": 19.2,
|
||||
"setpoint": 21.0,
|
||||
"thermostatMode": "Program",
|
||||
"hvacAction": "Idle",
|
||||
"minAllowedTemperature": 5.0,
|
||||
"maxAllowedTemperature": 30.0,
|
||||
"temperatureUnit": "C",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"currentTemperature": 20.5,
|
||||
"setpoint": 22.0,
|
||||
"thermostatMode": "Comfort",
|
||||
"hvacAction": "Heating",
|
||||
"minAllowedTemperature": 5.0,
|
||||
"maxAllowedTemperature": 30.0,
|
||||
"temperatureUnit": "C",
|
||||
@@ -31,6 +32,7 @@
|
||||
"currentTemperature": 19.0,
|
||||
"setpoint": 21.0,
|
||||
"thermostatMode": "Program",
|
||||
"hvacAction": "Idle",
|
||||
"minAllowedTemperature": 5.0,
|
||||
"maxAllowedTemperature": 30.0,
|
||||
"temperatureUnit": "C",
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'preset_modes': list([
|
||||
'comfort',
|
||||
'eco',
|
||||
'defrost',
|
||||
'timer',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -38,8 +44,8 @@
|
||||
'platform': 'watts',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'thermostat',
|
||||
'unique_id': 'thermostat_456',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -49,6 +55,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 19.0,
|
||||
'friendly_name': 'Bedroom Thermostat',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
@@ -56,7 +63,14 @@
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'comfort',
|
||||
'eco',
|
||||
'defrost',
|
||||
'timer',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'temperature': 21.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -81,6 +95,12 @@
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'preset_modes': list([
|
||||
'comfort',
|
||||
'eco',
|
||||
'defrost',
|
||||
'timer',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -106,8 +126,8 @@
|
||||
'platform': 'watts',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'translation_key': 'thermostat',
|
||||
'unique_id': 'thermostat_123',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -117,6 +137,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 20.5,
|
||||
'friendly_name': 'Living Room Thermostat',
|
||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
@@ -124,7 +145,14 @@
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'preset_mode': 'comfort',
|
||||
'preset_modes': list([
|
||||
'comfort',
|
||||
'eco',
|
||||
'defrost',
|
||||
'timer',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 17>,
|
||||
'temperature': 22.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
'device_id': 'thermostat_123',
|
||||
'device_name': 'Living Room Thermostat',
|
||||
'device_type': 'thermostat',
|
||||
'hvac_action': 'Idle',
|
||||
'hvac_action': 'Heating',
|
||||
'interface': 'homeassistant.components.THERMOSTAT',
|
||||
'is_online': True,
|
||||
'max_allowed_temperature': 30.0,
|
||||
@@ -131,7 +131,7 @@
|
||||
'device_id': 'thermostat_123',
|
||||
'device_name': 'Living Room Thermostat',
|
||||
'device_type': 'thermostat',
|
||||
'hvac_action': 'Idle',
|
||||
'hvac_action': 'Heating',
|
||||
'interface': 'homeassistant.components.THERMOSTAT',
|
||||
'is_online': True,
|
||||
'max_allowed_temperature': 30.0,
|
||||
|
||||
@@ -10,9 +10,11 @@ from visionpluspython.models import ThermostatMode
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACMode,
|
||||
)
|
||||
@@ -188,6 +190,99 @@ async def test_set_hvac_mode_off(
|
||||
)
|
||||
|
||||
|
||||
async def test_set_preset_mode_comfort(
|
||||
hass: HomeAssistant,
|
||||
mock_watts_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting preset mode to comfort."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "climate.living_room_thermostat",
|
||||
ATTR_PRESET_MODE: "comfort",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_watts_client.set_thermostat_mode.assert_called_once_with(
|
||||
"thermostat_123", ThermostatMode.COMFORT
|
||||
)
|
||||
|
||||
|
||||
async def test_set_preset_mode_defrost(
|
||||
hass: HomeAssistant,
|
||||
mock_watts_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting preset mode to defrost."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "climate.living_room_thermostat",
|
||||
ATTR_PRESET_MODE: "defrost",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_watts_client.set_thermostat_mode.assert_called_once_with(
|
||||
"thermostat_123", ThermostatMode.DEFROST
|
||||
)
|
||||
|
||||
|
||||
async def test_set_preset_mode_timer(
|
||||
hass: HomeAssistant,
|
||||
mock_watts_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting preset mode to timer."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "climate.living_room_thermostat",
|
||||
ATTR_PRESET_MODE: "timer",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_watts_client.set_thermostat_mode.assert_called_once_with(
|
||||
"thermostat_123", ThermostatMode.TIMER
|
||||
)
|
||||
|
||||
|
||||
async def test_set_preset_mode_error(
|
||||
hass: HomeAssistant,
|
||||
mock_watts_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test error handling when setting preset mode fails."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_watts_client.set_thermostat_mode.side_effect = RuntimeError("API Error")
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="An error occurred while setting the preset mode"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "climate.living_room_thermostat",
|
||||
ATTR_PRESET_MODE: "defrost",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_temperature_api_error(
|
||||
hass: HomeAssistant,
|
||||
mock_watts_client: AsyncMock,
|
||||
|
||||
Reference in New Issue
Block a user