This commit is contained in:
Franck Nijhof 2024-04-23 21:50:08 +02:00 committed by GitHub
commit 60be2af8ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 936 additions and 286 deletions

View File

@ -157,3 +157,11 @@ class AirthingsHeaterEnergySensor(
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
return self.coordinator.data[self._id].sensors[self.entity_description.key] # type: ignore[no-any-return]
@property
def available(self) -> bool:
"""Check if device and sensor is available in data."""
return (
super().available
and self.entity_description.key in self.coordinator.data[self._id].sensors
)

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/doods",
"iot_class": "local_polling",
"loggers": ["pydoods"],
"requirements": ["pydoods==1.0.2", "Pillow==10.2.0"]
"requirements": ["pydoods==1.0.2", "Pillow==10.3.0"]
}

View File

@ -6,5 +6,5 @@
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/generic",
"iot_class": "local_push",
"requirements": ["ha-av==10.1.1", "Pillow==10.2.0"]
"requirements": ["ha-av==10.1.1", "Pillow==10.3.0"]
}

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==7.0.3"]
"requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.0"]
}

View File

@ -93,7 +93,7 @@ BUTTON_EDIT = {
}
validate_addr = cv.matches_regex(r"\[\d\d:\d\d:\d\d:\d\d\]")
validate_addr = cv.matches_regex(r"\[(?:\d\d:)?\d\d:\d\d:\d\d\]")
async def validate_add_controller(
@ -565,15 +565,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_KEYPADS: [
{
CONF_ADDR: keypad[CONF_ADDR],
CONF_BUTTONS: [
{
CONF_LED: button[CONF_LED],
CONF_NAME: button[CONF_NAME],
CONF_NUMBER: button[CONF_NUMBER],
CONF_RELEASE_DELAY: button[CONF_RELEASE_DELAY],
}
for button in keypad[CONF_BUTTONS]
],
CONF_BUTTONS: [],
CONF_NAME: keypad[CONF_NAME],
}
for keypad in config[CONF_KEYPADS]

View File

@ -191,13 +191,13 @@ class HyperionVisiblePrioritySensor(HyperionSensor):
if priority[KEY_COMPONENTID] == "COLOR":
state_value = priority[KEY_VALUE][KEY_RGB]
else:
state_value = priority[KEY_OWNER]
state_value = priority.get(KEY_OWNER)
attrs = {
"component_id": priority[KEY_COMPONENTID],
"origin": priority[KEY_ORIGIN],
"priority": priority[KEY_PRIORITY],
"owner": priority[KEY_OWNER],
"owner": priority.get(KEY_OWNER),
}
if priority[KEY_COMPONENTID] == "COLOR":

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==10.2.0"]
"requirements": ["Pillow==10.3.0"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==7.0.3"]
"requirements": ["ical==8.0.0"]
}

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==7.0.3"]
"requirements": ["ical==8.0.0"]
}

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/matrix",
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
"requirements": ["matrix-nio==0.24.0", "Pillow==10.2.0"]
"requirements": ["matrix-nio==0.24.0", "Pillow==10.3.0"]
}

View File

@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "platinum",
"requirements": ["pymodbus==3.6.7"]
"requirements": ["pymodbus==3.6.8"]
}

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["plexapi", "plexwebsocket"],
"requirements": [
"PlexAPI==4.15.11",
"PlexAPI==4.15.12",
"plexauth==0.0.6",
"plexwebsocket==0.0.14"
],

View File

@ -3,5 +3,5 @@
"name": "Camera Proxy",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": ["Pillow==10.2.0"]
"requirements": ["Pillow==10.3.0"]
}

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"iot_class": "calculated",
"loggers": ["pyzbar"],
"requirements": ["Pillow==10.2.0", "pyzbar==0.1.7"]
"requirements": ["Pillow==10.3.0", "pyzbar==0.1.7"]
}

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "platinum",
"requirements": ["renault-api==0.2.1"]
"requirements": ["renault-api==0.2.2"]
}

View File

@ -71,6 +71,6 @@ SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = (
coordinator="charge_mode",
data_key="chargeMode",
translation_key="charge_mode",
options=["always", "always_charging", "schedule_mode"],
options=["always", "always_charging", "schedule_mode", "scheduled"],
),
)

View File

@ -46,15 +46,17 @@ from .triggers.turn_on import async_get_turn_on_trigger
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
SUPPORT_SAMSUNGTV = (
MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.TURN_OFF
MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
)

View File

@ -4,5 +4,5 @@
"codeowners": ["@fabaff"],
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"iot_class": "local_polling",
"requirements": ["Pillow==10.2.0"]
"requirements": ["Pillow==10.3.0"]
}

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"iot_class": "cloud_polling",
"loggers": ["simplehound"],
"requirements": ["Pillow==10.2.0", "simplehound==0.3"]
"requirements": ["Pillow==10.3.0", "simplehound==0.3"]
}

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling",
"requirements": ["SQLAlchemy==2.0.29", "sqlparse==0.4.4"]
"requirements": ["SQLAlchemy==2.0.29", "sqlparse==0.5.0"]
}

View File

@ -28,7 +28,6 @@ from homeassistant.const import (
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
@ -44,6 +43,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.util.dt import utcnow
from .browse_media import (
@ -207,12 +207,7 @@ async def async_setup_entry(
platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync")
# Start server discovery task if not already running
if hass.is_running:
hass.async_create_task(start_server_discovery(hass))
else:
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, start_server_discovery(hass)
)
config_entry.async_on_unload(async_at_start(hass, start_server_discovery))
class SqueezeBoxEntity(MediaPlayerEntity):

View File

@ -91,7 +91,7 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity):
self._fuel_type = fuel_type
self._attr_translation_key = fuel_type
self._attr_unique_id = f"{station.id}_{fuel_type}"
attrs = {
attrs: dict[str, int | str | float | None] = {
ATTR_BRAND: station.brand,
ATTR_FUEL_TYPE: fuel_type,
ATTR_STATION_NAME: station.name,
@ -102,8 +102,8 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity):
}
if coordinator.show_on_map:
attrs[ATTR_LATITUDE] = str(station.lat)
attrs[ATTR_LONGITUDE] = str(station.lng)
attrs[ATTR_LATITUDE] = station.lat
attrs[ATTR_LONGITUDE] = station.lng
self._attr_extra_state_attributes = attrs
@property

View File

@ -10,6 +10,6 @@
"tf-models-official==2.5.0",
"pycocotools==2.0.6",
"numpy==1.26.0",
"Pillow==10.2.0"
"Pillow==10.3.0"
]
}

View File

@ -58,7 +58,7 @@ SHIFT_STATES = {"P": "p", "D": "d", "R": "r", "N": "n"}
class TeslemetrySensorEntityDescription(SensorEntityDescription):
"""Describes Teslemetry Sensor entity."""
value_fn: Callable[[StateType], StateType | datetime] = lambda x: x
value_fn: Callable[[StateType], StateType] = lambda x: x
VEHICLE_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = (
@ -447,8 +447,14 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
description: TeslemetrySensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(vehicle, description.key)
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._value)
class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity):
"""Base class for Teslemetry vehicle metric sensors."""

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==74"],
"requirements": ["aiounifi==75"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -12,6 +12,7 @@ from dataclasses import dataclass, field
from datetime import timedelta
from typing import TYPE_CHECKING, Any, cast
from aiohttp import ClientError
from aiohttp.hdrs import METH_POST
from aiohttp.web import Request, Response
from aiowithings import NotificationCategory, WithingsClient
@ -340,7 +341,11 @@ class WithingsWebhookManager:
async def async_unsubscribe_webhooks(client: WithingsClient) -> None:
"""Unsubscribe to all Withings webhooks."""
try:
current_webhooks = await client.list_notification_configurations()
except ClientError:
LOGGER.exception("Error when unsubscribing webhooks")
return
for webhook_configuration in current_webhooks:
LOGGER.debug(

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/xmpp",
"iot_class": "cloud_push",
"loggers": ["pyasn1", "slixmpp"],
"requirements": ["slixmpp==1.8.4", "emoji==2.8.0"]
"requirements": ["slixmpp==1.8.5", "emoji==2.8.0"]
}

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
"requirements": ["zeroconf==0.132.0"]
"requirements": ["zeroconf==0.132.2"]
}

View File

@ -18,7 +18,7 @@ from .util.signal_type import SignalType
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -656,6 +656,12 @@ class _ScriptRun:
# check if condition already okay
if condition.async_template(self._hass, wait_template, self._variables, False):
self._variables["wait"]["completed"] = True
self._changed()
return
if timeout == 0:
self._changed()
self._async_handle_timeout()
return
futures, timeout_handle, timeout_future = self._async_futures_with_timeout(
@ -1085,6 +1091,11 @@ class _ScriptRun:
self._variables["wait"] = {"remaining": timeout, "trigger": None}
trace_set_result(wait=self._variables["wait"])
if timeout == 0:
self._changed()
self._async_handle_timeout()
return
futures, timeout_handle, timeout_future = self._async_futures_with_timeout(
timeout
)
@ -1115,6 +1126,14 @@ class _ScriptRun:
futures, timeout_handle, timeout_future, remove_triggers
)
def _async_handle_timeout(self) -> None:
"""Handle timeout."""
self._variables["wait"]["remaining"] = 0.0
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
self._log(_TIMEOUT_MSG)
trace_set_result(wait=self._variables["wait"], timeout=True)
raise _AbortScript from TimeoutError()
async def _async_wait_with_optional_timeout(
self,
futures: list[asyncio.Future[None]],
@ -1125,11 +1144,7 @@ class _ScriptRun:
try:
await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED)
if timeout_future and timeout_future.done():
self._variables["wait"]["remaining"] = 0.0
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
self._log(_TIMEOUT_MSG)
trace_set_result(wait=self._variables["wait"], timeout=True)
raise _AbortScript from TimeoutError()
self._async_handle_timeout()
finally:
if timeout_future and not timeout_future.done() and timeout_handle:
timeout_handle.cancel()

View File

@ -403,6 +403,8 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
if not auth_failed and self._listeners and not self.hass.is_stopping:
self._schedule_refresh()
self._async_refresh_finished()
if not self.last_update_success and not previous_update_success:
return
@ -413,6 +415,15 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
):
self.async_update_listeners()
@callback
def _async_refresh_finished(self) -> None:
"""Handle when a refresh has finished.
Called when refresh is finished before listeners are updated.
To be overridden by subclasses.
"""
@callback
def async_set_update_error(self, err: Exception) -> None:
"""Manually set an error, log the message and notify listeners."""
@ -446,20 +457,9 @@ class TimestampDataUpdateCoordinator(DataUpdateCoordinator[_DataT]):
last_update_success_time: datetime | None = None
async def _async_refresh(
self,
log_failures: bool = True,
raise_on_auth_failed: bool = False,
scheduled: bool = False,
raise_on_entry_error: bool = False,
) -> None:
"""Refresh data."""
await super()._async_refresh(
log_failures,
raise_on_auth_failed,
scheduled,
raise_on_entry_error,
)
@callback
def _async_refresh_finished(self) -> None:
"""Handle when a refresh has finished."""
if self.last_update_success:
self.last_update_success_time = utcnow()

View File

@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0
aiodiscover==2.0.0
aiohttp-fast-url-dispatcher==0.3.0
aiohttp-zlib-ng==0.3.1
aiohttp==3.9.4
aiohttp==3.9.5
aiohttp_cors==0.7.0
astral==2.2
async-interrupt==1.1.1
@ -40,7 +40,7 @@ mutagen==1.47.0
orjson==3.9.15
packaging>=23.1
paho-mqtt==1.6.1
Pillow==10.2.0
Pillow==10.3.0
pip>=21.3.1
psutil-home-assistant==0.0.1
PyJWT==2.8.0
@ -60,7 +60,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.13.1
webrtc-noise-gain==1.2.3
yarl==1.9.4
zeroconf==0.132.0
zeroconf==0.132.2
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238
@ -107,7 +107,7 @@ regex==2021.8.28
# requirements so we can directly link HA versions to these library versions.
anyio==4.3.0
h11==0.14.0
httpcore==1.0.4
httpcore==1.0.5
# Ensure we have a hyperframe version that works in Python 3.10
# 5.2.0 fixed a collections abc deprecation

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.4.3"
version = "2024.4.4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -23,7 +23,7 @@ classifiers = [
]
requires-python = ">=3.12.0"
dependencies = [
"aiohttp==3.9.4",
"aiohttp==3.9.5",
"aiohttp_cors==0.7.0",
"aiohttp-fast-url-dispatcher==0.3.0",
"aiohttp-zlib-ng==0.3.1",
@ -49,7 +49,7 @@ dependencies = [
"PyJWT==2.8.0",
# PyJWT has loose dependency. We want the latest one.
"cryptography==42.0.5",
"Pillow==10.2.0",
"Pillow==10.3.0",
"pyOpenSSL==24.1.0",
"orjson==3.9.15",
"packaging>=23.1",

View File

@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiohttp==3.9.4
aiohttp==3.9.5
aiohttp_cors==0.7.0
aiohttp-fast-url-dispatcher==0.3.0
aiohttp-zlib-ng==0.3.1
@ -24,7 +24,7 @@ Jinja2==3.1.3
lru-dict==1.3.0
PyJWT==2.8.0
cryptography==42.0.5
Pillow==10.2.0
Pillow==10.3.0
pyOpenSSL==24.1.0
orjson==3.9.15
packaging>=23.1

View File

@ -42,10 +42,10 @@ Mastodon.py==1.8.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
Pillow==10.2.0
Pillow==10.3.0
# homeassistant.components.plex
PlexAPI==4.15.11
PlexAPI==4.15.12
# homeassistant.components.progettihwsw
ProgettiHWSW==0.1.3
@ -392,7 +392,7 @@ aiotankerkoenig==0.4.1
aiotractive==0.5.6
# homeassistant.components.unifi
aiounifi==74
aiounifi==75
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@ -1118,7 +1118,7 @@ ibmiotf==0.3.4
# homeassistant.components.google
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
ical==7.0.3
ical==8.0.0
# homeassistant.components.ping
icmplib==3.0
@ -1973,7 +1973,7 @@ pymitv==1.4.3
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.6.7
pymodbus==3.6.8
# homeassistant.components.monoprice
pymonoprice==0.4
@ -2429,7 +2429,7 @@ refoss-ha==1.2.0
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.2.1
renault-api==0.2.2
# homeassistant.components.renson
renson-endura-delta==1.7.1
@ -2553,7 +2553,7 @@ sisyphus-control==3.1.3
slackclient==2.5.0
# homeassistant.components.xmpp
slixmpp==1.8.4
slixmpp==1.8.5
# homeassistant.components.smart_meter_texas
smart-meter-texas==0.4.7
@ -2595,7 +2595,7 @@ spiderpy==1.6.1
spotipy==2.23.0
# homeassistant.components.sql
sqlparse==0.4.4
sqlparse==0.5.0
# homeassistant.components.srp_energy
srpenergy==1.3.6
@ -2928,7 +2928,7 @@ zamg==0.3.6
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.132.0
zeroconf==0.132.2
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -36,10 +36,10 @@ HATasmota==0.8.0
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
Pillow==10.2.0
Pillow==10.3.0
# homeassistant.components.plex
PlexAPI==4.15.11
PlexAPI==4.15.12
# homeassistant.components.progettihwsw
ProgettiHWSW==0.1.3
@ -365,7 +365,7 @@ aiotankerkoenig==0.4.1
aiotractive==0.5.6
# homeassistant.components.unifi
aiounifi==74
aiounifi==75
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@ -908,7 +908,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.google
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
ical==7.0.3
ical==8.0.0
# homeassistant.components.ping
icmplib==3.0
@ -1533,7 +1533,7 @@ pymeteoclimatic==0.1.0
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.6.7
pymodbus==3.6.8
# homeassistant.components.monoprice
pymonoprice==0.4
@ -1875,7 +1875,7 @@ refoss-ha==1.2.0
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.2.1
renault-api==0.2.2
# homeassistant.components.renson
renson-endura-delta==1.7.1
@ -1999,7 +1999,7 @@ spiderpy==1.6.1
spotipy==2.23.0
# homeassistant.components.sql
sqlparse==0.4.4
sqlparse==0.5.0
# homeassistant.components.srp_energy
srpenergy==1.3.6
@ -2263,7 +2263,7 @@ yt-dlp==2024.04.09
zamg==0.3.6
# homeassistant.components.zeroconf
zeroconf==0.132.0
zeroconf==0.132.2
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -100,7 +100,7 @@ regex==2021.8.28
# requirements so we can directly link HA versions to these library versions.
anyio==4.3.0
h11==0.14.0
httpcore==1.0.4
httpcore==1.0.5
# Ensure we have a hyperframe version that works in Python 3.10
# 5.2.0 fixed a collections abc deprecation

View File

@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.homeworks.const import (
CONF_ADDR,
CONF_BUTTONS,
CONF_DIMMERS,
CONF_INDEX,
CONF_KEYPADS,
@ -161,26 +160,6 @@ async def test_import_flow(
{
CONF_ADDR: "[02:08:02:01]",
CONF_NAME: "Foyer Keypad",
CONF_BUTTONS: [
{
CONF_NAME: "Morning",
CONF_NUMBER: 1,
CONF_LED: True,
CONF_RELEASE_DELAY: None,
},
{
CONF_NAME: "Relax",
CONF_NUMBER: 2,
CONF_LED: True,
CONF_RELEASE_DELAY: None,
},
{
CONF_NAME: "Dim up",
CONF_NUMBER: 3,
CONF_LED: False,
CONF_RELEASE_DELAY: 0.2,
},
],
}
],
},
@ -207,16 +186,7 @@ async def test_import_flow(
"keypads": [
{
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"buttons": [],
"name": "Foyer Keypad",
}
],
@ -574,8 +544,12 @@ async def test_options_add_remove_light_flow(
)
@pytest.mark.parametrize("keypad_address", ["[02:08:03:01]", "[02:08:03]"])
async def test_options_add_remove_keypad_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_homeworks: MagicMock,
keypad_address: str,
) -> None:
"""Test options flow to add and remove a keypad."""
mock_config_entry.add_to_hass(hass)
@ -596,7 +570,7 @@ async def test_options_add_remove_keypad_flow(
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_ADDR: "[02:08:03:01]",
CONF_ADDR: keypad_address,
CONF_NAME: "Hall Keypad",
},
)
@ -622,7 +596,7 @@ async def test_options_add_remove_keypad_flow(
],
"name": "Foyer Keypad",
},
{"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"},
{"addr": keypad_address, "buttons": [], "name": "Hall Keypad"},
],
"port": 1234,
}
@ -642,7 +616,7 @@ async def test_options_add_remove_keypad_flow(
assert result["step_id"] == "remove_keypad"
assert result["data_schema"].schema["index"].options == {
"0": "Foyer Keypad ([02:08:02:01])",
"1": "Hall Keypad ([02:08:03:01])",
"1": f"Hall Keypad ({keypad_address})",
}
result = await hass.config_entries.options.async_configure(
@ -655,7 +629,7 @@ async def test_options_add_remove_keypad_flow(
{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0},
],
"host": "192.168.0.1",
"keypads": [{"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}],
"keypads": [{"addr": keypad_address, "buttons": [], "name": "Hall Keypad"}],
"port": 1234,
}
await hass.async_block_till_done()

View File

@ -159,7 +159,6 @@ async def test_visible_effect_state_changes(hass: HomeAssistant) -> None:
KEY_ACTIVE: True,
KEY_COMPONENTID: "COLOR",
KEY_ORIGIN: "System",
KEY_OWNER: "System",
KEY_PRIORITY: 250,
KEY_VALUE: {KEY_RGB: [0, 0, 0]},
KEY_VISIBLE: True,

View File

@ -127,7 +127,12 @@ MOCK_VEHICLES = {
{
ATTR_ENTITY_ID: "select.reg_number_charge_mode",
ATTR_ICON: "mdi:calendar-remove",
ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"],
ATTR_OPTIONS: [
"always",
"always_charging",
"schedule_mode",
"scheduled",
],
ATTR_STATE: "always",
ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode",
},
@ -363,7 +368,12 @@ MOCK_VEHICLES = {
{
ATTR_ENTITY_ID: "select.reg_number_charge_mode",
ATTR_ICON: "mdi:calendar-clock",
ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"],
ATTR_OPTIONS: [
"always",
"always_charging",
"schedule_mode",
"scheduled",
],
ATTR_STATE: "schedule_mode",
ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode",
},
@ -599,7 +609,12 @@ MOCK_VEHICLES = {
{
ATTR_ENTITY_ID: "select.reg_number_charge_mode",
ATTR_ICON: "mdi:calendar-remove",
ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"],
ATTR_OPTIONS: [
"always",
"always_charging",
"schedule_mode",
"scheduled",
],
ATTR_STATE: "always",
ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_mode",
},

View File

@ -82,6 +82,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'config_entry_id': <ANY>,
@ -121,6 +122,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'context': <ANY>,
@ -175,6 +177,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'config_entry_id': <ANY>,
@ -214,6 +217,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'context': <ANY>,
@ -268,6 +272,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'config_entry_id': <ANY>,
@ -307,6 +312,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'context': <ANY>,
@ -401,6 +407,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'config_entry_id': <ANY>,
@ -440,6 +447,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'context': <ANY>,
@ -494,6 +502,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'config_entry_id': <ANY>,
@ -533,6 +542,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'context': <ANY>,
@ -587,6 +597,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'config_entry_id': <ANY>,
@ -626,6 +637,7 @@
'always',
'always_charging',
'schedule_mode',
'scheduled',
]),
}),
'context': <ANY>,

View File

@ -9,7 +9,7 @@
'TV',
'HDMI',
]),
'supported_features': <MediaPlayerEntityFeature: 20413>,
'supported_features': <MediaPlayerEntityFeature: 24509>,
}),
'context': <ANY>,
'entity_id': 'media_player.any',
@ -51,7 +51,7 @@
'original_name': None,
'platform': 'samsungtv',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 20413>,
'supported_features': <MediaPlayerEntityFeature: 24509>,
'translation_key': None,
'unique_id': 'sample-entry-id',
'unit_of_measurement': None,

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from urllib.parse import urlparse
from aiohttp import ClientConnectionError
from aiohttp.hdrs import METH_HEAD
from aiowithings import (
NotificationCategory,
@ -508,6 +509,110 @@ async def test_cloud_disconnect(
assert withings.subscribe_notification.call_count == 12
async def test_internet_disconnect(
hass: HomeAssistant,
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test we can recover from internet disconnects."""
await mock_cloud(hass)
await hass.async_block_till_done()
with (
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
patch.object(cloud, "async_is_connected", return_value=True),
patch.object(cloud, "async_active_subscription", return_value=True),
patch(
"homeassistant.components.cloud.async_create_cloudhook",
return_value="https://hooks.nabu.casa/ABCD",
),
patch(
"homeassistant.components.withings.async_get_config_entry_implementation",
),
patch(
"homeassistant.components.cloud.async_delete_cloudhook",
),
patch(
"homeassistant.components.withings.webhook_generate_url",
),
):
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
assert cloud.async_active_subscription(hass) is True
assert cloud.async_is_connected(hass) is True
assert withings.revoke_notification_configurations.call_count == 3
assert withings.subscribe_notification.call_count == 6
await hass.async_block_till_done()
withings.list_notification_configurations.side_effect = ClientConnectionError
async_mock_cloud_connection_status(hass, False)
await hass.async_block_till_done()
assert withings.revoke_notification_configurations.call_count == 3
async_mock_cloud_connection_status(hass, True)
await hass.async_block_till_done()
assert withings.subscribe_notification.call_count == 12
async def test_cloud_disconnect_retry(
hass: HomeAssistant,
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test we retry to create webhook connection again after cloud disconnects."""
await mock_cloud(hass)
await hass.async_block_till_done()
with (
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
patch.object(cloud, "async_is_connected", return_value=True),
patch.object(
cloud, "async_active_subscription", return_value=True
) as mock_async_active_subscription,
patch(
"homeassistant.components.cloud.async_create_cloudhook",
return_value="https://hooks.nabu.casa/ABCD",
),
patch(
"homeassistant.components.withings.async_get_config_entry_implementation",
),
patch(
"homeassistant.components.cloud.async_delete_cloudhook",
),
patch(
"homeassistant.components.withings.webhook_generate_url",
),
):
await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)
assert cloud.async_active_subscription(hass) is True
assert cloud.async_is_connected(hass) is True
assert mock_async_active_subscription.call_count == 3
await hass.async_block_till_done()
async_mock_cloud_connection_status(hass, False)
await hass.async_block_till_done()
assert mock_async_active_subscription.call_count == 3
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_async_active_subscription.call_count == 4
@pytest.mark.parametrize(
("body", "expected_code"),
[

View File

@ -1311,6 +1311,184 @@ async def test_wait_timeout(
assert_action_trace(expected_trace)
@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_trigger_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait trigger with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {
"wait_for_trigger": {
"platform": "state",
"entity_id": "switch.test",
"to": "off",
}
}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "on")
hass.async_create_task(script_obj.async_run(context=Context()))
try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise
assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text
variable_wait = {"wait": {"trigger": None, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)
@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_trigger_matches_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait trigger that matches with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {
"wait_for_trigger": {
"platform": "state",
"entity_id": "switch.test",
"to": "off",
}
}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "off")
hass.async_create_task(script_obj.async_run(context=Context()))
try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise
assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text
variable_wait = {"wait": {"trigger": None, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)
@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_template_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait template with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {"wait_template": "{{ states.switch.test.state == 'off' }}"}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "on")
hass.async_create_task(script_obj.async_run(context=Context()))
try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise
assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text
variable_wait = {"wait": {"completed": False, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)
@pytest.mark.parametrize(
"timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}]
)
async def test_wait_template_matches_with_zero_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str
) -> None:
"""Test the wait template that matches with zero timeout option."""
event = "test_event"
events = async_capture_events(hass, event)
action = {"wait_template": "{{ states.switch.test.state == 'off' }}"}
action["timeout"] = timeout_param
action["continue_on_timeout"] = True
sequence = cv.SCRIPT_SCHEMA([action, {"event": event}])
sequence = await script.async_validate_actions_config(hass, sequence)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
wait_started_flag = async_watch_for_action(script_obj, "wait")
hass.states.async_set("switch.test", "off")
hass.async_create_task(script_obj.async_run(context=Context()))
try:
await asyncio.wait_for(wait_started_flag.wait(), 1)
except (AssertionError, TimeoutError):
await script_obj.async_stop()
raise
assert not script_obj.is_running
assert len(events) == 1
assert "(timeout: 0:00:00)" in caplog.text
variable_wait = {"wait": {"completed": True, "remaining": 0.0}}
expected_trace = {
"0": [
{
"result": variable_wait,
"variables": variable_wait,
}
],
"1": [{"result": {"event": "test_event", "event_data": {}}}],
}
assert_action_trace(expected_trace)
@pytest.mark.parametrize(
("continue_on_timeout", "n_events"), [(False, 0), (True, 1), (None, 1)]
)

View File

@ -1,6 +1,6 @@
"""Tests for the update coordinator."""
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from unittest.mock import AsyncMock, Mock, patch
import urllib.error
@ -12,7 +12,7 @@ import requests
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import update_coordinator
from homeassistant.util.dt import utcnow
@ -716,3 +716,35 @@ async def test_always_callback_when_always_update_is_true(
update_callback.reset_mock()
remove_callbacks()
async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None:
"""Test last_update_success_time is set before calling listeners."""
last_update_success_times: list[datetime | None] = []
async def refresh() -> int:
return 1
crd = update_coordinator.TimestampDataUpdateCoordinator[int](
hass,
_LOGGER,
name="test",
update_method=refresh,
update_interval=timedelta(seconds=10),
)
@callback
def listener():
last_update_success_times.append(crd.last_update_success_time)
unsub = crd.async_add_listener(listener)
await crd.async_refresh()
assert len(last_update_success_times) == 1
# Ensure the time is set before the listener is called
assert last_update_success_times != [None]
unsub()
await crd.async_refresh()
assert len(last_update_success_times) == 1