mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
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:
co-authored by
Copilot Autofix powered by AI
parent
ce6b6601fa
commit
eeca75b937
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user