Compare commits

...

78 Commits

Author SHA1 Message Date
Franck Nijhof
00d5e89951 2026.3.4 (#166285) 2026-03-24 08:11:42 +01:00
Petro31
557d072a4d Update template light test framework (#164688) 2026-03-24 06:38:58 +00:00
Franck Nijhof
6c3917e927 Bump version to 2026.3.4 2026-03-23 19:24:24 +00:00
Bram Kragten
e895c1b2fd Update frontend to 20260312.1 (#166251) 2026-03-23 19:20:37 +00:00
Matrix
dae971cd98 Bump yolink-api to 0.6.3 (#166232) 2026-03-23 19:20:36 +00:00
Peter Grauvogel
807df50eab Bump greenplanet-energy-api from 0.1.4 to 0.1.10 (#166217) 2026-03-23 19:15:57 +00:00
MarkGodwin
aa05ff03b3 Bump tplink-omada-client to fix breaking changes in Omada API (#166206) 2026-03-23 19:15:56 +00:00
Tommy Goode
622b92682e Fix zwave_js fan speed mapping for GE/Jasco Enbrighten 55258 / ZW4002 (#166169) 2026-03-23 17:46:26 +00:00
J. Nick Koston
a81146a227 Bump oralb-ble to 1.1.0 (#166165) 2026-03-23 17:46:24 +00:00
EnjoyingM
530dcadf19 Bump wolf_comm to 0.0.48 (#166144) 2026-03-23 17:31:22 +00:00
Michael
4aa67ddf22 Fix reload of FRITZ!Box Tools in case of connection issues (#166111) 2026-03-23 17:24:39 +00:00
Josef Zweck
8e95b19c4c Bump aiotedee to 0.2.27 (#166101)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-23 17:24:38 +00:00
Sean O'Keeffe
5558b33600 Add additional miele oven programs (#166100) 2026-03-23 17:24:36 +00:00
Ray Xue
0130ac6770 Bump xiaomi-ble to 1.10.0 (#166099) 2026-03-23 17:24:35 +00:00
tronikos
26d22e4d62 Bump python-google-weather-api to 0.0.6 (#166085) 2026-03-23 17:24:33 +00:00
Jack Boswell
532bc02d66 Update starlink-grpc-core to 1.2.4 (#165882) 2026-03-23 17:24:32 +00:00
Petro31
893eac0e84 Correct validation of scripts in template entities (#165226) 2026-03-23 17:22:39 +00:00
Franck Nijhof
c1bd83c9c0 2026.3.3 (#166076) 2026-03-20 23:01:26 +01:00
TimL
b3c27e9f93 Bump Pysmlight 0.3.1 (#166060) 2026-03-20 20:26:10 +00:00
TimL
92e237ade2 Bump Pysmlight to 0.3.0 (#165658) 2026-03-20 20:26:08 +00:00
Franck Nijhof
cbc573a6b1 Bump version to 2026.3.3 2026-03-20 19:56:30 +00:00
TimL
0c059cfc27 Properly handle buttons of SMLIGHT SLZB-MRxU devices (#166058) 2026-03-20 19:55:55 +00:00
tronikos
143ce9d7b3 Bump opower to 0.17.1 (#166044) 2026-03-20 19:55:17 +00:00
Michael
a6aa837d40 Fix enable/disable device tracking feature during setup of FRITZ!Box Tools (#166027) 2026-03-20 19:52:45 +00:00
Joost Lekkerkerker
c58b4a0066 Don't create fridge setpoint if no range in SmartThings (#166018) 2026-03-20 19:52:43 +00:00
Hai-Nam Nguyen
5155242ba7 Bump hyponcloud from 0.3.0 to 0.9.0 (#166005) 2026-03-20 19:52:42 +00:00
Hai-Nam Nguyen
085680f6bf Fix unit when plant power is above 1000W in Hypontech (#165959)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-20 19:52:41 +00:00
AlCalzone
98ecaaa6d2 Do not use moving states for Multilevel Switch CC v1-3 Z-Wave covers (#165909) 2026-03-20 19:52:39 +00:00
Erwin Douna
5ad199fe16 Proxmox fix restart/reboot action (#165901) 2026-03-20 19:52:38 +00:00
Stefan Agner
413cb98424 Fix Abort exception caught by wrong handler in backup encrypt/decrypt (#165852)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:52:37 +00:00
Robert Svensson
b38c5bcaf2 Bump axis to v67 (#165840) 2026-03-20 19:52:35 +00:00
Joost Lekkerkerker
fa85dfb3b5 Bump pySmartThings to 3.7.2 (#165810) 2026-03-20 19:52:34 +00:00
Robert Resch
f0c6a035db Bump pyOpenSSL to 26.0.0 (#165770) 2026-03-20 19:52:33 +00:00
Ludovic BOUÉ
3f0c200e56 Fix Matter firmware update detection when version strings are identical (#165509) 2026-03-20 19:52:32 +00:00
Raj Laud
a2259ede28 Fix SmartLithium 8-cell support in victron_ble (#165496)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 19:52:30 +00:00
Willem-Jan van Rootselaar
24c2b6fe81 Fix optional static values in bsblan (#165488) 2026-03-20 19:52:29 +00:00
Alex Merkel
efc7350e6f LG Soundbar: Fix incorrect state and outdated track information (#165148) 2026-03-20 19:52:28 +00:00
Khole
5f525fc2a1 Hive: Fix bug in config flow for authentication and device registration (#165061) 2026-03-20 19:52:26 +00:00
Tucker Kern
f619a3e7af Snapcast: Fix incorrect identifier extraction in async_join_players (#165020) 2026-03-20 19:52:25 +00:00
Paul Tarjan
4e43492342 Skip unmapped and watchdog event types in Hikvision NVR event injection (#165009)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 19:52:24 +00:00
Erwin Douna
39e70071d3 Start orphaned entries in normal mode only (#164815)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-20 19:52:22 +00:00
Tom
6da0936a66 Improve ProxmoxVE permissions validation (#164770) 2026-03-20 19:52:21 +00:00
Martin Ecker
5257702530 Add correct speed fan mapping for Z-Wave GE/Jasco Enbrighten ZWA4013 (#164500) 2026-03-20 19:52:20 +00:00
Daniel Hjelseth Høyer
93da5be052 Fix Tibber update token (#164295)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-20 19:52:18 +00:00
Franck Nijhof
2c47e83342 2026.3.2 (#165675) 2026-03-16 13:23:27 +01:00
Franck Nijhof
e3c6a2184d Bump version to 2026.3.2 2026-03-16 10:27:01 +00:00
Simone Chemelli
0ba0829350 Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:25:08 +00:00
Allen Porter
678048e681 Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 10:25:07 +00:00
Jan Bouwhuis
743eeeae53 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:25:05 +00:00
Raj Laud
46555c6d9a Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 10:25:04 +00:00
Simone Chemelli
dbaca0a723 Bump aioamazondevices to 13.0.1 (#165476) 2026-03-16 10:25:02 +00:00
Joost Lekkerkerker
9bb2959029 Bump pySmartThings to 3.7.0 (#165468) 2026-03-16 10:25:01 +00:00
Robert Resch
0304781fa9 Bump orjson to 3.11.7 (#165443) 2026-03-16 10:25:00 +00:00
J. Nick Koston
e081d28aa4 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-16 10:24:58 +00:00
TheJulianJES
34aa28c72f Bump ZHA to 1.0.2 (#165423) 2026-03-16 10:24:56 +00:00
Bram Kragten
cfa2946db8 Update frontend to 20260312.0 (#165420) 2026-03-16 10:24:55 +00:00
Galorhallen
1b0779347c Update govee local api to 2.4.0 (#165418) 2026-03-16 10:24:54 +00:00
Joost Lekkerkerker
93a281e7af Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-16 10:24:53 +00:00
Josef Zweck
6b32e27fd3 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-16 10:24:51 +00:00
Zach Feldman
79928a8c7c August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-16 10:24:50 +00:00
Steve Easley
9146518e13 Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-16 10:24:48 +00:00
Dan Raper
e9c5172f43 Bump ohme to 1.7.0 (#165318) 2026-03-16 10:24:47 +00:00
TheJulianJES
cce21ad4b9 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-16 10:24:46 +00:00
Simone Chemelli
10ec02ca3c Fix switch set for Vodafone Station (#165273) 2026-03-16 10:18:26 +00:00
Josef Zweck
bdf54491e5 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-16 10:18:25 +00:00
Bram Kragten
0b05d34238 Add reorder support to area selector (#165211) 2026-03-16 10:18:24 +00:00
Åke Strandberg
4c69a1c5f7 Add missing code for Miele dryer (#165122) 2026-03-16 10:17:00 +00:00
Steve Easley
6f1f56dcaa Bump jvc_projector dependency to 2.0.2 (#165099) 2026-03-16 10:16:59 +00:00
Jordan Harvey
d0b9991232 Bump pyanglianwater to 3.1.1 (#165097) 2026-03-16 10:16:58 +00:00
Artur Pragacz
aacf39be8a Make restore state resilient to extra_restore_state_data errors (#165086) 2026-03-16 10:16:56 +00:00
Erwin Douna
bf055da82c Bump pyportainer to 1.0.33 (#165080) 2026-03-16 10:12:26 +00:00
Erwin Douna
0fb118bcd9 Bump pyportainer 1.0.32 (#164803) 2026-03-16 10:12:25 +00:00
Erwin Douna
954ef7d1f5 Fix forced VERIFY_SSL in Portainer (#165079) 2026-03-16 09:56:32 +00:00
Joakim Plate
b091299320 Update pychromecast to 14.0.10 (#165069) 2026-03-16 09:56:31 +00:00
J. Nick Koston
52483e18b2 Bump yalexs-ble to 3.2.8 (#165018) 2026-03-16 09:56:29 +00:00
AlCalzone
57e8683ed7 Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003) 2026-03-16 09:56:28 +00:00
Simone Chemelli
67faace978 Fix dnd switch status for Alexa Devices (#164953) 2026-03-16 09:56:26 +00:00
Simone Chemelli
e4be64fcb1 Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 09:56:25 +00:00
137 changed files with 5325 additions and 1954 deletions

View File

@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model = self.device.model
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model=self.device.model,
model_id=self.device.device_type,
manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=(
self.device.software_version
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
sw_version=self.device.software_version,
serial_number=serial_num,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.0.0"]
"requirements": ["aioamazondevices==13.0.1"]
}

View File

@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None
await method(self.device, state)
await self.coordinator.async_request_refresh()
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.0"]
"requirements": ["pyanglianwater==3.1.1"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
except (
AugustApiAIOHTTPError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
}

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==66"],
"requirements": ["axis==67"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -246,6 +246,8 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
@@ -332,8 +334,10 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size

View File

@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_PASSKEY, DOMAIN
from .const import CONF_PASSKEY, DOMAIN, LOGGER
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services
@@ -52,7 +52,7 @@ class BSBLanData:
client: BSBLAN
device: Device
info: Info
static: StaticState
static: StaticState | None
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
# the connection by fetching firmware version
await bsblan.initialize()
# Fetch device metadata in parallel for faster startup
device, info, static = await asyncio.gather(
# Fetch required device metadata in parallel for faster startup
device, info = await asyncio.gather(
bsblan.device(),
bsblan.info(),
bsblan.static_values(),
)
except BSBLANConnectionError as err:
raise ConfigEntryNotReady(
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
translation_key="setup_general_error",
) from err
try:
static = await bsblan.static_values()
except (BSBLANError, TimeoutError) as err:
LOGGER.debug(
"Static values not available for %s: %s",
entry.data[CONF_HOST],
err,
)
static = None
# Create coordinators with the already-initialized client
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)

View File

@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
# Set temperature range if available, otherwise use Home Assistant defaults
if data.static.min_temp is not None and data.static.min_temp.value is not None:
self._attr_min_temp = data.static.min_temp.value
if data.static.max_temp is not None and data.static.max_temp.value is not None:
self._attr_max_temp = data.static.max_temp.value
if (static := data.static) is not None:
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
self._attr_min_temp = min_temp.value
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
self._attr_max_temp = max_temp.value
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
@property

View File

@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
"sensor": data.fast_coordinator.data.sensor.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump(),
},
"static": data.static.model_dump(),
"static": data.static.model_dump() if data.static is not None else None,
}
# Add DHW config and schedule from slow coordinator if available

View File

@@ -15,7 +15,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.9"],
"requirements": ["PyChromecast==14.0.10"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.0"]
"requirements": ["aiocomelit==2.0.1"]
}

View File

@@ -283,6 +283,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
self._use_tls = user_input[CONF_SSL]
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
self._port = self._determine_port(user_input)

View File

@@ -13,6 +13,7 @@ from fritzconnection.core.exceptions import (
FritzSecurityError,
FritzServiceError,
)
from requests.exceptions import ConnectionError
from homeassistant.const import Platform
@@ -68,6 +69,7 @@ BUTTON_TYPE_WOL = "WakeOnLan"
UPTIME_DEVIATION = 5
FRITZ_EXCEPTIONS = (
ConnectionError,
FritzActionError,
FritzActionFailedError,
FritzConnectionException,

View File

@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict = {}
networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2):
networks[i] = {
"ssid": network_info["NewSSID"],
"bssid": network_info["NewBSSID"],
"standard": network_info["NewStandard"],
"enabled": network_info["NewEnable"],
"status": network_info["NewStatus"],
}
networks[i] = network_info
for i, network in networks.copy().items():
networks[i]["switch_name"] = network["ssid"]
networks[i]["switch_name"] = network["NewSSID"]
if (
len(
[
j
for j, n in networks.items()
if slugify(n["ssid"]) == slugify(network["ssid"])
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
]
)
> 1
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
for key, attr in attributes_dict.items():
self._attributes[attr] = self.port_mapping[key]
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
resp = await self._avm_wrapper.async_add_port_mapping(
await self._avm_wrapper.async_add_port_mapping(
self.connection_type, self.port_mapping
)
return bool(resp is not None)
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False)
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
"""Handle switch state change request."""
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
self._avm_wrapper.devices[self._mac].wan_access = turn_on
self.async_write_ha_state()
return True
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
avm_wrapper: AvmWrapper,
device_friendly_name: str,
network_num: int,
network_data: dict,
network_data: dict[str, Any],
) -> None:
"""Init Fritz Wifi switch."""
self._avm_wrapper = avm_wrapper
self._wifi_info = network_data
self._attributes = {}
self._attr_entity_category = EntityCategory.CONFIG
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
type=SWITCH_TYPE_WIFINETWORK,
callback_update=self._async_fetch_update,
callback_switch=self._async_switch_on_off_executor,
init_state=network_data["enabled"],
init_state=network_data["NewEnable"],
)
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
self._attributes["mac_address_control"] = wifi_info[
"NewMACAddressControlEnabled"
]
self._wifi_info = wifi_info
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
"""Handle wifi switch."""
self._wifi_info["NewEnable"] = turn_on
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260304.0"]
"requirements": ["home-assistant-frontend==20260312.1"]
}

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_weather_api"],
"quality_scale": "bronze",
"requirements": ["python-google-weather-api==0.0.4"]
"requirements": ["python-google-weather-api==0.0.6"]
}

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==2.3.0"]
"requirements": ["govee-local-api==2.4.0"]
}

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["greenplanet-energy-api==0.1.4"],
"requirements": ["greenplanet-energy-api==0.1.10"],
"single_config_entry": true
}

View File

@@ -117,13 +117,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
# Map raw event type names to friendly names using SENSOR_MAP
mapped_events: dict[str, list[int]] = {}
for event_type, channels in nvr_events.items():
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
event_key = event_type.lower()
# Skip videoloss - used as watchdog by pyhik, not a real sensor
if event_key == "videoloss":
continue
friendly_name = SENSOR_MAP.get(event_key)
if friendly_name is None:
_LOGGER.debug("Skipping unmapped event type: %s", event_type)
continue
if friendly_name in mapped_events:
mapped_events[friendly_name].extend(channels)
else:
mapped_events[friendly_name] = list(channels)
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
camera.inject_events(mapped_events)
if mapped_events:
camera.inject_events(mapped_events)
else:
_LOGGER.debug(
"No event triggers returned from %s. "

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from apyhiveapi import Auth
@@ -26,6 +27,8 @@ from homeassistant.core import callback
from . import HiveConfigEntry
from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN
_LOGGER = logging.getLogger(__name__)
class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Hive config flow."""
@@ -36,7 +39,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self.data: dict[str, Any] = {}
self.tokens: dict[str, str] = {}
self.tokens: dict[str, Any] = {}
self.device_registration: bool = False
self.device_name = "Home Assistant"
@@ -67,11 +70,22 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
except HiveApiError:
errors["base"] = "no_internet_available"
if (
auth_result := self.tokens.get("AuthenticationResult", {})
) and auth_result.get("NewDeviceMetadata"):
_LOGGER.debug("Login successful, New device detected")
self.device_registration = True
return await self.async_step_configuration()
if self.tokens.get("ChallengeName") == "SMS_MFA":
_LOGGER.debug("Login successful, SMS 2FA required")
# Complete SMS 2FA.
return await self.async_step_2fa()
if not errors:
_LOGGER.debug(
"Login successful, no new device detected, no 2FA required"
)
# Complete the entry.
try:
return await self.async_setup_hive_entry()
@@ -103,6 +117,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "no_internet_available"
if not errors:
_LOGGER.debug("2FA successful")
if self.source == SOURCE_REAUTH:
return await self.async_setup_hive_entry()
self.device_registration = True
@@ -119,10 +134,11 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input:
if self.device_registration:
_LOGGER.debug("Attempting to register device")
self.device_name = user_input["device_name"]
await self.hive_auth.device_registration(user_input["device_name"])
self.data["device_data"] = await self.hive_auth.get_device_data()
_LOGGER.debug("Device registration successful")
try:
return await self.async_setup_hive_entry()
except UnknownHiveError:
@@ -142,6 +158,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
raise UnknownHiveError
# Setup the config entry
_LOGGER.debug("Setting up Hive entry")
self.data["tokens"] = self.tokens
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
@@ -160,6 +177,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
}
_LOGGER.debug("Reauthenticating user")
return await self.async_step_user(data)
@staticmethod

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["hyponcloud==0.3.0"]
"requirements": ["hyponcloud==0.9.0"]
}

View File

@@ -21,11 +21,17 @@ from .coordinator import HypontechConfigEntry, HypontechDataCoordinator
from .entity import HypontechEntity, HypontechPlantEntity
def _power_unit(data: OverviewData | PlantData) -> str:
"""Return the unit of measurement for power based on the API unit."""
return UnitOfPower.KILO_WATT if data.company.upper() == "KW" else UnitOfPower.WATT
@dataclass(frozen=True, kw_only=True)
class HypontechSensorDescription(SensorEntityDescription):
"""Describes Hypontech overview sensor entity."""
value_fn: Callable[[OverviewData], float | None]
unit_fn: Callable[[OverviewData], str] | None = None
@dataclass(frozen=True, kw_only=True)
@@ -33,15 +39,16 @@ class HypontechPlantSensorDescription(SensorEntityDescription):
"""Describes Hypontech plant sensor entity."""
value_fn: Callable[[PlantData], float | None]
unit_fn: Callable[[PlantData], str] | None = None
OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
HypontechSensorDescription(
key="pv_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.power,
unit_fn=_power_unit,
),
HypontechSensorDescription(
key="lifetime_energy",
@@ -64,10 +71,10 @@ OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
PLANT_SENSORS: tuple[HypontechPlantSensorDescription, ...] = (
HypontechPlantSensorDescription(
key="pv_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.power,
unit_fn=_power_unit,
),
HypontechPlantSensorDescription(
key="lifetime_energy",
@@ -124,6 +131,13 @@ class HypontechOverviewSensor(HypontechEntity, SensorEntity):
self.entity_description = description
self._attr_unique_id = f"{coordinator.account_id}_{description.key}"
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self.entity_description.unit_fn is not None:
return self.entity_description.unit_fn(self.coordinator.data.overview)
return super().native_unit_of_measurement
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
@@ -146,6 +160,13 @@ class HypontechPlantSensor(HypontechPlantEntity, SensorEntity):
self.entity_description = description
self._attr_unique_id = f"{plant_id}_{description.key}"
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self.entity_description.unit_fn is not None:
return self.entity_description.unit_fn(self.plant)
return super().native_unit_of_measurement
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""

View File

@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
IntellifireSensorEntityDescription(
key="timer_end_timestamp",
translation_key="timer_end_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=_time_remaining_to_timestamp,
),

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==2.0.1"]
"requirements": ["pyjvcprojector==2.0.3"]
}

View File

@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
"""Representation of an LG soundbar device."""
_attr_should_poll = False
_attr_state = MediaPlayerState.ON
_attr_state = MediaPlayerState.OFF
_attr_supported_features = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE
@@ -79,6 +79,8 @@ class LGDevice(MediaPlayerEntity):
self._treble = 0
self._device = None
self._support_play_control = False
self._device_on = False
self._stream_type = 0
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)}, name=host
)
@@ -113,6 +115,7 @@ class LGDevice(MediaPlayerEntity):
if "i_curr_func" in data:
self._function = data["i_curr_func"]
if "b_powerstatus" in data:
self._device_on = data["b_powerstatus"]
if data["b_powerstatus"]:
self._attr_state = MediaPlayerState.ON
else:
@@ -157,17 +160,34 @@ class LGDevice(MediaPlayerEntity):
def _update_playinfo(self, data: dict[str, Any]) -> None:
"""Update the player info."""
if "i_stream_type" in data:
if self._stream_type != data["i_stream_type"]:
self._stream_type = data["i_stream_type"]
# Ask device for current play info when stream type changed.
self._device.get_play()
if data["i_stream_type"] == 0:
# If the stream type is 0 (aka the soundbar is used as an actual soundbar)
# the last track info should be cleared and the state should only be on or off,
# as all playing/paused are not applicable in this mode
self._attr_media_image_url = None
self._attr_media_artist = None
self._attr_media_title = None
if self._device_on:
self._attr_state = MediaPlayerState.ON
else:
self._attr_state = MediaPlayerState.OFF
if "i_play_ctrl" in data:
if data["i_play_ctrl"] == 0:
self._attr_state = MediaPlayerState.PLAYING
else:
self._attr_state = MediaPlayerState.PAUSED
if self._device_on and self._stream_type != 0:
if data["i_play_ctrl"] == 0:
self._attr_state = MediaPlayerState.PLAYING
else:
self._attr_state = MediaPlayerState.PAUSED
if "s_albumart" in data:
self._attr_media_image_url = data["s_albumart"]
self._attr_media_image_url = data["s_albumart"].strip() or None
if "s_artist" in data:
self._attr_media_artist = data["s_artist"]
self._attr_media_artist = data["s_artist"].strip() or None
if "s_title" in data:
self._attr_media_title = data["s_title"]
self._attr_media_title = data["s_title"].strip() or None
if "b_support_play_ctrl" in data:
self._support_play_control = data["b_support_play_ctrl"]

View File

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

View File

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

View File

@@ -80,6 +80,7 @@ class MatterUpdate(MatterEntity, UpdateEntity):
# Matter server.
_attr_should_poll = True
_software_update: MatterSoftwareVersion | None = None
_installed_software_version: int | None = None
_cancel_update: CALLBACK_TYPE | None = None
_attr_supported_features = (
UpdateEntityFeature.INSTALL
@@ -92,6 +93,9 @@ class MatterUpdate(MatterEntity, UpdateEntity):
def _update_from_device(self) -> None:
"""Update from device."""
self._installed_software_version = self.get_matter_attribute_value(
clusters.BasicInformation.Attributes.SoftwareVersion
)
self._attr_installed_version = self.get_matter_attribute_value(
clusters.BasicInformation.Attributes.SoftwareVersionString
)
@@ -123,6 +127,22 @@ class MatterUpdate(MatterEntity, UpdateEntity):
else:
self._attr_update_percentage = None
def _format_latest_version(
self, update_information: MatterSoftwareVersion
) -> str | None:
"""Return the version string to expose in Home Assistant."""
latest_version = update_information.software_version_string
if self._installed_software_version is None:
return latest_version
if update_information.software_version == self._installed_software_version:
return self._attr_installed_version or latest_version
if latest_version == self._attr_installed_version:
return f"{latest_version} ({update_information.software_version})"
return latest_version
async def async_update(self) -> None:
"""Call when the entity needs to be updated."""
try:
@@ -130,11 +150,13 @@ class MatterUpdate(MatterEntity, UpdateEntity):
node_id=self._endpoint.node.node_id
)
if not update_information:
self._software_update = None
self._attr_latest_version = self._attr_installed_version
self._attr_release_url = None
return
self._software_update = update_information
self._attr_latest_version = update_information.software_version_string
self._attr_latest_version = self._format_latest_version(update_information)
self._attr_release_url = update_information.release_notes_url
except UpdateCheckError as err:
@@ -212,7 +234,12 @@ class MatterUpdate(MatterEntity, UpdateEntity):
software_version: str | int | None = version
if self._software_update is not None and (
version is None or version == self._software_update.software_version_string
version is None
or version
in {
self._software_update.software_version_string,
self._attr_latest_version,
}
):
# Update to the version previously fetched and shown.
# We can pass the integer version directly to speedup download.

View File

@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
finished = 522, 11012
extra_dry = 523
hand_iron = 524
hygiene_drying = 525
moisten = 526
thermo_spin = 527
timed_drying = 528
@@ -615,8 +616,10 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
pyrolytic = 323
descale = 326
evaporate_water = 327
rinse = 333
shabbat_program = 335
yom_tov = 336
hydroclean = 341
drying = 357, 2028
heat_crockery = 358
prove_dough = 359, 2023
@@ -721,7 +724,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
belgian_sponge_cake = 624
goose_unstuffed = 625
rack_of_lamb_with_vegetables = 634
yorkshire_pudding = 635
yorkshire_pudding = 635, 2352
meat_loaf = 636
defrost_meat = 647
defrost_vegetables = 654
@@ -1121,7 +1124,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
wholegrain_rice = 3376
parboiled_rice_steam_cooking = 3380
parboiled_rice_rapid_steam_cooking = 3381
basmati_rice_steam_cooking = 3383
basmati_rice_steam_cooking = 3382, 3383
basmati_rice_rapid_steam_cooking = 3384
jasmine_rice_steam_cooking = 3386
jasmine_rice_rapid_steam_cooking = 3387
@@ -1129,7 +1132,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
huanghuanian_rapid_steam_cooking = 3390
simiao_steam_cooking = 3392
simiao_rapid_steam_cooking = 3393
long_grain_rice_general_steam_cooking = 3395
long_grain_rice_general_steam_cooking = 3394, 3395
long_grain_rice_general_rapid_steam_cooking = 3396
chongming_steam_cooking = 3398
chongming_rapid_steam_cooking = 3399

View File

@@ -560,6 +560,7 @@
"hot_water": "Hot water",
"huanghuanian_rapid_steam_cooking": "Huanghuanian (rapid steam cooking)",
"huanghuanian_steam_cooking": "Huanghuanian (steam cooking)",
"hydroclean": "HydroClean",
"hygiene": "Hygiene",
"intensive": "Intensive",
"intensive_bake": "Intensive bake",
@@ -1006,6 +1007,7 @@
"heating_up_phase": "Heating up phase",
"hot_milk": "Hot milk",
"hygiene": "Hygiene",
"hygiene_drying": "Hygiene drying",
"interim_rinse": "Interim rinse",
"keep_warm": "Keep warm",
"keeping_warm": "Keeping warm",

View File

@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
latitude: float | None
longitude: float | None
gps_accuracy: float
# Reset manually set location to allow automatic zone detection
self._attr_location_name = None
if isinstance(
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
) and isinstance(

View File

@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
return {
"device_info": client.device_info,
"vehicles": client.vehicles,
"ct_connected": client.ct_connected,
"cap_available": client.cap_available,
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.6.0"]
"requirements": ["ohme==1.7.0"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.5"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.5"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.17.0"]
"requirements": ["opower==0.17.1"]
}

View File

@@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["oralb_ble"],
"requirements": ["oralb-ble==1.0.2"]
"requirements": ["oralb-ble==1.1.0"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.8.0"]
"requirements": ["python-otbr-api==2.9.0"]
}

View File

@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
self._abort_if_unique_id_configured()
# Logic that can be reverted back once the new unique ID is in
existing_entry = await self.async_set_unique_id(
user_input[CONF_API_TOKEN]
)
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
return self.async_abort(reason="already_configured")
return self.async_update_reload_and_abort(
reconf_entry,
data_updates={

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.31"]
"requirements": ["pyportainer==1.0.33"]
}

View File

@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
ProxmoxVMButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
ProxmoxContainerButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
@@ -277,7 +277,8 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the node button action via executor."""
if not is_granted(self.coordinator.permissions, p_type="nodes"):
node_id = self._node_data.node["node"]
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_node_power",
@@ -285,7 +286,7 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
self._node_data.node["node"],
node_id,
)
@@ -309,7 +310,8 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the VM button action via executor."""
if not is_granted(self.coordinator.permissions, p_type="vms"):
vmid = self.vm_data["vmid"]
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_vm_lxc_power",
@@ -318,7 +320,7 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
self.entity_description.press_action,
self.coordinator,
self._node_name,
self.vm_data["vmid"],
vmid,
)
@@ -342,8 +344,9 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the container button action via executor."""
vmid = self.container_data["vmid"]
# Container power actions fall under vms
if not is_granted(self.coordinator.permissions, p_type="vms"):
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_vm_lxc_power",
@@ -352,5 +355,5 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
self.entity_description.press_action,
self.coordinator,
self._node_name,
self.container_data["vmid"],
vmid,
)

View File

@@ -6,8 +6,13 @@ from .const import PERM_POWER
def is_granted(
permissions: dict[str, dict[str, int]],
p_type: str = "vms",
p_id: str | int | None = None, # can be str for nodes
permission: str = PERM_POWER,
) -> bool:
"""Validate user permissions for the given type and permission."""
path = f"/{p_type}"
return permissions.get(path, {}).get(permission) == 1
paths = [f"/{p_type}/{p_id}", f"/{p_type}", "/"]
for path in paths:
value = permissions.get(path, {}).get(permission)
if value is not None:
return value == 1
return False

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -34,5 +34,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.6.0"]
"requirements": ["pysmartthings==3.7.2"]
}

View File

@@ -43,6 +43,10 @@ async def async_setup_entry(
for component in device.status
if component in ("cooler", "freezer", "onedoor")
and Capability.THERMOSTAT_COOLING_SETPOINT in device.status[component]
and device.status[component][Capability.THERMOSTAT_COOLING_SETPOINT][
Attribute.COOLING_SETPOINT_RANGE
].value
is not None
)
async_add_entities(entities)

View File

@@ -1,4 +1,4 @@
"""Support for SLZB-06 buttons."""
"""Support for SLZB buttons."""
from __future__ import annotations
@@ -35,24 +35,25 @@ class SmButtonDescription(ButtonEntityDescription):
press_fn: Callable[[CmdWrapper, int], Awaitable[None]]
BUTTONS: list[SmButtonDescription] = [
SmButtonDescription(
key="core_restart",
translation_key="core_restart",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda cmd, idx: cmd.reboot(),
),
CORE_BUTTON = SmButtonDescription(
key="core_restart",
translation_key="core_restart",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda cmd, idx: cmd.reboot(),
)
RADIO_BUTTONS: list[SmButtonDescription] = [
SmButtonDescription(
key="zigbee_restart",
translation_key="zigbee_restart",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda cmd, idx: cmd.zb_restart(),
press_fn=lambda cmd, idx: cmd.zb_restart(idx=idx),
),
SmButtonDescription(
key="zigbee_flash_mode",
translation_key="zigbee_flash_mode",
entity_registry_enabled_default=False,
press_fn=lambda cmd, idx: cmd.zb_bootloader(),
press_fn=lambda cmd, idx: cmd.zb_bootloader(idx=idx),
),
]
@@ -73,7 +74,13 @@ async def async_setup_entry(
coordinator = entry.runtime_data.data
radios = coordinator.data.info.radios
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
entities = [SmButton(coordinator, CORE_BUTTON)]
count = len(radios) if coordinator.data.info.u_device else 1
for idx in range(count):
entities.extend(SmButton(coordinator, button, idx) for button in RADIO_BUTTONS)
async_add_entities(entities)
entity_created = [False] * len(radios)
@callback
@@ -103,7 +110,7 @@ async def async_setup_entry(
class SmButton(SmEntity, ButtonEntity):
"""Defines a SLZB-06 button."""
"""Defines a SLZB button."""
coordinator: SmDataUpdateCoordinator
entity_description: SmButtonDescription
@@ -115,7 +122,7 @@ class SmButton(SmEntity, ButtonEntity):
description: SmButtonDescription,
idx: int = 0,
) -> None:
"""Initialize SLZB-06 button entity."""
"""Initialize SLZB button entity."""
super().__init__(coordinator)
self.entity_description = description

View File

@@ -12,7 +12,7 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pysmlight==0.2.16"],
"requirements": ["pysmlight==0.3.1"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."

View File

@@ -290,16 +290,29 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
and entity.unique_id != self.unique_id
]
# Get unique ID prefix for this host
unique_id_prefix = self.get_unique_id(self.coordinator.host_id, "")
for client in clients:
# Valid entity is a snapcast client
# Validate entity is a snapcast client
if not client.unique_id.startswith(CLIENT_PREFIX):
raise ServiceValidationError(
f"Entity '{client.entity_id}' is not a Snapcast client device."
)
# Validate client belongs to the same server
if not client.unique_id.startswith(unique_id_prefix):
raise ServiceValidationError(
f"Entity '{client.entity_id}' does not belong to the same Snapcast server."
)
# Extract client ID and join it to the current group
identifier = client.unique_id.split("_")[-1]
await self._current_group.add_client(identifier)
identifier = client.unique_id.removeprefix(unique_id_prefix)
try:
await self._current_group.add_client(identifier)
except KeyError as e:
raise ServiceValidationError(
f"Client with identifier '{identifier}' does not exist on the server."
) from e
self.async_write_ha_state()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/starlink",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["starlink-grpc-core==1.2.3"]
"requirements": ["starlink-grpc-core==1.2.4"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["aiotedee"],
"quality_scale": "platinum",
"requirements": ["aiotedee==0.2.25"]
"requirements": ["aiotedee==0.2.27"]
}

View File

@@ -80,6 +80,16 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
SCRIPT_FIELDS = (
CONF_ARM_AWAY_ACTION,
CONF_ARM_CUSTOM_BYPASS_ACTION,
CONF_ARM_HOME_ACTION,
CONF_ARM_NIGHT_ACTION,
CONF_ARM_VACATION_ACTION,
CONF_DISARM_ACTION,
CONF_TRIGGER_ACTION,
)
DEFAULT_NAME = "Template Alarm Control Panel"
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
@@ -152,6 +162,7 @@ async def async_setup_entry(
StateAlarmControlPanelEntity,
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)
@@ -172,6 +183,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_ALARM_CONTROL_PANELS,
script_options=SCRIPT_FIELDS,
)

View File

@@ -36,6 +36,8 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Template Button"
DEFAULT_OPTIMISTIC = False
SCRIPT_FIELDS = (CONF_PRESS,)
BUTTON_YAML_SCHEMA = vol.Schema(
{
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
@@ -66,6 +68,7 @@ async def async_setup_platform(
None,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -81,6 +84,7 @@ async def async_setup_entry(
async_add_entities,
StateButtonEntity,
BUTTON_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -71,6 +71,14 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
CONF_OPEN_AND_CLOSE = "open_or_close"
SCRIPT_FIELDS = (
CLOSE_ACTION,
OPEN_ACTION,
POSITION_ACTION,
STOP_ACTION,
TILT_ACTION,
)
TILT_FEATURES = (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
@@ -165,6 +173,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_COVERS,
script_options=SCRIPT_FIELDS,
)
@@ -181,6 +190,7 @@ async def async_setup_entry(
StateCoverEntity,
COVER_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)

View File

@@ -87,6 +87,15 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Fan"
SCRIPT_FIELDS = (
CONF_OFF_ACTION,
CONF_ON_ACTION,
CONF_SET_DIRECTION_ACTION,
CONF_SET_OSCILLATING_ACTION,
CONF_SET_PERCENTAGE_ACTION,
CONF_SET_PRESET_MODE_ACTION,
)
FAN_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DIRECTION): cv.template,
@@ -159,6 +168,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_FANS,
script_options=SCRIPT_FIELDS,
)
@@ -174,6 +184,7 @@ async def async_setup_entry(
async_add_entities,
StateFanEntity,
FAN_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -8,6 +8,7 @@ import logging
from typing import Any
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.components import blueprint
from homeassistant.config_entries import ConfigEntry
@@ -25,7 +26,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers import issue_registry as ir, template
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import (
@@ -34,6 +35,7 @@ from homeassistant.helpers.entity_platform import (
async_get_platforms,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.script import async_validate_actions_config
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -208,6 +210,21 @@ def _format_template(value: Any, field: str | None = None) -> Any:
return str(value)
def _get_config_breadcrumbs(config: ConfigType) -> str:
"""Try to coerce entity information from the config."""
breadcrumb = "Template Entity"
# Default entity id should be in most legacy configuration because
# it's created from the legacy slug. Vacuum and Lock do not have a
# slug, therefore we need to use the name or unique_id.
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
breadcrumb = default_entity_id.split(".")[-1]
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
breadcrumb = f"unique_id: {unique_id}"
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
breadcrumb = name.template
return breadcrumb
def format_migration_config(
config: ConfigType | list[ConfigType], depth: int = 0
) -> ConfigType | list[ConfigType]:
@@ -252,16 +269,7 @@ def create_legacy_template_issue(
if domain not in PLATFORMS:
return
breadcrumb = "Template Entity"
# Default entity id should be in most legacy configuration because
# it's created from the legacy slug. Vacuum and Lock do not have a
# slug, therefore we need to use the name or unique_id.
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
breadcrumb = default_entity_id.split(".")[-1]
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
breadcrumb = f"unique_id: {unique_id}"
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
breadcrumb = name.template
breadcrumb = _get_config_breadcrumbs(config)
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
@@ -296,6 +304,39 @@ def create_legacy_template_issue(
)
async def validate_template_scripts(
hass: HomeAssistant,
config: ConfigType,
script_options: tuple[str, ...] | None = None,
) -> None:
"""Validate template scripts."""
if not script_options:
return
def _humanize(err: Exception, data: Any) -> str:
"""Humanize vol.Invalid, stringify other exceptions."""
if isinstance(err, vol.Invalid):
return humanize_error(data, err)
return str(err)
breadcrumb: str | None = None
for script_option in script_options:
if (script_config := config.pop(script_option, None)) is not None:
try:
config[script_option] = await async_validate_actions_config(
hass, script_config
)
except (vol.Invalid, HomeAssistantError) as err:
if not breadcrumb:
breadcrumb = _get_config_breadcrumbs(config)
_LOGGER.error(
"The '%s' actions for %s failed to setup: %s",
script_option,
breadcrumb,
_humanize(err, script_config),
)
async def async_setup_template_platform(
hass: HomeAssistant,
domain: str,
@@ -306,6 +347,7 @@ async def async_setup_template_platform(
discovery_info: DiscoveryInfoType | None,
legacy_fields: dict[str, str] | None = None,
legacy_key: str | None = None,
script_options: tuple[str, ...] | None = None,
) -> None:
"""Set up the Template platform."""
if discovery_info is None:
@@ -337,10 +379,14 @@ async def async_setup_template_platform(
# Trigger Configuration
if "coordinator" in discovery_info:
if trigger_entity_cls:
entities = [
trigger_entity_cls(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
]
entities = []
for entity_config in discovery_info["entities"]:
await validate_template_scripts(hass, entity_config, script_options)
entities.append(
trigger_entity_cls(
hass, discovery_info["coordinator"], entity_config
)
)
async_add_entities(entities)
else:
raise PlatformNotReady(
@@ -349,6 +395,9 @@ async def async_setup_template_platform(
return
# Modern Configuration
for entity_config in discovery_info["entities"]:
await validate_template_scripts(hass, entity_config, script_options)
async_create_template_tracking_entities(
state_entity_cls,
async_add_entities,
@@ -365,6 +414,7 @@ async def async_setup_template_entry(
state_entity_cls: type[TemplateEntity],
config_schema: vol.Schema | vol.All,
replace_value_template: bool = False,
script_options: tuple[str, ...] | None = None,
) -> None:
"""Setup the Template from a config entry."""
options = dict(config_entry.options)
@@ -377,6 +427,7 @@ async def async_setup_template_entry(
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
validated_config = config_schema(options)
await validate_template_scripts(hass, validated_config, script_options)
async_add_entities(
[state_entity_cls(hass, validated_config, config_entry.entry_id)]

View File

@@ -129,6 +129,18 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Light"
SCRIPT_FIELDS = (
CONF_EFFECT_ACTION,
CONF_HS_ACTION,
CONF_LEVEL_ACTION,
CONF_OFF_ACTION,
CONF_ON_ACTION,
CONF_RGB_ACTION,
CONF_RGBW_ACTION,
CONF_RGBWW_ACTION,
CONF_TEMPERATURE_ACTION,
)
LIGHT_COMMON_SCHEMA = vol.Schema(
{
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
@@ -142,8 +154,6 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
vol.Optional(CONF_MIN_MIREDS): cv.template,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGB): cv.template,
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
@@ -226,6 +236,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_LIGHTS,
script_options=SCRIPT_FIELDS,
)
@@ -242,6 +253,7 @@ async def async_setup_entry(
StateLightEntity,
LIGHT_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)

View File

@@ -64,6 +64,13 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
SCRIPT_FIELDS = (
CONF_LOCK,
CONF_OPEN,
CONF_UNLOCK,
)
LOCK_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CODE_FORMAT): cv.template,
@@ -112,6 +119,7 @@ async def async_setup_platform(
async_add_entities,
discovery_info,
LEGACY_FIELDS,
script_options=SCRIPT_FIELDS,
)
@@ -127,6 +135,7 @@ async def async_setup_entry(
async_add_entities,
StateLockEntity,
LOCK_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -46,6 +46,8 @@ CONF_SET_VALUE = "set_value"
DEFAULT_NAME = "Template Number"
DEFAULT_OPTIMISTIC = False
SCRIPT_FIELDS = (CONF_SET_VALUE,)
NUMBER_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
@@ -81,6 +83,7 @@ async def async_setup_platform(
TriggerNumberEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -96,6 +99,7 @@ async def async_setup_entry(
async_add_entities,
StateNumberEntity,
NUMBER_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -47,6 +47,8 @@ CONF_SELECT_OPTION = "select_option"
DEFAULT_NAME = "Template Select"
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
SELECT_COMMON_SCHEMA = vol.Schema(
{
vol.Required(ATTR_OPTIONS): cv.template,
@@ -79,6 +81,7 @@ async def async_setup_platform(
TriggerSelectEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -94,6 +97,7 @@ async def async_setup_entry(
async_add_entities,
TemplateSelect,
SELECT_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -57,11 +57,16 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Switch"
SCRIPT_FIELDS = (
CONF_TURN_OFF,
CONF_TURN_ON,
)
SWITCH_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
}
)
@@ -109,6 +114,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_SWITCHES,
script_options=SCRIPT_FIELDS,
)
@@ -125,6 +131,7 @@ async def async_setup_entry(
StateSwitchEntity,
SWITCH_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)

View File

@@ -65,6 +65,8 @@ CONF_SPECIFIC_VERSION = "specific_version"
CONF_TITLE = "title"
CONF_UPDATE_PERCENTAGE = "update_percentage"
SCRIPT_FIELDS = (CONF_INSTALL,)
UPDATE_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
@@ -105,6 +107,7 @@ async def async_setup_platform(
TriggerUpdateEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -120,6 +123,7 @@ async def async_setup_entry(
async_add_entities,
StateUpdateEntity,
UPDATE_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -76,6 +76,16 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
SCRIPT_FIELDS = (
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
SERVICE_RETURN_TO_BASE,
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
)
VACUUM_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
@@ -150,6 +160,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_VACUUMS,
script_options=SCRIPT_FIELDS,
)
@@ -165,6 +176,7 @@ async def async_setup_entry(
async_add_entities,
TemplateStateVacuumEntity,
VACUUM_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -2,9 +2,10 @@
from __future__ import annotations
from datetime import timedelta
import asyncio
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, TypedDict, cast
from aiohttp.client_exceptions import ClientError
import tibber
@@ -38,6 +39,58 @@ FIVE_YEARS = 5 * 365 * 24
_LOGGER = logging.getLogger(__name__)
class TibberHomeData(TypedDict):
"""Data for a Tibber home used by the price sensor."""
currency: str
price_unit: str
current_price: float | None
current_price_time: datetime | None
intraday_price_ranking: float | None
max_price: float
avg_price: float
min_price: float
off_peak_1: float
peak: float
off_peak_2: float
month_cost: float | None
peak_hour: float | None
peak_hour_time: datetime | None
month_cons: float | None
app_nickname: str | None
grid_company: str | None
estimated_annual_consumption: int | None
def _build_home_data(home: tibber.TibberHome) -> TibberHomeData:
"""Build TibberHomeData from a TibberHome for the price sensor."""
current_price, last_updated, price_rank = home.current_price_data()
attributes = home.current_attributes()
result: TibberHomeData = {
"currency": home.currency,
"price_unit": home.price_unit,
"current_price": current_price,
"current_price_time": last_updated,
"intraday_price_ranking": price_rank,
"max_price": attributes["max_price"],
"avg_price": attributes["avg_price"],
"min_price": attributes["min_price"],
"off_peak_1": attributes["off_peak_1"],
"peak": attributes["peak"],
"off_peak_2": attributes["off_peak_2"],
"month_cost": home.month_cost,
"peak_hour": home.peak_hour,
"peak_hour_time": home.peak_hour_time,
"month_cons": home.month_cons,
"app_nickname": home.info["viewer"]["home"].get("appNickname"),
"grid_company": home.info["viewer"]["home"]["meteringPointData"]["gridCompany"],
"estimated_annual_consumption": home.info["viewer"]["home"][
"meteringPointData"
]["estimatedAnnualConsumption"],
}
return result
class TibberDataCoordinator(DataUpdateCoordinator[None]):
"""Handle Tibber data and insert statistics."""
@@ -57,13 +110,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
name=f"Tibber {tibber_connection.name}",
update_interval=timedelta(minutes=20),
)
self._tibber_connection = tibber_connection
async def _async_update_data(self) -> None:
"""Update data via API."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
try:
await self._tibber_connection.fetch_consumption_data_active_homes()
await self._tibber_connection.fetch_production_data_active_homes()
await tibber_connection.fetch_consumption_data_active_homes()
await tibber_connection.fetch_production_data_active_homes()
await self._insert_statistics()
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
@@ -75,7 +131,10 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async def _insert_statistics(self) -> None:
"""Insert Tibber statistics."""
for home in self._tibber_connection.get_homes():
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
for home in tibber_connection.get_homes():
sensors: list[tuple[str, bool, str | None, str]] = []
if home.hourly_consumption_data:
sensors.append(
@@ -194,6 +253,76 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async_add_external_statistics(self.hass, metadata, statistics)
class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
"""Handle Tibber price data and insert statistics."""
config_entry: TibberConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: TibberConfigEntry,
) -> None:
"""Initialize the price coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN} price",
update_interval=timedelta(minutes=1),
)
def _seconds_until_next_15_minute(self) -> float:
"""Return seconds until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
now = dt_util.utcnow()
next_minute = ((now.minute // 15) + 1) * 15
if next_minute >= 60:
next_run = now.replace(minute=0, second=0, microsecond=0) + timedelta(
hours=1
)
else:
next_run = now.replace(
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
)
return (next_run - now).total_seconds()
async def _async_update_data(self) -> dict[str, TibberHomeData]:
"""Update data via API and return per-home data for sensors."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
active_homes = tibber_connection.get_homes(only_active=True)
try:
await asyncio.gather(
tibber_connection.fetch_consumption_data_active_homes(),
tibber_connection.fetch_production_data_active_homes(),
)
now = dt_util.now()
homes_to_update = [
home
for home in active_homes
if (
(last_data_timestamp := home.last_data_timestamp) is None
or (last_data_timestamp - now).total_seconds() < 11 * 3600
)
]
if homes_to_update:
await asyncio.gather(
*(home.update_info_and_price_info() for home in homes_to_update)
)
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
except tibber.FatalHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
result = {home.home_id: _build_home_data(home) for home in active_homes}
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
return result
class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
"""Fetch and cache Tibber Data API device capabilities."""

View File

@@ -3,10 +3,8 @@
from __future__ import annotations
from collections.abc import Callable
import datetime
from datetime import timedelta
import logging
from random import randrange
from typing import Any
import aiohttp
@@ -42,18 +40,20 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util import Throttle, dt as dt_util
from homeassistant.util import dt as dt_util
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
from .coordinator import (
TibberDataAPICoordinator,
TibberDataCoordinator,
TibberPriceCoordinator,
)
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:currency-usd"
SCAN_INTERVAL = timedelta(minutes=1)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PARALLEL_UPDATES = 0
TWENTY_MINUTES = 20 * 60
RT_SENSORS_UNIQUE_ID_MIGRATION = {
"accumulated_consumption_last_hour": "accumulated consumption current hour",
@@ -610,6 +610,7 @@ async def _async_setup_graphql_sensors(
entity_registry = er.async_get(hass)
coordinator: TibberDataCoordinator | None = None
price_coordinator: TibberPriceCoordinator | None = None
entities: list[TibberSensor] = []
for home in tibber_connection.get_homes(only_active=False):
try:
@@ -626,7 +627,9 @@ async def _async_setup_graphql_sensors(
raise PlatformNotReady from err
if home.has_active_subscription:
entities.append(TibberSensorElPrice(home))
if price_coordinator is None:
price_coordinator = TibberPriceCoordinator(hass, entry)
entities.append(TibberSensorElPrice(price_coordinator, home))
if coordinator is None:
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
entities.extend(
@@ -737,19 +740,21 @@ class TibberSensor(SensorEntity):
return device_info
class TibberSensorElPrice(TibberSensor):
class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator]):
"""Representation of a Tibber sensor for el price."""
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_translation_key = "electricity_price"
def __init__(self, tibber_home: TibberHome) -> None:
def __init__(
self,
coordinator: TibberPriceCoordinator,
tibber_home: TibberHome,
) -> None:
"""Initialize the sensor."""
super().__init__(tibber_home=tibber_home)
self._last_updated: datetime.datetime | None = None
self._spread_load_constant = randrange(TWENTY_MINUTES)
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
self._attr_available = False
self._attr_native_unit_of_measurement = tibber_home.price_unit
self._attr_extra_state_attributes = {
"app_nickname": None,
"grid_company": None,
@@ -768,51 +773,38 @@ class TibberSensorElPrice(TibberSensor):
self._device_name = self._home_name
async def async_update(self) -> None:
"""Get the latest data and updates the states."""
now = dt_util.now()
if (
not self._tibber_home.last_data_timestamp
or (self._tibber_home.last_data_timestamp - now).total_seconds()
< 10 * 3600 - self._spread_load_constant
or not self.available
):
_LOGGER.debug("Asking for new data")
await self._fetch_data()
elif (
self._tibber_home.price_total
and self._last_updated
and self._last_updated.hour == now.hour
and now - self._last_updated < timedelta(minutes=15)
and self._tibber_home.last_data_timestamp
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
data = self.coordinator.data
if not data or (
(home_data := data.get(self._tibber_home.home_id)) is None
or (current_price := home_data.get("current_price")) is None
):
self._attr_available = False
self.async_write_ha_state()
return
res = self._tibber_home.current_price_data()
self._attr_native_value, self._last_updated, price_rank = res
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
attrs = self._tibber_home.current_attributes()
self._attr_extra_state_attributes.update(attrs)
self._attr_available = self._attr_native_value is not None
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self) -> None:
_LOGGER.debug("Fetching data")
try:
await self._tibber_home.update_info_and_price_info()
except TimeoutError, aiohttp.ClientError:
return
data = self._tibber_home.info["viewer"]["home"]
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
"gridCompany"
self._attr_native_unit_of_measurement = home_data.get(
"price_unit", self._tibber_home.price_unit
)
self._attr_native_value = current_price
self._attr_extra_state_attributes["intraday_price_ranking"] = home_data.get(
"intraday_price_ranking"
)
self._attr_extra_state_attributes["max_price"] = home_data["max_price"]
self._attr_extra_state_attributes["avg_price"] = home_data["avg_price"]
self._attr_extra_state_attributes["min_price"] = home_data["min_price"]
self._attr_extra_state_attributes["off_peak_1"] = home_data["off_peak_1"]
self._attr_extra_state_attributes["peak"] = home_data["peak"]
self._attr_extra_state_attributes["off_peak_2"] = home_data["off_peak_2"]
self._attr_extra_state_attributes["app_nickname"] = home_data["app_nickname"]
self._attr_extra_state_attributes["grid_company"] = home_data["grid_company"]
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
"estimated_annual_consumption"
]
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
"meteringPointData"
]["estimatedAnnualConsumption"]
self._attr_available = True
self.async_write_ha_state()
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["tplink-omada-client==1.5.3"]
"requirements": ["tplink-omada-client==1.5.6"]
}

View File

@@ -81,6 +81,7 @@ clean_area:
selector:
area:
multiple: true
reorder: true
send_command:
target:

View File

@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
Keys.WARNING: VictronBLESensorEntityDescription(
key=Keys.WARNING,
device_class=SensorDeviceClass.ENUM,
translation_key="alarm",
translation_key="warning",
options=ALARM_OPTIONS,
),
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
@@ -410,7 +410,7 @@ SENSOR_DESCRIPTIONS = {
),
}
for i in range(1, 8):
for i in range(1, 9):
cell_key = getattr(Keys, f"CELL_{i}_VOLTAGE")
SENSOR_DESCRIPTIONS[cell_key] = VictronBLESensorEntityDescription(
key=cell_key,
@@ -418,6 +418,7 @@ for i in range(1, 8):
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
translation_placeholders={"cell": str(i)},
)

View File

@@ -248,7 +248,24 @@
"name": "[%key:component::victron_ble::common::starter_voltage%]"
},
"warning": {
"name": "Warning"
"name": "Warning",
"state": {
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
}
},
"yield_today": {
"name": "Yield today"

View File

@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
data,
session,
)
self._session = session
# Last resort as no MAC or S/N can be retrieved via API
self._id = config_entry.unique_id
@@ -135,11 +136,15 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
try:
await self.api.login()
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
_LOGGER.debug(
"Session cookies missing for host %s, re-login",
self.api.base_url.host,
)
await self.api.login()
raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data()
data_wifi = await self.api.get_wifi_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -104,6 +104,7 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
await self.coordinator.api.set_wifi_status(
status, self.entity_description.typology, self.entity_description.band
)
await self.coordinator.async_request_refresh()
except CannotAuthenticate as err:
self.coordinator.config_entry.async_start_reauth(self.hass)
raise HomeAssistantError(

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["wolf_comm"],
"requirements": ["wolf-comm==0.0.23"]
"requirements": ["wolf-comm==0.0.48"]
}

View File

@@ -25,5 +25,5 @@
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==1.6.0"]
"requirements": ["xiaomi-ble==1.10.0"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.const import Brand
from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND
@@ -15,7 +15,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -42,11 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_yale(hass, entry, yale_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
except (YaleApiError, ClientResponseError, CannotConnect) as err:
except (
YaleApiError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
}

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["yalexs-ble==3.2.7"]
"requirements": ["yalexs-ble==3.2.8"]
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/yolink",
"integration_type": "hub",
"iot_class": "cloud_push",
"requirements": ["yolink-api==0.6.1"]
"requirements": ["yolink-api==0.6.3"]
}

View File

@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.0.1", "serialx==0.6.2"],
"requirements": ["zha==1.0.2", "serialx==0.6.2"],
"usb": [
{
"description": "*2652*",

View File

@@ -9,6 +9,7 @@ from zwave_js_server.const import (
SET_VALUE_SUCCESS,
TARGET_STATE_PROPERTY,
TARGET_VALUE_PROPERTY,
CommandClass,
SetValueStatus,
)
from zwave_js_server.const.command_class.barrier_operator import BarrierState
@@ -87,6 +88,8 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
_current_position_value: ZwaveValue | None = None
_target_position_value: ZwaveValue | None = None
_stop_position_value: ZwaveValue | None = None
# Remember whether the moving state can be tracked reliably for this device.
_moving_state_disabled: bool = False
def _set_position_values(
self,
@@ -153,11 +156,13 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
if not self._attr_is_opening and not self._attr_is_closing:
return
if (current := self._current_position_value) is None or current.value is None:
return
if (
(current := self._current_position_value) is not None
and (target := self._target_position_value) is not None
and current.value is not None
and current.value == target.value
(t := self._target_position_value) is not None
and t.value is not None
and current.value == t.value
):
self._attr_is_opening = False
self._attr_is_closing = False
@@ -182,9 +187,10 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
self._target_position_value, target_position
)
if (
self._moving_state_disabled
# If the command is unsupervised, or the device reported that it started
# working, we can assume the cover is moving in the desired direction.
result is None
or result is None
or result.status
not in (SetValueStatus.WORKING, SetValueStatus.SUCCESS_UNSUPERVISED)
# If we don't know the current position, we don't know which direction
@@ -350,6 +356,17 @@ class ZWaveMultilevelSwitchCover(CoverPositionMixin):
),
)
# Multilevel Switch CC v3 and earlier don't report targetValue,
# so we cannot determine when the cover stops moving,
# especially when the device is controlled physically.
# OPENING/CLOSING states must not be used for these devices,
# because they will become stale/incorrect.
if (
self.info.primary_value.command_class == CommandClass.SWITCH_MULTILEVEL
and self.info.primary_value.cc_version < 4
):
self._moving_state_disabled = True
# Entity class attributes
self._attr_device_class = CoverDeviceClass.WINDOW
if (

View File

@@ -205,15 +205,13 @@ DISCOVERY_SCHEMAS = [
FanValueMapping(speeds=[(1, 33), (34, 67), (68, 99)]),
),
),
# GE/Jasco - In-Wall Smart Fan Controls
# GE/Jasco - In-Wall Smart Fan Controls - 14314 / ZW4002
ZWaveDiscoverySchema(
platform=Platform.FAN,
hint="has_fan_value_mapping",
manufacturer_id={0x0063},
product_id={
0x3131,
0x3337, # 14287 / 55258 / ZW4002
0x3533, # 58446 / ZWA4013
0x3138, # 14314 / ZW4002
},
product_type={0x4944},
@@ -222,6 +220,30 @@ DISCOVERY_SCHEMAS = [
FanValueMapping(speeds=[(1, 32), (33, 66), (67, 99)]),
),
),
# GE/Jasco - In-Wall Smart Fan Controls - 14287 / 55258 / ZW4002
ZWaveDiscoverySchema(
platform=Platform.FAN,
hint="has_fan_value_mapping",
manufacturer_id={0x0063},
product_id={0x3337},
product_type={0x4944},
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
data_template=FixedFanValueMappingDataTemplate(
FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]),
),
),
# GE/Jasco - In-Wall Smart Fan Controls - 58446 / ZWA4013
ZWaveDiscoverySchema(
platform=Platform.FAN,
hint="has_fan_value_mapping",
manufacturer_id={0x0063},
product_id={0x3533},
product_type={0x4944},
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
data_template=FixedFanValueMappingDataTemplate(
FanValueMapping(speeds=[(1, 25), (26, 50), (51, 75), (76, 99)]),
),
),
# Leviton ZW4SF fan controllers using switch multilevel CC
ZWaveDiscoverySchema(
platform=Platform.FAN,

View File

@@ -2245,9 +2245,10 @@ class ConfigEntries:
self._entries = entries
self.async_update_issues()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, self._async_scan_orphan_ignored_entries
)
if not self.hass.config.recovery_mode and not self.hass.config.safe_mode:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, self._async_scan_orphan_ignored_entries
)
async def _async_scan_orphan_ignored_entries(
self, event: Event[NoEventData]

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "1"
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, 14, 2)

View File

@@ -181,15 +181,24 @@ class RestoreStateData:
}
# Start with the currently registered states
stored_states = [
StoredState(
current_states_by_entity_id[entity_id],
entity.extra_restore_state_data,
now,
stored_states: list[StoredState] = []
for entity_id, entity in self.entities.items():
if entity_id not in current_states_by_entity_id:
continue
try:
extra_data = entity.extra_restore_state_data
except Exception:
_LOGGER.exception(
"Error getting extra restore state data for %s", entity_id
)
continue
stored_states.append(
StoredState(
current_states_by_entity_id[entity_id],
extra_data,
now,
)
)
for entity_id, entity in self.entities.items()
if entity_id in current_states_by_entity_id
]
expiration_time = now - STATE_EXPIRATION
for entity_id, stored_state in self.last_states.items():
@@ -219,6 +228,8 @@ class RestoreStateData:
)
except HomeAssistantError as exc:
_LOGGER.error("Error saving current states", exc_info=exc)
except Exception:
_LOGGER.exception("Unexpected error saving current states")
@callback
def async_setup_dump(self, *args: Any) -> None:
@@ -258,13 +269,15 @@ class RestoreStateData:
@callback
def async_restore_entity_removed(
self, entity_id: str, extra_data: ExtraStoredData | None
self,
entity_id: str,
state: State | None,
extra_data: ExtraStoredData | None,
) -> None:
"""Unregister this entity from saving state."""
# When an entity is being removed from hass, store its last state. This
# allows us to support state restoration if the entity is removed, then
# re-added while hass is still running.
state = self.hass.states.get(entity_id)
# To fully mimic all the attribute data types when loaded from storage,
# we're going to serialize it to JSON and then re-load it.
if state is not None:
@@ -287,8 +300,18 @@ class RestoreEntity(Entity):
async def async_internal_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
try:
extra_data = self.extra_restore_state_data
except Exception:
_LOGGER.exception(
"Error getting extra restore state data for %s", self.entity_id
)
state = None
extra_data = None
else:
state = self.hass.states.get(self.entity_id)
async_get(self.hass).async_restore_entity_removed(
self.entity_id, self.extra_restore_state_data
self.entity_id, state, extra_data
)
await super().async_internal_will_remove_from_hass()

View File

@@ -301,6 +301,7 @@ class AreaSelectorConfig(BaseSelectorConfig, total=False):
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
multiple: bool
reorder: bool
@SELECTORS.register("area")
@@ -320,6 +321,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("reorder", default=False): cv.boolean,
}
)

View File

@@ -40,7 +40,7 @@ habluetooth==5.8.0
hass-nabucasa==1.15.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.1
home-assistant-intents==2026.3.3
httpx==0.28.1
ifaddr==0.2.0
@@ -48,7 +48,7 @@ Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
openai==2.21.0
orjson==3.11.5
orjson==3.11.7
packaging>=23.1
paho-mqtt==2.1.0
Pillow==12.1.1
@@ -57,7 +57,7 @@ psutil-home-assistant==0.0.1
PyJWT==2.10.1
pymicro-vad==1.0.1
PyNaCl==1.6.2
pyOpenSSL==25.3.0
pyOpenSSL==26.0.0
pyserial==3.5
pyspeex-noise==1.0.2
python-slugify==8.0.4

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.3.1"
version = "2026.3.4"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -64,8 +64,8 @@ dependencies = [
"cryptography==46.0.5",
"Pillow==12.1.1",
"propcache==0.4.1",
"pyOpenSSL==25.3.0",
"orjson==3.11.5",
"pyOpenSSL==26.0.0",
"orjson==3.11.7",
"packaging>=23.1",
"psutil-home-assistant==0.0.1",
"python-slugify==8.0.4",

4
requirements.txt generated
View File

@@ -34,14 +34,14 @@ ifaddr==0.2.0
Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
orjson==3.11.5
orjson==3.11.7
packaging>=23.1
Pillow==12.1.1
propcache==0.4.1
psutil-home-assistant==0.0.1
PyJWT==2.10.1
pymicro-vad==1.0.1
pyOpenSSL==25.3.0
pyOpenSSL==26.0.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.0

56
requirements_all.txt generated
View File

@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.9
PyChromecast==14.0.10
# homeassistant.components.flume
PyFlume==0.6.5
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.0.0
aioamazondevices==13.0.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -224,7 +224,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==2.0.0
aiocomelit==2.0.1
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
@@ -422,7 +422,7 @@ aiosyncthing==0.7.1
aiotankerkoenig==0.5.1
# homeassistant.components.tedee
aiotedee==0.2.25
aiotedee==0.2.27
# homeassistant.components.tractive
aiotractive==1.0.0
@@ -593,7 +593,7 @@ avea==1.6.1
# avion==0.10
# homeassistant.components.axis
axis==66
axis==67
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.7
@@ -1122,7 +1122,7 @@ gotailwind==0.3.0
govee-ble==0.44.0
# homeassistant.components.govee_light_local
govee-local-api==2.3.0
govee-local-api==2.4.0
# homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2
@@ -1137,7 +1137,7 @@ greeclimate==2.1.1
greeneye_monitor==3.0.3
# homeassistant.components.green_planet_energy
greenplanet-energy-api==0.1.4
greenplanet-energy-api==0.1.10
# homeassistant.components.greenwave
greenwavereality==0.5.1
@@ -1226,7 +1226,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.1
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1256,7 +1256,7 @@ huum==0.8.1
hyperion-py==0.7.6
# homeassistant.components.hypontech
hyponcloud==0.3.0
hyponcloud==0.9.0
# homeassistant.components.iammeter
iammeter==0.2.1
@@ -1271,7 +1271,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.0
ical==13.2.2
# homeassistant.components.caldav
icalendar==6.3.1
@@ -1663,7 +1663,7 @@ odp-amsterdam==6.1.2
oemthermostat==1.1.1
# homeassistant.components.ohme
ohme==1.6.0
ohme==1.7.0
# homeassistant.components.ollama
ollama==0.5.1
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.5
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1714,10 +1714,10 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
opower==0.17.0
opower==0.17.1
# homeassistant.components.oralb
oralb-ble==1.0.2
oralb-ble==1.1.0
# homeassistant.components.oru
oru==0.1.11
@@ -1938,7 +1938,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.0
pyanglianwater==3.1.1
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -2179,7 +2179,7 @@ pyitachip2ir==0.0.7
pyituran==0.1.5
# homeassistant.components.jvc_projector
pyjvcprojector==2.0.1
pyjvcprojector==2.0.3
# homeassistant.components.kaleidescape
pykaleidescape==1.1.3
@@ -2370,7 +2370,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.31
pyportainer==1.0.33
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2473,7 +2473,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1
# homeassistant.components.smartthings
pysmartthings==3.6.0
pysmartthings==3.7.2
# homeassistant.components.smarty
pysmarty2==0.10.3
@@ -2485,7 +2485,7 @@ pysmhi==1.1.0
pysml==0.1.5
# homeassistant.components.smlight
pysmlight==0.2.16
pysmlight==0.3.1
# homeassistant.components.snmp
pysnmp==7.1.22
@@ -2563,7 +2563,7 @@ python-gitlab==1.6.0
python-google-drive-api==0.1.0
# homeassistant.components.google_weather
python-google-weather-api==0.0.4
python-google-weather-api==0.0.6
# homeassistant.components.analytics_insights
python-homeassistant-analytics==0.9.0
@@ -2612,7 +2612,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.8.0
python-otbr-api==2.9.0
# homeassistant.components.overseerr
python-overseerr==0.9.0
@@ -2984,7 +2984,7 @@ starline==0.1.5
starlingbank==3.2
# homeassistant.components.starlink
starlink-grpc-core==1.2.3
starlink-grpc-core==1.2.4
# homeassistant.components.statsd
statsd==3.2.1
@@ -3106,7 +3106,7 @@ total-connect-client==2025.12.2
tp-connected==0.0.4
# homeassistant.components.tplink_omada
tplink-omada-client==1.5.3
tplink-omada-client==1.5.6
# homeassistant.components.transmission
transmission-rpc==7.0.3
@@ -3274,7 +3274,7 @@ wirelesstagpy==0.8.1
wled==0.21.0
# homeassistant.components.wolflink
wolf-comm==0.0.23
wolf-comm==0.0.48
# homeassistant.components.wsdot
wsdot==0.0.1
@@ -3283,7 +3283,7 @@ wsdot==0.0.1
wyoming==1.7.2
# homeassistant.components.xiaomi_ble
xiaomi-ble==1.6.0
xiaomi-ble==1.10.0
# homeassistant.components.knx
xknx==3.15.0
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.2.7
yalexs-ble==3.2.8
# homeassistant.components.august
# homeassistant.components.yale
@@ -3320,7 +3320,7 @@ yeelight==0.7.16
yeelightsunflower==0.0.10
# homeassistant.components.yolink
yolink-api==0.6.1
yolink-api==0.6.3
# homeassistant.components.youless
youless-api==2.2.0
@@ -3347,7 +3347,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.0.1
zha==1.0.2
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.9
PyChromecast==14.0.10
# homeassistant.components.flume
PyFlume==0.6.5
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.0.0
aioamazondevices==13.0.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==2.0.0
aiocomelit==2.0.1
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
@@ -407,7 +407,7 @@ aiosyncthing==0.7.1
aiotankerkoenig==0.5.1
# homeassistant.components.tedee
aiotedee==0.2.25
aiotedee==0.2.27
# homeassistant.components.tractive
aiotractive==1.0.0
@@ -545,7 +545,7 @@ automower-ble==0.2.8
av==16.0.1
# homeassistant.components.axis
axis==66
axis==67
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.7
@@ -998,7 +998,7 @@ gotailwind==0.3.0
govee-ble==0.44.0
# homeassistant.components.govee_light_local
govee-local-api==2.3.0
govee-local-api==2.4.0
# homeassistant.components.gpsd
gps3==0.33.3
@@ -1010,7 +1010,7 @@ greeclimate==2.1.1
greeneye_monitor==3.0.3
# homeassistant.components.green_planet_energy
greenplanet-energy-api==0.1.4
greenplanet-energy-api==0.1.10
# homeassistant.components.pure_energie
gridnet==5.0.1
@@ -1087,7 +1087,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.1
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1114,7 +1114,7 @@ huum==0.8.1
hyperion-py==0.7.6
# homeassistant.components.hypontech
hyponcloud==0.3.0
hyponcloud==0.9.0
# homeassistant.components.iaqualink
iaqualink==0.6.0
@@ -1126,7 +1126,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.0
ical==13.2.2
# homeassistant.components.caldav
icalendar==6.3.1
@@ -1449,7 +1449,7 @@ objgraph==3.5.0
odp-amsterdam==6.1.2
# homeassistant.components.ohme
ohme==1.6.0
ohme==1.7.0
# homeassistant.components.ollama
ollama==0.5.1
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.5
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1491,10 +1491,10 @@ openrgb-python==0.3.6
openwebifpy==4.3.1
# homeassistant.components.opower
opower==0.17.0
opower==0.17.1
# homeassistant.components.oralb
oralb-ble==1.0.2
oralb-ble==1.1.0
# homeassistant.components.ourgroceries
ourgroceries==1.5.4
@@ -1669,7 +1669,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.0
pyanglianwater==3.1.1
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -1856,7 +1856,7 @@ pyisy==3.4.1
pyituran==0.1.5
# homeassistant.components.jvc_projector
pyjvcprojector==2.0.1
pyjvcprojector==2.0.3
# homeassistant.components.kaleidescape
pykaleidescape==1.1.3
@@ -2020,7 +2020,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.31
pyportainer==1.0.33
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2102,7 +2102,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1
# homeassistant.components.smartthings
pysmartthings==3.6.0
pysmartthings==3.7.2
# homeassistant.components.smarty
pysmarty2==0.10.3
@@ -2114,7 +2114,7 @@ pysmhi==1.1.0
pysml==0.1.5
# homeassistant.components.smlight
pysmlight==0.2.16
pysmlight==0.3.1
# homeassistant.components.snmp
pysnmp==7.1.22
@@ -2165,7 +2165,7 @@ python-fullykiosk==0.0.14
python-google-drive-api==0.1.0
# homeassistant.components.google_weather
python-google-weather-api==0.0.4
python-google-weather-api==0.0.6
# homeassistant.components.analytics_insights
python-homeassistant-analytics==0.9.0
@@ -2208,7 +2208,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.8.0
python-otbr-api==2.9.0
# homeassistant.components.overseerr
python-overseerr==0.9.0
@@ -2514,7 +2514,7 @@ srpenergy==1.3.6
starline==0.1.5
# homeassistant.components.starlink
starlink-grpc-core==1.2.3
starlink-grpc-core==1.2.4
# homeassistant.components.statsd
statsd==3.2.1
@@ -2606,7 +2606,7 @@ toonapi==0.3.0
total-connect-client==2025.12.2
# homeassistant.components.tplink_omada
tplink-omada-client==1.5.3
tplink-omada-client==1.5.6
# homeassistant.components.transmission
transmission-rpc==7.0.3
@@ -2753,7 +2753,7 @@ wiffi==1.1.2
wled==0.21.0
# homeassistant.components.wolflink
wolf-comm==0.0.23
wolf-comm==0.0.48
# homeassistant.components.wsdot
wsdot==0.0.1
@@ -2762,7 +2762,7 @@ wsdot==0.0.1
wyoming==1.7.2
# homeassistant.components.xiaomi_ble
xiaomi-ble==1.6.0
xiaomi-ble==1.10.0
# homeassistant.components.knx
xknx==3.15.0
@@ -2783,7 +2783,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.2.7
yalexs-ble==3.2.8
# homeassistant.components.august
# homeassistant.components.yale
@@ -2793,7 +2793,7 @@ yalexs==9.2.0
yeelight==0.7.16
# homeassistant.components.yolink
yolink-api==0.6.1
yolink-api==0.6.3
# homeassistant.components.youless
youless-api==2.2.0
@@ -2817,7 +2817,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.0.1
zha==1.0.2
# homeassistant.components.zinvolt
zinvolt==0.3.0

View File

@@ -2,10 +2,7 @@
from unittest.mock import AsyncMock
from aioamazondevices.const.devices import (
SPEAKER_GROUP_DEVICE_TYPE,
SPEAKER_GROUP_FAMILY,
)
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
import pytest
@@ -117,7 +114,7 @@ async def test_alexa_dnd_group_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model=SPEAKER_GROUP_DEVICE_TYPE,
model="Speaker Group",
entry_type=dr.DeviceEntryType.SERVICE,
)
@@ -156,7 +153,7 @@ async def test_alexa_unsupported_notification_sensor_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model=SPEAKER_GROUP_DEVICE_TYPE,
model="Speaker Group",
entry_type=dr.DeviceEntryType.SERVICE,
)

View File

@@ -2,7 +2,7 @@
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
from aiohttp import ClientError, ClientResponseError
import pytest
from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
@@ -18,7 +18,11 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
@@ -304,3 +308,59 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
"""Test OAuth token request reauth error starts a reauth flow."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestReauthError(
request_info=Mock(real_url="https://auth.august.com/access_token"),
status=401,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "pick_implementation"
assert flows[0]["context"]["source"] == "reauth"
async def test_oauth_token_request_transient_error_is_retryable(
hass: HomeAssistant,
) -> None:
"""Test OAuth token transient request error marks entry for setup retry."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestTransientError(
request_info=Mock(real_url="https://auth.august.com/access_token"),
status=500,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
"""Test OAuth transport client errors mark entry for setup retry."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=ClientError("connection error"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -45,6 +45,21 @@ async def test_celsius_fahrenheit(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_climate_entity_loads_without_static_values(
hass: HomeAssistant,
mock_bsblan: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the climate entity still loads when static values are unavailable."""
mock_bsblan.static_values.side_effect = BSBLANError("General error")
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.attributes["current_temperature"] is not None
async def test_climate_entity_properties(
hass: HomeAssistant,
mock_bsblan: AsyncMock,

View File

@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock
from bsblan import BSBLANError
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
@@ -27,3 +28,26 @@ async def test_diagnostics(
hass, hass_client, mock_config_entry
)
assert diagnostics_data == snapshot
async def test_diagnostics_without_static_values(
hass: HomeAssistant,
mock_bsblan: AsyncMock,
hass_client: ClientSessionGenerator,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test diagnostics when static values are not available."""
mock_bsblan.static_values.side_effect = BSBLANError("General error")
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
diagnostics_data = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
assert "info" in diagnostics_data
assert "device" in diagnostics_data
assert "fast_coordinator_data" in diagnostics_data
assert diagnostics_data["static"] is None

View File

@@ -78,30 +78,50 @@ async def test_config_entry_auth_failed_triggers_reauth(
@pytest.mark.parametrize(
("method", "exception", "expected_state"),
("method", "exception", "expected_state", "assert_static_fallback"),
[
(
"initialize",
BSBLANError("General error"),
ConfigEntryState.SETUP_ERROR,
False,
),
(
"device",
BSBLANConnectionError("Connection failed"),
ConfigEntryState.SETUP_RETRY,
False,
),
(
"info",
BSBLANAuthError("Authentication failed"),
ConfigEntryState.SETUP_ERROR,
False,
),
(
"static_values",
BSBLANError("General error"),
ConfigEntryState.LOADED,
True,
),
(
"static_values",
TimeoutError("Connection timeout"),
ConfigEntryState.LOADED,
True,
),
("static_values", BSBLANError("General error"), ConfigEntryState.SETUP_ERROR),
],
)
async def test_config_entry_static_data_errors(
async def test_config_entry_setup_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_bsblan: MagicMock,
method: str,
exception: Exception,
expected_state: ConfigEntryState,
assert_static_fallback: bool,
) -> None:
"""Test various errors during static data fetching trigger appropriate config entry states."""
"""Test setup errors trigger appropriate config entry states."""
# Mock the specified method to raise the exception
getattr(mock_bsblan, method).side_effect = exception
@@ -110,6 +130,8 @@ async def test_config_entry_static_data_errors(
await hass.async_block_till_done()
assert mock_config_entry.state is expected_state
if assert_static_fallback:
assert mock_config_entry.runtime_data.static is None
async def test_coordinator_dhw_config_update_error(

View File

@@ -57,7 +57,7 @@ from tests.common import MockConfigEntry
@pytest.mark.parametrize(
("show_advanced_options", "user_input", "expected_config"),
("show_advanced_options", "user_input", "expected_config", "expected_options"),
[
(
True,
@@ -69,6 +69,11 @@ from tests.common import MockConfigEntry
CONF_PORT: 1234,
CONF_SSL: False,
},
{
CONF_OLD_DISCOVERY: False,
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
CONF_FEATURE_DEVICE_TRACKING: True,
},
),
(
False,
@@ -80,10 +85,19 @@ from tests.common import MockConfigEntry
CONF_PORT: 49000,
CONF_SSL: False,
},
{
CONF_OLD_DISCOVERY: False,
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
CONF_FEATURE_DEVICE_TRACKING: True,
},
),
(
False,
{**MOCK_USER_INPUT_SIMPLE, CONF_SSL: True},
{
**MOCK_USER_INPUT_SIMPLE,
CONF_SSL: True,
CONF_FEATURE_DEVICE_TRACKING: False,
},
{
CONF_HOST: "fake_host",
CONF_PASSWORD: "fake_pass",
@@ -91,6 +105,11 @@ from tests.common import MockConfigEntry
CONF_PORT: 49443,
CONF_SSL: True,
},
{
CONF_OLD_DISCOVERY: False,
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
CONF_FEATURE_DEVICE_TRACKING: False,
},
),
],
)
@@ -100,6 +119,7 @@ async def test_user(
show_advanced_options: bool,
user_input: dict,
expected_config: dict,
expected_options: dict,
) -> None:
"""Test starting a flow by user."""
with (
@@ -143,10 +163,7 @@ async def test_user(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == expected_config
assert (
result["options"][CONF_CONSIDER_HOME]
== DEFAULT_CONSIDER_HOME.total_seconds()
)
assert result["options"] == expected_options
assert not result["result"].unique_id
assert mock_setup_entry.called

View File

@@ -396,6 +396,11 @@ async def test_switch_device_no_ip_address(
"async_set_deflection_enable",
STATE_ON,
),
(
"switch.mock_title_wi_fi_mywifi",
"async_set_wlan_configuration",
STATE_ON,
),
],
)
async def test_switch_turn_on_off(

View File

@@ -141,5 +141,5 @@ def mock_hik_nvr(mock_hikcamera: MagicMock) -> MagicMock:
camera = mock_hikcamera.return_value
camera.get_type = "NVR"
camera.current_event_states = {}
camera.get_event_triggers.return_value = {"Motion": [1, 2]}
camera.get_event_triggers.return_value = {"VMD": [1, 2]}
return mock_hikcamera

Some files were not shown because too many files have changed in this diff Show More