Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/nest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
async_get_media_source_devices,
async_get_transcoder,
)
from .services import async_setup_services
from .types import DevicesAddedListener, NestConfigEntry, NestData

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -115,6 +116,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Nest components with dispatch between old/new flows."""
hass.http.register_view(NestEventMediaView(hass))
hass.http.register_view(NestEventMediaThumbnailView(hass))
async_setup_services(hass)
return True


Expand Down
27 changes: 26 additions & 1 deletion homeassistant/components/nest/climate.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Support for Google Nest SDM climate devices."""

from datetime import timedelta
from typing import Any, cast

from google_nest_sdm.device import Device
Expand Down Expand Up @@ -67,7 +68,7 @@
FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()}
FAN_INV_MODES = list(FAN_INV_MODE_MAP)

MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API
MAX_FAN_DURATION = 43200 # 12 hours is the max in the SDM API
MIN_TEMP = 10
MAX_TEMP = 32
MIN_TEMP_RANGE = 1.66667
Expand Down Expand Up @@ -344,3 +345,27 @@ async def async_set_fan_mode(self, fan_mode: str) -> None:
raise HomeAssistantError(
f"Error setting {self.entity_id} fan mode to {fan_mode}: {err}"
) from err

async def async_set_fan_timer(self, duration: timedelta) -> None:
"""Set a short term fan timer."""
if not self.supported_features & ClimateEntityFeature.FAN_MODE:
raise HomeAssistantError(f"Entity {self.entity_id} does not support fan")

if self.hvac_mode == HVACMode.OFF:
raise HomeAssistantError(
f"Cannot turn on fan for {self.entity_id}, please set an HVAC mode (e.g. heat/cool) first"
)

seconds = int(duration.total_seconds())
if seconds <= 0 or seconds > MAX_FAN_DURATION:
raise ValueError(
f"Duration {seconds} for {self.entity_id} must be between 1 and {MAX_FAN_DURATION} seconds"
)

trait = self._device.traits[FanTrait.NAME]
try:
await trait.set_timer(FAN_INV_MODE_MAP[FAN_ON], duration=seconds)
except ApiException as err:
raise HomeAssistantError(
f"Error setting {self.entity_id} fan timer: {err}"
) from err
7 changes: 7 additions & 0 deletions homeassistant/components/nest/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"services": {
"set_fan_timer": {
"service": "mdi:fan-clock"
}
}
}
30 changes: 30 additions & 0 deletions homeassistant/components/nest/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Define services for the Nest integration."""

import voluptuous as vol

from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
ClimateEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service

from .const import DOMAIN

SERVICE_SET_FAN_TIMER = "set_fan_timer"


@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for the Nest integration."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_FAN_TIMER,
entity_domain=CLIMATE_DOMAIN,
schema={
vol.Required("duration"): cv.time_period,
},
func="async_set_fan_timer",
required_features=[ClimateEntityFeature.FAN_MODE],
)
12 changes: 12 additions & 0 deletions homeassistant/components/nest/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
set_fan_timer:
target:
entity:
domain: climate
integration: nest
supported_features:
- climate.ClimateEntityFeature.FAN_MODE
fields:
duration:
required: true
selector:
duration:
12 changes: 12 additions & 0 deletions homeassistant/components/nest/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,17 @@
"create_new_topic": "Create new topic"
}
}
},
"services": {
"set_fan_timer": {
"description": "Sets the fan to run for a specific duration.",
"fields": {
"duration": {
"description": "The duration the fan should run for.",
"name": "Duration"
}
},
"name": "Set fan timer"
}
}
}
19 changes: 16 additions & 3 deletions homeassistant/components/weatherflow_cloud/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,22 @@ class WeatherFlowCloudSensorREST(WeatherFlowSensorBase):

coordinator: WeatherFlowCloudUpdateCoordinatorREST

@property
def _observation(self) -> Observation | None:
"""Return the current station observation."""
observations = self.coordinator.data[self.station_id].observation.obs
if not observations:
return None
return observations[0]

@property
def available(self) -> bool:
"""Get if available."""
return super().available and self._observation is not None

@property
def native_value(self) -> StateType | datetime:
"""Return the native value."""
return self.entity_description.value_fn(
self.coordinator.data[self.station_id].observation.obs[0]
)
if (observation := self._observation) is None:
return None
return self.entity_description.value_fn(observation)
2 changes: 1 addition & 1 deletion homeassistant/components/wmspro/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"documentation": "https://www.home-assistant.io/integrations/wmspro",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["pywmspro==0.3.3"]
"requirements": ["pywmspro==0.3.4"]
}
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

192 changes: 191 additions & 1 deletion tests/components/nest/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotSupported,
ServiceValidationError,
)

from .common import (
DEVICE_COMMAND,
DEVICE_ID,
DOMAIN,
CreateDevice,
PlatformSetup,
create_nest_event,
Expand Down Expand Up @@ -1085,6 +1090,191 @@ async def test_thermostat_set_fan(
}


async def test_set_fan_timer(
hass: HomeAssistant,
setup_platform: PlatformSetup,
auth: FakeAuth,
create_device: CreateDevice,
) -> None:
"""Test the set_fan_timer service."""
create_device.create(
{
"sdm.devices.traits.Fan": {
"timerMode": "OFF",
},
"sdm.devices.traits.ThermostatHvac": {"status": "HEATING"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "OFF"],
"mode": "HEAT",
},
"sdm.devices.traits.TemperatureTrait": {"ambientTemperatureCelsius": 20.0},
}
)
await setup_platform()

# Call the set_fan_timer service
await hass.services.async_call(
DOMAIN,
"set_fan_timer",
{
"entity_id": "climate.my_thermostat",
"duration": {"minutes": 15},
},
blocking=True,
)

assert auth.method == "post"
assert auth.url == DEVICE_COMMAND
assert auth.json == {
"command": "sdm.devices.commands.Fan.SetTimer",
"params": {
"duration": "900s",
"timerMode": "ON",
},
}


async def test_set_fan_timer_hvac_off(
hass: HomeAssistant,
setup_platform: PlatformSetup,
auth: FakeAuth,
create_device: CreateDevice,
) -> None:
"""Test the set_fan_timer service when HVAC is off."""
create_device.create(
{
"sdm.devices.traits.Fan": {
"timerMode": "OFF",
},
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "OFF"],
"mode": "OFF",
},
}
)
await setup_platform()

with pytest.raises(HomeAssistantError, match="Cannot turn on fan"):
await hass.services.async_call(
DOMAIN,
"set_fan_timer",
{
"entity_id": "climate.my_thermostat",
"duration": {"minutes": 15},
},
blocking=True,
)


async def test_set_fan_timer_no_fan(
hass: HomeAssistant,
setup_platform: PlatformSetup,
create_device: CreateDevice,
) -> None:
"""Test the set_fan_timer service when device does not support fan."""
create_device.create(
{
"sdm.devices.traits.ThermostatHvac": {"status": "HEATING"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "OFF"],
"mode": "HEAT",
},
}
)
await setup_platform()

with pytest.raises(ServiceNotSupported, match="does not support action"):
await hass.services.async_call(
DOMAIN,
"set_fan_timer",
{
"entity_id": "climate.my_thermostat",
"duration": {"minutes": 15},
},
blocking=True,
)


async def test_set_fan_timer_api_error(
hass: HomeAssistant,
setup_platform: PlatformSetup,
auth: FakeAuth,
create_device: CreateDevice,
) -> None:
"""Test the set_fan_timer service with an API error."""
create_device.create(
{
"sdm.devices.traits.Fan": {
"timerMode": "OFF",
},
"sdm.devices.traits.ThermostatHvac": {"status": "HEATING"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "OFF"],
"mode": "HEAT",
},
}
)
await setup_platform()

auth.responses = [aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)]
with pytest.raises(HomeAssistantError, match="Error setting .* fan timer"):
await hass.services.async_call(
DOMAIN,
"set_fan_timer",
{
"entity_id": "climate.my_thermostat",
"duration": {"minutes": 15},
},
blocking=True,
)


async def test_set_fan_timer_invalid_duration(
hass: HomeAssistant,
setup_platform: PlatformSetup,
create_device: CreateDevice,
) -> None:
"""Test the set_fan_timer service with invalid durations."""
create_device.create(
{
"sdm.devices.traits.Fan": {
"timerMode": "OFF",
},
"sdm.devices.traits.ThermostatHvac": {"status": "HEATING"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "OFF"],
"mode": "HEAT",
},
}
)
await setup_platform()

# Duration too long (over 12 hours)
with pytest.raises(ValueError, match="must be between 1 and 43200 seconds"):
await hass.services.async_call(
DOMAIN,
"set_fan_timer",
{
"entity_id": "climate.my_thermostat",
"duration": {"hours": 16},
},
blocking=True,
)

# Duration zero
with pytest.raises(ValueError, match="must be between 1 and 43200 seconds"):
await hass.services.async_call(
DOMAIN,
"set_fan_timer",
{
"entity_id": "climate.my_thermostat",
"duration": {"seconds": 0},
},
blocking=True,
)


async def test_thermostat_set_fan_when_off(
hass: HomeAssistant,
setup_platform: PlatformSetup,
Expand Down
Loading