Watts: add timer mode service (#169846)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
theobld-ww
2026-05-11 21:18:13 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent ce6b6601fa
commit eeca75b937
8 changed files with 228 additions and 3 deletions
+15 -1
View File
@@ -12,8 +12,13 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, SUPPORTED_DEVICE_TYPES
from .coordinator import (
@@ -21,11 +26,20 @@ from .coordinator import (
WattsVisionDeviceData,
WattsVisionHubCoordinator,
)
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SWITCH]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Watts Vision component."""
async_setup_services(hass)
return True
@dataclass
class WattsVisionRuntimeData:
+44 -1
View File
@@ -1,8 +1,10 @@
"""Climate platform for Watts Vision integration."""
from datetime import timedelta
import logging
from typing import Any
from visionpluspython.exceptions import WattsVisionError
from visionpluspython.models import ThermostatDevice, ThermostatMode
from homeassistant.components.climate import (
@@ -13,7 +15,7 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -194,6 +196,47 @@ class WattsVisionClimate(WattsVisionEntity[ThermostatDevice], ClimateEntity):
await self.coordinator.async_refresh()
async def async_activate_timer_mode(
self, temperature: float, duration: timedelta
) -> None:
"""Activate timer mode with a target temperature and duration."""
if not self._attr_min_temp <= temperature <= self._attr_max_temp:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="timer_temperature_out_of_range",
translation_placeholders={
"temperature": str(temperature),
"min_temp": str(self._attr_min_temp),
"max_temp": str(self._attr_max_temp),
},
)
duration_minutes, remainder = divmod(duration, timedelta(minutes=1))
if remainder:
duration_minutes += 1
try:
await self.coordinator.client.activate_thermostat_timer(
self.device_id, temperature, duration_minutes
)
except (WattsVisionError, ValueError, RuntimeError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="activate_timer_mode_error",
) from err
_LOGGER.debug(
"Successfully activated timer mode: %s%s for %d min on %s",
temperature,
self.temperature_unit,
duration_minutes,
self.device_id,
)
self.coordinator.trigger_fast_polling()
await self.coordinator.async_refresh()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
mode = HVAC_MODE_TO_THERMOSTAT[hvac_mode]
+4
View File
@@ -67,3 +67,7 @@ HVAC_ACTION_TO_HA: dict[str, HVACAction] = {
}
SUPPORTED_DEVICE_TYPES = (ThermostatDevice, SwitchDevice)
# Timer service
SERVICE_ACTIVATE_TIMER_MODE = "activate_timer_mode"
ATTR_DURATION = "duration"
@@ -14,5 +14,10 @@
}
}
}
},
"services": {
"activate_timer_mode": {
"service": "mdi:timer"
}
}
}
@@ -0,0 +1,31 @@
"""Services for Watts Vision integration."""
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import ATTR_DURATION, DOMAIN, SERVICE_ACTIVATE_TIMER_MODE
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for the Watts Vision integration."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ACTIVATE_TIMER_MODE,
entity_domain=CLIMATE_DOMAIN,
schema={
vol.Required(ATTR_TEMPERATURE): vol.Coerce(float),
vol.Required(ATTR_DURATION): vol.All(
cv.time_period,
vol.Range(min=timedelta(minutes=1), max=timedelta(days=1)),
),
},
func="async_activate_timer_mode",
)
@@ -0,0 +1,18 @@
activate_timer_mode:
target:
entity:
domain: climate
integration: watts
fields:
temperature:
required: true
selector:
number:
step: 0.5
unit_of_measurement: "°"
mode: box
duration:
required: true
selector:
duration:
enable_second: false
@@ -45,6 +45,9 @@
}
},
"exceptions": {
"activate_timer_mode_error": {
"message": "An error occurred while activating timer mode"
},
"authentication_failed": {
"message": "Authentication failed"
},
@@ -83,6 +86,25 @@
},
"temporary_connection_error": {
"message": "Temporary connection error"
},
"timer_temperature_out_of_range": {
"message": "Timer temperature {temperature} is out of range ({min_temp}-{max_temp})"
}
},
"services": {
"activate_timer_mode": {
"description": "Activates timer mode on the thermostat for a specified temperature and duration.",
"fields": {
"duration": {
"description": "Duration of the timer.",
"name": "Duration"
},
"temperature": {
"description": "Target temperature while the timer is active.",
"name": "Temperature"
}
},
"name": "Activate timer mode"
}
}
}
+89 -1
View File
@@ -18,9 +18,14 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.components.watts.const import (
ATTR_DURATION,
DOMAIN,
SERVICE_ACTIVATE_TIMER_MODE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
@@ -283,6 +288,89 @@ async def test_set_preset_mode_error(
)
@pytest.mark.parametrize(
("duration", "expected_minutes"),
[
(timedelta(minutes=90), 90),
(timedelta(minutes=1, seconds=30), 2),
],
)
async def test_activate_timer_mode(
hass: HomeAssistant,
mock_watts_client: AsyncMock,
mock_config_entry: MockConfigEntry,
duration: timedelta,
expected_minutes: int,
) -> None:
"""Test activating timer mode with temperature and duration."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
DOMAIN,
SERVICE_ACTIVATE_TIMER_MODE,
{
ATTR_ENTITY_ID: "climate.living_room_thermostat",
ATTR_TEMPERATURE: 20.5,
ATTR_DURATION: duration,
},
blocking=True,
)
mock_watts_client.activate_thermostat_timer.assert_called_once_with(
"thermostat_123", 20.5, expected_minutes
)
@pytest.mark.parametrize("temperature", [4.5, 30.5])
async def test_activate_timer_mode_temperature_out_of_range(
hass: HomeAssistant,
mock_watts_client: AsyncMock,
mock_config_entry: MockConfigEntry,
temperature: float,
) -> None:
"""Test that out-of-range timer temperatures are rejected."""
await setup_integration(hass, mock_config_entry)
with pytest.raises(ServiceValidationError, match="out of range"):
await hass.services.async_call(
DOMAIN,
SERVICE_ACTIVATE_TIMER_MODE,
{
ATTR_ENTITY_ID: "climate.living_room_thermostat",
ATTR_TEMPERATURE: temperature,
ATTR_DURATION: timedelta(minutes=90),
},
blocking=True,
)
mock_watts_client.activate_thermostat_timer.assert_not_called()
async def test_activate_timer_mode_error(
hass: HomeAssistant,
mock_watts_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test error handling when activating timer mode fails."""
await setup_integration(hass, mock_config_entry)
mock_watts_client.activate_thermostat_timer.side_effect = RuntimeError("API Error")
with pytest.raises(
HomeAssistantError, match="An error occurred while activating timer mode"
):
await hass.services.async_call(
DOMAIN,
SERVICE_ACTIVATE_TIMER_MODE,
{
ATTR_ENTITY_ID: "climate.living_room_thermostat",
ATTR_TEMPERATURE: 20.5,
ATTR_DURATION: timedelta(minutes=90),
},
blocking=True,
)
async def test_set_temperature_api_error(
hass: HomeAssistant,
mock_watts_client: AsyncMock,