This commit is contained in:
Franck Nijhof 2024-02-22 16:08:18 +01:00 committed by GitHub
commit 1ee39275fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 469 additions and 106 deletions

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.7.2"] "requirements": ["aioairzone==0.7.4"]
} }

View File

@ -52,8 +52,10 @@ def get_service(
return None return None
# Ordered list of URLs # Ordered list of URLs
if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]): if urls := config.get(CONF_URL):
_LOGGER.error("Invalid Apprise URL(s) supplied") for entry in urls:
if not a_obj.add(entry):
_LOGGER.error("One or more specified Apprise URL(s) are invalid")
return None return None
return AppriseNotificationService(a_obj) return AppriseNotificationService(a_obj)

View File

@ -68,13 +68,22 @@ async def _async_reproduce_states(
[ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW], [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW],
) )
if ATTR_PRESET_MODE in state.attributes: if (
ATTR_PRESET_MODE in state.attributes
and state.attributes[ATTR_PRESET_MODE] is not None
):
await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE]) await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE])
if ATTR_SWING_MODE in state.attributes: if (
ATTR_SWING_MODE in state.attributes
and state.attributes[ATTR_SWING_MODE] is not None
):
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE]) await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
if ATTR_FAN_MODE in state.attributes: if (
ATTR_FAN_MODE in state.attributes
and state.attributes[ATTR_FAN_MODE] is not None
):
await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE]) await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE])
if ATTR_HUMIDITY in state.attributes: if ATTR_HUMIDITY in state.attributes:

View File

@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["deluge_client"], "loggers": ["deluge_client"],
"requirements": ["deluge-client==1.7.1"] "requirements": ["deluge-client==1.10.2"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"], "loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.9", "deebot-client==5.2.1"] "requirements": ["py-sucks==0.9.9", "deebot-client==5.2.2"]
} }

View File

@ -95,7 +95,7 @@ class EcovacsLegacyVacuum(StateVacuumEntity):
This will not change the entity's state. If the error caused the state This will not change the entity's state. If the error caused the state
to change, that will come through as a separate on_status event to change, that will come through as a separate on_status event
""" """
if error == "no_error": if error in ["no_error", sucks.ERROR_CODES["100"]]:
self.error = None self.error = None
else: else:
self.error = error self.error = error

View File

@ -1,9 +1,12 @@
"""Support for Enigma2 media players.""" """Support for Enigma2 media players."""
from __future__ import annotations from __future__ import annotations
from aiohttp.client_exceptions import ClientConnectorError import contextlib
from logging import getLogger
from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedError
from openwebif.api import OpenWebIfDevice from openwebif.api import OpenWebIfDevice
from openwebif.enums import RemoteControlCodes, SetVolumeOption from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
import voluptuous as vol import voluptuous as vol
from yarl import URL from yarl import URL
@ -50,6 +53,8 @@ ATTR_MEDIA_DESCRIPTION = "media_description"
ATTR_MEDIA_END_TIME = "media_end_time" ATTR_MEDIA_END_TIME = "media_end_time"
ATTR_MEDIA_START_TIME = "media_start_time" ATTR_MEDIA_START_TIME = "media_start_time"
_LOGGER = getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -143,7 +148,12 @@ class Enigma2Device(MediaPlayerEntity):
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Turn off media player.""" """Turn off media player."""
await self._device.turn_off() if self._device.turn_off_to_deep:
with contextlib.suppress(ServerDisconnectedError):
await self._device.set_powerstate(PowerState.DEEP_STANDBY)
self._attr_available = False
else:
await self._device.set_powerstate(PowerState.STANDBY)
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the media player on.""" """Turn the media player on."""
@ -191,8 +201,19 @@ class Enigma2Device(MediaPlayerEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update state of the media_player.""" """Update state of the media_player."""
try:
await self._device.update() await self._device.update()
self._attr_available = not self._device.is_offline except ClientConnectorError as err:
if self._attr_available:
_LOGGER.warning(
"%s is unavailable. Error: %s", self._device.base.host, err
)
self._attr_available = False
return
if not self._attr_available:
_LOGGER.debug("%s is available", self._device.base.host)
self._attr_available = True
if not self._device.status.in_standby: if not self._device.status.in_standby:
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {

View File

@ -6,5 +6,5 @@
"dependencies": ["network"], "dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local", "documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["govee-local-api==1.4.1"] "requirements": ["govee-local-api==1.4.4"]
} }

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["holidays==0.42", "babel==2.13.1"] "requirements": ["holidays==0.43", "babel==2.13.1"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/html5", "documentation": "https://www.home-assistant.io/integrations/html5",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["http_ece", "py_vapid", "pywebpush"], "loggers": ["http_ece", "py_vapid", "pywebpush"],
"requirements": ["pywebpush==1.9.2"] "requirements": ["pywebpush==1.14.1"]
} }

View File

@ -16,8 +16,8 @@ from homeassistant.const import (
) )
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify from homeassistant.util import slugify
@ -186,6 +186,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
lutron_client.connect() lutron_client.connect()
_LOGGER.info("Connected to main repeater at %s", host) _LOGGER.info("Connected to main repeater at %s", host)
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
entry_data = LutronData( entry_data = LutronData(
client=lutron_client, client=lutron_client,
binary_sensors=[], binary_sensors=[],
@ -201,17 +204,39 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
for area in lutron_client.areas: for area in lutron_client.areas:
_LOGGER.debug("Working on area %s", area.name) _LOGGER.debug("Working on area %s", area.name)
for output in area.outputs: for output in area.outputs:
platform = None
_LOGGER.debug("Working on output %s", output.type) _LOGGER.debug("Working on output %s", output.type)
if output.type == "SYSTEM_SHADE": if output.type == "SYSTEM_SHADE":
entry_data.covers.append((area.name, output)) entry_data.covers.append((area.name, output))
platform = Platform.COVER
elif output.type == "CEILING_FAN_TYPE": elif output.type == "CEILING_FAN_TYPE":
entry_data.fans.append((area.name, output)) entry_data.fans.append((area.name, output))
platform = Platform.FAN
# Deprecated, should be removed in 2024.8 # Deprecated, should be removed in 2024.8
entry_data.lights.append((area.name, output)) entry_data.lights.append((area.name, output))
elif output.is_dimmable: elif output.is_dimmable:
entry_data.lights.append((area.name, output)) entry_data.lights.append((area.name, output))
platform = Platform.LIGHT
else: else:
entry_data.switches.append((area.name, output)) entry_data.switches.append((area.name, output))
platform = Platform.SWITCH
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
output.uuid,
output.legacy_uuid,
entry_data.client.guid,
)
_async_check_device_identifiers(
hass,
device_registry,
output.uuid,
output.legacy_uuid,
entry_data.client.guid,
)
for keypad in area.keypads: for keypad in area.keypads:
for button in keypad.buttons: for button in keypad.buttons:
# If the button has a function assigned to it, add it as a scene # If the button has a function assigned to it, add it as a scene
@ -228,11 +253,46 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
) )
entry_data.scenes.append((area.name, keypad, button, led)) entry_data.scenes.append((area.name, keypad, button, led))
platform = Platform.SCENE
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
button.uuid,
button.legacy_uuid,
entry_data.client.guid,
)
if led is not None:
platform = Platform.SWITCH
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
led.uuid,
led.legacy_uuid,
entry_data.client.guid,
)
entry_data.buttons.append(LutronButton(hass, area.name, keypad, button)) entry_data.buttons.append(LutronButton(hass, area.name, keypad, button))
if area.occupancy_group is not None: if area.occupancy_group is not None:
entry_data.binary_sensors.append((area.name, area.occupancy_group)) entry_data.binary_sensors.append((area.name, area.occupancy_group))
platform = Platform.BINARY_SENSOR
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
area.occupancy_group.uuid,
area.occupancy_group.legacy_uuid,
entry_data.client.guid,
)
_async_check_device_identifiers(
hass,
device_registry,
area.occupancy_group.uuid,
area.occupancy_group.legacy_uuid,
entry_data.client.guid,
)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id, config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, lutron_client.guid)}, identifiers={(DOMAIN, lutron_client.guid)},
@ -247,6 +307,52 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True return True
def _async_check_entity_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
platform: str,
uuid: str,
legacy_uuid: str,
controller_guid: str,
) -> None:
"""If uuid becomes available update to use it."""
if not uuid:
return
unique_id = f"{controller_guid}_{legacy_uuid}"
entity_id = entity_registry.async_get_entity_id(
domain=platform, platform=DOMAIN, unique_id=unique_id
)
if entity_id:
new_unique_id = f"{controller_guid}_{uuid}"
_LOGGER.debug("Updating entity id from %s to %s", unique_id, new_unique_id)
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
def _async_check_device_identifiers(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
uuid: str,
legacy_uuid: str,
controller_guid: str,
) -> None:
"""If uuid becomes available update to use it."""
if not uuid:
return
unique_id = f"{controller_guid}_{legacy_uuid}"
device = device_registry.async_get_device(identifiers={(DOMAIN, unique_id)})
if device:
new_unique_id = f"{controller_guid}_{uuid}"
_LOGGER.debug("Updating device id from %s to %s", unique_id, new_unique_id)
device_registry.async_update_device(
device.id, new_identifiers={(DOMAIN, new_unique_id)}
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Clean up resources and entities associated with the integration.""" """Clean up resources and entities associated with the integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -41,11 +41,11 @@ class LutronBaseEntity(Entity):
self.schedule_update_ha_state() self.schedule_update_ha_state()
@property @property
def unique_id(self) -> str | None: def unique_id(self) -> str:
"""Return a unique ID.""" """Return a unique ID."""
# Temporary fix for https://github.com/thecynic/pylutron/issues/70
if self._lutron_device.uuid is None: if self._lutron_device.uuid is None:
return None return f"{self._controller.guid}_{self._lutron_device.legacy_uuid}"
return f"{self._controller.guid}_{self._lutron_device.uuid}" return f"{self._controller.guid}_{self._lutron_device.uuid}"
def update(self) -> None: def update(self) -> None:
@ -63,7 +63,7 @@ class LutronDevice(LutronBaseEntity):
"""Initialize the device.""" """Initialize the device."""
super().__init__(area_name, lutron_device, controller) super().__init__(area_name, lutron_device, controller)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, lutron_device.uuid)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Lutron", manufacturer="Lutron",
name=lutron_device.name, name=lutron_device.name,
suggested_area=area_name, suggested_area=area_name,

View File

@ -150,5 +150,5 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
await store.async_save(savable_state(hass)) await store.async_save(savable_state(hass))
if CONF_CLOUDHOOK_URL in entry.data: if CONF_CLOUDHOOK_URL in entry.data:
with suppress(cloud.CloudNotAvailable): with suppress(cloud.CloudNotAvailable, ValueError):
await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])

View File

@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds", "documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["motionblinds"], "loggers": ["motionblinds"],
"requirements": ["motionblinds==0.6.20"] "requirements": ["motionblinds==0.6.21"]
} }

View File

@ -354,9 +354,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._user = self._reauth_entry.data[CONF_USERNAME] self._user = self._reauth_entry.data[CONF_USERNAME]
self._server = self._reauth_entry.data[CONF_HUB] self._server = self._reauth_entry.data[CONF_HUB]
self._api_type = self._reauth_entry.data[CONF_API_TYPE] self._api_type = self._reauth_entry.data.get(CONF_API_TYPE, APIType.CLOUD)
if self._reauth_entry.data[CONF_API_TYPE] == APIType.LOCAL: if self._api_type == APIType.LOCAL:
self._host = self._reauth_entry.data[CONF_HOST] self._host = self._reauth_entry.data[CONF_HOST]
return await self.async_step_user(dict(entry_data)) return await self.async_step_user(dict(entry_data))

View File

@ -100,10 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
try: try:
return await host.api.check_new_firmware() return await host.api.check_new_firmware()
except (ReolinkError, asyncio.exceptions.CancelledError) as err: except ReolinkError as err:
task = asyncio.current_task()
if task is not None:
task.uncancel()
if starting: if starting:
_LOGGER.debug( _LOGGER.debug(
"Error checking Reolink firmware update at startup " "Error checking Reolink firmware update at startup "
@ -133,15 +130,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
update_interval=FIRMWARE_UPDATE_INTERVAL, update_interval=FIRMWARE_UPDATE_INTERVAL,
) )
# Fetch initial data so we have data when entities subscribe # Fetch initial data so we have data when entities subscribe
try: results = await asyncio.gather(
# If camera WAN blocked, firmware check fails, do not prevent setup
await asyncio.gather(
device_coordinator.async_config_entry_first_refresh(), device_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_config_entry_first_refresh(), firmware_coordinator.async_config_entry_first_refresh(),
return_exceptions=True,
) )
except ConfigEntryNotReady: # If camera WAN blocked, firmware check fails, do not prevent setup
# so don't check firmware_coordinator exceptions
if isinstance(results[0], BaseException):
await host.stop() await host.stop()
raise raise results[0]
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
host=host, host=host,

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.8.7"] "requirements": ["reolink-aio==0.8.8"]
} }

View File

@ -11,7 +11,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["rokuecp"], "loggers": ["rokuecp"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["rokuecp==0.19.0"], "requirements": ["rokuecp==0.19.1"],
"ssdp": [ "ssdp": [
{ {
"st": "roku:ecp", "st": "roku:ecp",

View File

@ -24,7 +24,7 @@
"documentation": "https://www.home-assistant.io/integrations/roomba", "documentation": "https://www.home-assistant.io/integrations/roomba",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["paho_mqtt", "roombapy"], "loggers": ["paho_mqtt", "roombapy"],
"requirements": ["roombapy==1.6.10"], "requirements": ["roombapy==1.6.12"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_amzn-alexa._tcp.local.", "type": "_amzn-alexa._tcp.local.",

View File

@ -12,6 +12,7 @@ from aiotankerkoenig import (
TankerkoenigConnectionError, TankerkoenigConnectionError,
TankerkoenigError, TankerkoenigError,
TankerkoenigInvalidKeyError, TankerkoenigInvalidKeyError,
TankerkoenigRateLimitError,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -19,7 +20,7 @@ from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_FUEL_TYPES, CONF_STATIONS from .const import CONF_FUEL_TYPES, CONF_STATIONS
@ -78,13 +79,22 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator):
station_ids = list(self.stations) station_ids = list(self.stations)
prices = {} prices = {}
# The API seems to only return at most 10 results, so split the list in chunks of 10 # The API seems to only return at most 10 results, so split the list in chunks of 10
# and merge it together. # and merge it together.
for index in range(ceil(len(station_ids) / 10)): for index in range(ceil(len(station_ids) / 10)):
try:
data = await self._tankerkoenig.prices( data = await self._tankerkoenig.prices(
station_ids[index * 10 : (index + 1) * 10] station_ids[index * 10 : (index + 1) * 10]
) )
except TankerkoenigInvalidKeyError as err:
raise ConfigEntryAuthFailed(err) from err
except (TankerkoenigError, TankerkoenigConnectionError) as err:
if isinstance(err, TankerkoenigRateLimitError):
_LOGGER.warning(
"API rate limit reached, consider to increase polling interval"
)
raise UpdateFailed(err) from err
prices.update(data) prices.update(data)
return prices return prices

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "documentation": "https://www.home-assistant.io/integrations/tankerkoenig",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aiotankerkoenig"], "loggers": ["aiotankerkoenig"],
"requirements": ["aiotankerkoenig==0.3.0"] "requirements": ["aiotankerkoenig==0.4.1"]
} }

View File

@ -3,6 +3,9 @@
import asyncio import asyncio
from typing import Any from typing import Any
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -45,11 +48,22 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator
async def wake_up_if_asleep(self) -> None: async def wake_up_if_asleep(self) -> None:
"""Wake up the vehicle if its asleep.""" """Wake up the vehicle if its asleep."""
async with self._wakelock: async with self._wakelock:
times = 0
while self.coordinator.data["state"] != TeslemetryState.ONLINE: while self.coordinator.data["state"] != TeslemetryState.ONLINE:
state = (await self.api.wake_up())["response"]["state"] try:
if times == 0:
cmd = await self.api.wake_up()
else:
cmd = await self.api.vehicle()
state = cmd["response"]["state"]
except TeslaFleetError as e:
raise HomeAssistantError(str(e)) from e
self.coordinator.data["state"] = state self.coordinator.data["state"] = state
if state != TeslemetryState.ONLINE: if state != TeslemetryState.ONLINE:
await asyncio.sleep(5) times += 1
if times >= 4: # Give up after 30 seconds total
raise HomeAssistantError("Could not wake up vehicle")
await asyncio.sleep(times * 5)
def get(self, key: str | None = None, default: Any | None = None) -> Any: def get(self, key: str | None = None, default: Any | None = None) -> Any:
"""Return a specific value from coordinator data.""" """Return a specific value from coordinator data."""

View File

@ -11,6 +11,7 @@ from tessie_api import (
) )
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
@ -112,7 +113,10 @@ class TessieClimateEntity(TessieEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the climate temperature.""" """Set the climate temperature."""
temp = kwargs[ATTR_TEMPERATURE] if mode := kwargs.get(ATTR_HVAC_MODE):
await self.async_set_hvac_mode(mode)
if temp := kwargs.get(ATTR_TEMPERATURE):
await self.run(set_temperature, temperature=temp) await self.run(set_temperature, temperature=temp)
self.set(("climate_state_driver_temp_setting", temp)) self.set(("climate_state_driver_temp_setting", temp))

View File

@ -20,6 +20,7 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from homeassistant.util.dt import as_utc
from . import TileData from . import TileData
from .const import DOMAIN from .const import DOMAIN
@ -145,15 +146,22 @@ class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerE
@callback @callback
def _update_from_latest_data(self) -> None: def _update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
self._attr_extra_state_attributes.update( self._attr_extra_state_attributes = {
{
ATTR_ALTITUDE: self._tile.altitude, ATTR_ALTITUDE: self._tile.altitude,
ATTR_IS_LOST: self._tile.lost, ATTR_IS_LOST: self._tile.lost,
ATTR_LAST_LOST_TIMESTAMP: self._tile.lost_timestamp,
ATTR_LAST_TIMESTAMP: self._tile.last_timestamp,
ATTR_RING_STATE: self._tile.ring_state, ATTR_RING_STATE: self._tile.ring_state,
ATTR_VOIP_STATE: self._tile.voip_state, ATTR_VOIP_STATE: self._tile.voip_state,
} }
for timestamp_attr in (
(ATTR_LAST_LOST_TIMESTAMP, self._tile.lost_timestamp),
(ATTR_LAST_TIMESTAMP, self._tile.last_timestamp),
):
if not timestamp_attr[1]:
# If the API doesn't return a value for a particular timestamp
# attribute, skip it:
continue
self._attr_extra_state_attributes[timestamp_attr[0]] = as_utc(
timestamp_attr[1]
) )
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:

View File

@ -78,7 +78,7 @@ class ProtectLight(ProtectDeviceEntity, LightEntity):
is a change. is a change.
""" """
return (self._attr_available, self._attr_brightness) return (self._attr_available, self._attr_is_on, self._attr_brightness)
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["holidays"], "loggers": ["holidays"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["holidays==0.42"] "requirements": ["holidays==0.43"]
} }

View File

@ -16,7 +16,7 @@ from .helpers.deprecation import (
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 2 MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -141,10 +141,6 @@ pubnub!=6.4.0
# https://github.com/dahlia/iso4217/issues/16 # https://github.com/dahlia/iso4217/issues/16
iso4217!=1.10.20220401 iso4217!=1.10.20220401
# Matplotlib 3.6.2 has issues building wheels on armhf/armv7
# We need at least >=2.1.0 (tensorflow integration -> pycocotools)
matplotlib==3.6.1
# pyOpenSSL 24.0.0 or later required to avoid import errors when # pyOpenSSL 24.0.0 or later required to avoid import errors when
# cryptography 42.0.0 is installed with botocore # cryptography 42.0.0 is installed with botocore
pyOpenSSL>=24.0.0 pyOpenSSL>=24.0.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.2.2" version = "2024.2.3"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -191,7 +191,7 @@ aioairq==0.3.2
aioairzone-cloud==0.3.8 aioairzone-cloud==0.3.8
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.7.2 aioairzone==0.7.4
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2024.01.0 aioambient==2024.01.0
@ -377,7 +377,7 @@ aioswitcher==3.4.1
aiosyncthing==0.5.1 aiosyncthing==0.5.1
# homeassistant.components.tankerkoenig # homeassistant.components.tankerkoenig
aiotankerkoenig==0.3.0 aiotankerkoenig==0.4.1
# homeassistant.components.tractive # homeassistant.components.tractive
aiotractive==0.5.6 aiotractive==0.5.6
@ -687,7 +687,7 @@ debugpy==1.8.0
# decora==0.6 # decora==0.6
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
deebot-client==5.2.1 deebot-client==5.2.2
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
@ -695,7 +695,7 @@ deebot-client==5.2.1
defusedxml==0.7.1 defusedxml==0.7.1
# homeassistant.components.deluge # homeassistant.components.deluge
deluge-client==1.7.1 deluge-client==1.10.2
# homeassistant.components.lametric # homeassistant.components.lametric
demetriek==0.4.0 demetriek==0.4.0
@ -967,7 +967,7 @@ gotailwind==0.2.2
govee-ble==0.31.0 govee-ble==0.31.0
# homeassistant.components.govee_light_local # homeassistant.components.govee_light_local
govee-local-api==1.4.1 govee-local-api==1.4.4
# homeassistant.components.remote_rpi_gpio # homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2 gpiozero==1.6.2
@ -1059,7 +1059,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.42 holidays==0.43
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20240207.1 home-assistant-frontend==20240207.1
@ -1313,7 +1313,7 @@ moehlenhoff-alpha2==1.3.0
mopeka-iot-ble==0.5.0 mopeka-iot-ble==0.5.0
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.6.20 motionblinds==0.6.21
# homeassistant.components.motioneye # homeassistant.components.motioneye
motioneye-client==0.3.14 motioneye-client==0.3.14
@ -2360,7 +2360,7 @@ pywaze==0.5.1
pyweatherflowudp==1.4.5 pyweatherflowudp==1.4.5
# homeassistant.components.html5 # homeassistant.components.html5
pywebpush==1.9.2 pywebpush==1.14.1
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==1.4.0 pywemo==1.4.0
@ -2423,7 +2423,7 @@ renault-api==0.2.1
renson-endura-delta==1.7.1 renson-endura-delta==1.7.1
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.8.7 reolink-aio==0.8.8
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -2444,13 +2444,13 @@ rjpl==0.3.6
rocketchat-API==0.6.1 rocketchat-API==0.6.1
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.19.0 rokuecp==0.19.1
# homeassistant.components.romy # homeassistant.components.romy
romy==0.0.7 romy==0.0.7
# homeassistant.components.roomba # homeassistant.components.roomba
roombapy==1.6.10 roombapy==1.6.12
# homeassistant.components.roon # homeassistant.components.roon
roonapi==0.1.6 roonapi==0.1.6

View File

@ -170,7 +170,7 @@ aioairq==0.3.2
aioairzone-cloud==0.3.8 aioairzone-cloud==0.3.8
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.7.2 aioairzone==0.7.4
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2024.01.0 aioambient==2024.01.0
@ -350,7 +350,7 @@ aioswitcher==3.4.1
aiosyncthing==0.5.1 aiosyncthing==0.5.1
# homeassistant.components.tankerkoenig # homeassistant.components.tankerkoenig
aiotankerkoenig==0.3.0 aiotankerkoenig==0.4.1
# homeassistant.components.tractive # homeassistant.components.tractive
aiotractive==0.5.6 aiotractive==0.5.6
@ -562,7 +562,7 @@ dbus-fast==2.21.1
debugpy==1.8.0 debugpy==1.8.0
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
deebot-client==5.2.1 deebot-client==5.2.2
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
@ -570,7 +570,7 @@ deebot-client==5.2.1
defusedxml==0.7.1 defusedxml==0.7.1
# homeassistant.components.deluge # homeassistant.components.deluge
deluge-client==1.7.1 deluge-client==1.10.2
# homeassistant.components.lametric # homeassistant.components.lametric
demetriek==0.4.0 demetriek==0.4.0
@ -784,7 +784,7 @@ gotailwind==0.2.2
govee-ble==0.31.0 govee-ble==0.31.0
# homeassistant.components.govee_light_local # homeassistant.components.govee_light_local
govee-local-api==1.4.1 govee-local-api==1.4.4
# homeassistant.components.gpsd # homeassistant.components.gpsd
gps3==0.33.3 gps3==0.33.3
@ -855,7 +855,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.42 holidays==0.43
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20240207.1 home-assistant-frontend==20240207.1
@ -1049,7 +1049,7 @@ moehlenhoff-alpha2==1.3.0
mopeka-iot-ble==0.5.0 mopeka-iot-ble==0.5.0
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.6.20 motionblinds==0.6.21
# homeassistant.components.motioneye # homeassistant.components.motioneye
motioneye-client==0.3.14 motioneye-client==0.3.14
@ -1809,7 +1809,7 @@ pywaze==0.5.1
pyweatherflowudp==1.4.5 pyweatherflowudp==1.4.5
# homeassistant.components.html5 # homeassistant.components.html5
pywebpush==1.9.2 pywebpush==1.14.1
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==1.4.0 pywemo==1.4.0
@ -1857,7 +1857,7 @@ renault-api==0.2.1
renson-endura-delta==1.7.1 renson-endura-delta==1.7.1
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.8.7 reolink-aio==0.8.8
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.65 rflink==0.0.65
@ -1866,13 +1866,13 @@ rflink==0.0.65
ring-doorbell[listen]==0.8.7 ring-doorbell[listen]==0.8.7
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.19.0 rokuecp==0.19.1
# homeassistant.components.romy # homeassistant.components.romy
romy==0.0.7 romy==0.0.7
# homeassistant.components.roomba # homeassistant.components.roomba
roombapy==1.6.10 roombapy==1.6.12
# homeassistant.components.roon # homeassistant.components.roon
roonapi==0.1.6 roonapi==0.1.6

View File

@ -134,10 +134,6 @@ pubnub!=6.4.0
# https://github.com/dahlia/iso4217/issues/16 # https://github.com/dahlia/iso4217/issues/16
iso4217!=1.10.20220401 iso4217!=1.10.20220401
# Matplotlib 3.6.2 has issues building wheels on armhf/armv7
# We need at least >=2.1.0 (tensorflow integration -> pycocotools)
matplotlib==3.6.1
# pyOpenSSL 24.0.0 or later required to avoid import errors when # pyOpenSSL 24.0.0 or later required to avoid import errors when
# cryptography 42.0.0 is installed with botocore # cryptography 42.0.0 is installed with botocore
pyOpenSSL>=24.0.0 pyOpenSSL>=24.0.0

View File

@ -118,7 +118,48 @@ async def test_apprise_notification(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
# Validate calls were made under the hood correctly # Validate calls were made under the hood correctly
obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]]) obj.add.assert_called_once_with(config[BASE_COMPONENT]["url"])
obj.notify.assert_called_once_with(
**{"body": data["message"], "title": data["title"], "tag": None}
)
async def test_apprise_multiple_notification(hass: HomeAssistant) -> None:
"""Test apprise notification."""
config = {
BASE_COMPONENT: {
"name": "test",
"platform": "apprise",
"url": [
"mailto://user:pass@example.com, mailto://user:pass@gmail.com",
"json://user:pass@gmail.com",
],
}
}
# Our Message
data = {"title": "Test Title", "message": "Test Message"}
with patch(
"homeassistant.components.apprise.notify.apprise.Apprise"
) as mock_apprise:
obj = MagicMock()
obj.add.return_value = True
obj.notify.return_value = True
mock_apprise.return_value = obj
assert await async_setup_component(hass, BASE_COMPONENT, config)
await hass.async_block_till_done()
# Test the existence of our service
assert hass.services.has_service(BASE_COMPONENT, "test")
# Test the call to our underlining notify() call
await hass.services.async_call(BASE_COMPONENT, "test", data)
await hass.async_block_till_done()
# Validate 2 calls were made under the hood
assert obj.add.call_count == 2
obj.notify.assert_called_once_with( obj.notify.assert_called_once_with(
**{"body": data["message"], "title": data["title"], "tag": None} **{"body": data["message"], "title": data["title"], "tag": None}
) )

View File

@ -119,6 +119,25 @@ async def test_attribute(hass: HomeAssistant, service, attribute) -> None:
assert calls_1[0].data == {"entity_id": ENTITY_1, attribute: value} assert calls_1[0].data == {"entity_id": ENTITY_1, attribute: value}
@pytest.mark.parametrize(
("service", "attribute"),
[
(SERVICE_SET_PRESET_MODE, ATTR_PRESET_MODE),
(SERVICE_SET_SWING_MODE, ATTR_SWING_MODE),
(SERVICE_SET_FAN_MODE, ATTR_FAN_MODE),
],
)
async def test_attribute_with_none(hass: HomeAssistant, service, attribute) -> None:
"""Test that service call is not made for attributes with None value."""
calls_1 = async_mock_service(hass, DOMAIN, service)
await async_reproduce_states(hass, [State(ENTITY_1, None, {attribute: None})])
await hass.async_block_till_done()
assert len(calls_1) == 0
async def test_attribute_partial_temperature(hass: HomeAssistant) -> None: async def test_attribute_partial_temperature(hass: HomeAssistant) -> None:
"""Test that service call ignores null attributes.""" """Test that service call ignores null attributes."""
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE)

View File

@ -5,6 +5,7 @@ from unittest.mock import Mock, patch
import pytest import pytest
from homeassistant.components.cloud import CloudNotAvailable
from homeassistant.components.mobile_app.const import ( from homeassistant.components.mobile_app.const import (
ATTR_DEVICE_NAME, ATTR_DEVICE_NAME,
CONF_CLOUDHOOK_URL, CONF_CLOUDHOOK_URL,
@ -118,6 +119,32 @@ async def test_create_cloud_hook_on_setup(
await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps) await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps)
@pytest.mark.parametrize("exception", (CloudNotAvailable, ValueError))
async def test_remove_cloudhook(
hass: HomeAssistant,
hass_admin_user: MockUser,
caplog: pytest.LogCaptureFixture,
exception: Exception,
) -> None:
"""Test removing a cloud hook when config entry is removed."""
async def additional_steps(
config_entry: ConfigEntry, mock_create_cloudhook: Mock, cloud_hook: str
) -> None:
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
assert config_entry.data[CONF_CLOUDHOOK_URL] == cloud_hook
with patch(
"homeassistant.components.cloud.async_delete_cloudhook",
side_effect=exception,
) as delete_cloudhook:
await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
delete_cloudhook.assert_called_once_with(hass, webhook_id)
assert str(exception) not in caplog.text
await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps)
async def test_create_cloud_hook_aleady_exists( async def test_create_cloud_hook_aleady_exists(
hass: HomeAssistant, hass: HomeAssistant,
hass_admin_user: MockUser, hass_admin_user: MockUser,

View File

@ -98,12 +98,12 @@ def _mocked_discovery(*_):
roomba = RoombaInfo( roomba = RoombaInfo(
hostname="irobot-BLID", hostname="irobot-BLID",
robot_name="robot_name", robotname="robot_name",
ip=MOCK_IP, ip=MOCK_IP,
mac="mac", mac="mac",
firmware="firmware", sw="firmware",
sku="sku", sku="sku",
capabilities="capabilities", cap={"cap": 1},
) )
roomba_discovery.get_all = MagicMock(return_value=[roomba]) roomba_discovery.get_all = MagicMock(return_value=[roomba])

View File

@ -0,0 +1,51 @@
"""Tests for the Tankerkoening integration."""
from __future__ import annotations
from datetime import timedelta
from unittest.mock import AsyncMock
from aiotankerkoenig.exceptions import TankerkoenigRateLimitError
import pytest
from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.usefixtures("setup_integration")
async def test_rate_limit(
hass: HomeAssistant,
config_entry: MockConfigEntry,
tankerkoenig: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test detection of API rate limit."""
assert config_entry.state == ConfigEntryState.LOADED
state = hass.states.get("binary_sensor.station_somewhere_street_1_status")
assert state
assert state.state == "on"
tankerkoenig.prices.side_effect = TankerkoenigRateLimitError
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_SCAN_INTERVAL)
)
await hass.async_block_till_done()
assert (
"API rate limit reached, consider to increase polling interval" in caplog.text
)
state = hass.states.get("binary_sensor.station_somewhere_street_1_status")
assert state
assert state.state == STATE_UNAVAILABLE
tankerkoenig.prices.side_effect = None
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_SCAN_INTERVAL * 2)
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.station_somewhere_street_1_status")
assert state
assert state.state == "on"

View File

@ -37,6 +37,16 @@ def mock_wake_up():
yield mock_wake_up yield mock_wake_up
@pytest.fixture(autouse=True)
def mock_vehicle():
"""Mock Tesla Fleet API Vehicle Specific vehicle method."""
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.vehicle",
return_value=WAKE_UP_ONLINE,
) as mock_vehicle:
yield mock_vehicle
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_request(): def mock_request():
"""Mock Tesla Fleet API Vehicle Specific class.""" """Mock Tesla Fleet API Vehicle Specific class."""

View File

@ -26,6 +26,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import assert_entities, setup_platform from . import assert_entities, setup_platform
from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -108,7 +109,11 @@ async def test_errors(
async def test_asleep_or_offline( async def test_asleep_or_offline(
hass: HomeAssistant, mock_vehicle_data, freezer: FrozenDateTimeFactory hass: HomeAssistant,
mock_vehicle_data,
mock_wake_up,
mock_vehicle,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Tests asleep is handled.""" """Tests asleep is handled."""
@ -123,9 +128,47 @@ async def test_asleep_or_offline(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_vehicle_data.assert_called_once() mock_vehicle_data.assert_called_once()
mock_wake_up.reset_mock()
# Run a command that will wake up the vehicle, but not immediately # Run a command but fail trying to wake up the vehicle
mock_wake_up.side_effect = InvalidCommand
with pytest.raises(HomeAssistantError) as error:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
assert error
mock_wake_up.assert_called_once()
mock_wake_up.side_effect = None
mock_wake_up.reset_mock()
# Run a command but timeout trying to wake up the vehicle
mock_wake_up.return_value = WAKE_UP_ASLEEP
mock_vehicle.return_value = WAKE_UP_ASLEEP
with patch(
"homeassistant.components.teslemetry.entity.asyncio.sleep"
), pytest.raises(HomeAssistantError) as error:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
assert error
mock_wake_up.assert_called_once()
mock_vehicle.assert_called()
mock_wake_up.reset_mock()
mock_vehicle.reset_mock()
mock_wake_up.return_value = WAKE_UP_ONLINE
mock_vehicle.return_value = WAKE_UP_ONLINE
# Run a command and wake up the vehicle immediately
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_wake_up.assert_called_once()

View File

@ -54,14 +54,22 @@ async def test_climate(
with patch( with patch(
"homeassistant.components.tessie.climate.set_temperature", "homeassistant.components.tessie.climate.set_temperature",
return_value=TEST_RESPONSE, return_value=TEST_RESPONSE,
) as mock_set: ) as mock_set, patch(
"homeassistant.components.tessie.climate.start_climate_preconditioning",
return_value=TEST_RESPONSE,
) as mock_set2:
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20}, {
ATTR_ENTITY_ID: [entity_id],
ATTR_HVAC_MODE: HVACMode.HEAT_COOL,
ATTR_TEMPERATURE: 20,
},
blocking=True, blocking=True,
) )
mock_set.assert_called_once() mock_set.assert_called_once()
mock_set2.assert_called_once()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.attributes[ATTR_TEMPERATURE] == 20 assert state.attributes[ATTR_TEMPERATURE] == 20