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",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.7.2"]
"requirements": ["aioairzone==0.7.4"]
}

View File

@ -52,9 +52,11 @@ def get_service(
return None
# Ordered list of URLs
if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]):
_LOGGER.error("Invalid Apprise URL(s) supplied")
return None
if urls := config.get(CONF_URL):
for entry in urls:
if not a_obj.add(entry):
_LOGGER.error("One or more specified Apprise URL(s) are invalid")
return None
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],
)
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])
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])
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])
if ATTR_HUMIDITY in state.attributes:

View File

@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"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",
"iot_class": "cloud_push",
"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
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
else:
self.error = error

View File

@ -1,9 +1,12 @@
"""Support for Enigma2 media players."""
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.enums import RemoteControlCodes, SetVolumeOption
from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
import voluptuous as vol
from yarl import URL
@ -50,6 +53,8 @@ ATTR_MEDIA_DESCRIPTION = "media_description"
ATTR_MEDIA_END_TIME = "media_end_time"
ATTR_MEDIA_START_TIME = "media_start_time"
_LOGGER = getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@ -143,7 +148,12 @@ class Enigma2Device(MediaPlayerEntity):
async def async_turn_off(self) -> None:
"""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:
"""Turn the media player on."""
@ -191,8 +201,19 @@ class Enigma2Device(MediaPlayerEntity):
async def async_update(self) -> None:
"""Update state of the media_player."""
await self._device.update()
self._attr_available = not self._device.is_offline
try:
await self._device.update()
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:
self._attr_extra_state_attributes = {

View File

@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"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,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"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",
"iot_class": "cloud_push",
"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.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.device_registry as dr
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
@ -186,6 +186,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
lutron_client.connect()
_LOGGER.info("Connected to main repeater at %s", host)
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
entry_data = LutronData(
client=lutron_client,
binary_sensors=[],
@ -201,17 +204,39 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
for area in lutron_client.areas:
_LOGGER.debug("Working on area %s", area.name)
for output in area.outputs:
platform = None
_LOGGER.debug("Working on output %s", output.type)
if output.type == "SYSTEM_SHADE":
entry_data.covers.append((area.name, output))
platform = Platform.COVER
elif output.type == "CEILING_FAN_TYPE":
entry_data.fans.append((area.name, output))
platform = Platform.FAN
# Deprecated, should be removed in 2024.8
entry_data.lights.append((area.name, output))
elif output.is_dimmable:
entry_data.lights.append((area.name, output))
platform = Platform.LIGHT
else:
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 button in keypad.buttons:
# 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))
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))
if area.occupancy_group is not None:
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(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, lutron_client.guid)},
@ -247,6 +307,52 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
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:
"""Clean up resources and entities associated with the integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

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

View File

@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push",
"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._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]
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)):
try:
return await host.api.check_new_firmware()
except (ReolinkError, asyncio.exceptions.CancelledError) as err:
task = asyncio.current_task()
if task is not None:
task.uncancel()
except ReolinkError as err:
if starting:
_LOGGER.debug(
"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,
)
# Fetch initial data so we have data when entities subscribe
try:
# If camera WAN blocked, firmware check fails, do not prevent setup
await asyncio.gather(
device_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_config_entry_first_refresh(),
)
except ConfigEntryNotReady:
results = await asyncio.gather(
device_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_config_entry_first_refresh(),
return_exceptions=True,
)
# 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()
raise
raise results[0]
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
host=host,

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"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",
"loggers": ["rokuecp"],
"quality_scale": "silver",
"requirements": ["rokuecp==0.19.0"],
"requirements": ["rokuecp==0.19.1"],
"ssdp": [
{
"st": "roku:ecp",

View File

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

View File

@ -12,6 +12,7 @@ from aiotankerkoenig import (
TankerkoenigConnectionError,
TankerkoenigError,
TankerkoenigInvalidKeyError,
TankerkoenigRateLimitError,
)
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.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
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
@ -78,13 +79,22 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator):
station_ids = list(self.stations)
prices = {}
# The API seems to only return at most 10 results, so split the list in chunks of 10
# and merge it together.
for index in range(ceil(len(station_ids) / 10)):
data = await self._tankerkoenig.prices(
station_ids[index * 10 : (index + 1) * 10]
)
try:
data = await self._tankerkoenig.prices(
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)
return prices

View File

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

View File

@ -3,6 +3,9 @@
import asyncio
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.update_coordinator import CoordinatorEntity
@ -45,11 +48,22 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator
async def wake_up_if_asleep(self) -> None:
"""Wake up the vehicle if its asleep."""
async with self._wakelock:
times = 0
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
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:
"""Return a specific value from coordinator data."""

View File

@ -11,6 +11,7 @@ from tessie_api import (
)
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
@ -112,9 +113,12 @@ class TessieClimateEntity(TessieEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the climate temperature."""
temp = kwargs[ATTR_TEMPERATURE]
await self.run(set_temperature, temperature=temp)
self.set(("climate_state_driver_temp_setting", temp))
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)
self.set(("climate_state_driver_temp_setting", temp))
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the climate mode and state."""

View File

@ -20,6 +20,7 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util.dt import as_utc
from . import TileData
from .const import DOMAIN
@ -145,16 +146,23 @@ class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerE
@callback
def _update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
self._attr_extra_state_attributes.update(
{
ATTR_ALTITUDE: self._tile.altitude,
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_VOIP_STATE: self._tile.voip_state,
}
)
self._attr_extra_state_attributes = {
ATTR_ALTITUDE: self._tile.altitude,
ATTR_IS_LOST: self._tile.lost,
ATTR_RING_STATE: self._tile.ring_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:
"""Handle entity which will be added."""

View File

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

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"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"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
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
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
# cryptography 42.0.0 is installed with botocore
pyOpenSSL>=24.0.0

View File

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

View File

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

View File

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

View File

@ -134,10 +134,6 @@ pubnub!=6.4.0
# https://github.com/dahlia/iso4217/issues/16
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
# cryptography 42.0.0 is installed with botocore
pyOpenSSL>=24.0.0

View File

@ -118,7 +118,48 @@ async def test_apprise_notification(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
# 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(
**{"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}
@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:
"""Test that service call ignores null attributes."""
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE)

View File

@ -5,6 +5,7 @@ from unittest.mock import Mock, patch
import pytest
from homeassistant.components.cloud import CloudNotAvailable
from homeassistant.components.mobile_app.const import (
ATTR_DEVICE_NAME,
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)
@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(
hass: HomeAssistant,
hass_admin_user: MockUser,

View File

@ -98,12 +98,12 @@ def _mocked_discovery(*_):
roomba = RoombaInfo(
hostname="irobot-BLID",
robot_name="robot_name",
robotname="robot_name",
ip=MOCK_IP,
mac="mac",
firmware="firmware",
sw="firmware",
sku="sku",
capabilities="capabilities",
cap={"cap": 1},
)
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
@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)
def mock_request():
"""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 . import assert_entities, setup_platform
from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE
from tests.common import async_fire_time_changed
@ -108,7 +109,11 @@ async def test_errors(
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:
"""Tests asleep is handled."""
@ -123,9 +128,47 @@ async def test_asleep_or_offline(
async_fire_time_changed(hass)
await hass.async_block_till_done()
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(
CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True
)
await hass.async_block_till_done()
mock_wake_up.assert_called_once()

View File

@ -54,14 +54,22 @@ async def test_climate(
with patch(
"homeassistant.components.tessie.climate.set_temperature",
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(
CLIMATE_DOMAIN,
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,
)
mock_set.assert_called_once()
mock_set2.assert_called_once()
state = hass.states.get(entity_id)
assert state.attributes[ATTR_TEMPERATURE] == 20