Compare commits

...

100 Commits

Author SHA1 Message Date
Franck Nijhof
40d7f2a89e 2025.10.2 (#154181) 2025-10-10 23:19:19 +02:00
Shay Levy
13b717e2da Fix shelly remove orphaned entities (#154182)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-10 22:46:30 +02:00
Franck Nijhof
5fcfd3ad84 Bump version to 2025.10.2 2025-10-10 20:29:17 +00:00
Shay Levy
324a7b5443 Fix Shelly RPC cover update when the device is not initialized (#154159) 2025-10-10 20:27:30 +00:00
Robert Resch
491ae8f72c Bump deebot-client to 15.1.0 (#154154) 2025-10-10 20:23:10 +00:00
Justus
259247892f IOmeter bump version v0.2.0 (#154150) 2025-10-10 20:23:09 +00:00
Bram Kragten
caeda0ef64 Update frontend to 20251001.2 (#154143) 2025-10-10 20:23:08 +00:00
Paul Bottein
df35c535e4 Add missing entity category and icons for smlight integration (#154131) 2025-10-10 20:23:07 +00:00
Paulus Schoutsen
f93b9e0ed0 Z-Wave: ESPHome discovery to update all options (#154113) 2025-10-10 20:23:05 +00:00
peteS-UK
48a3372cf2 Fix for multiple Lyrion Music Server on a single Home Assistant server for Squeezebox (#154081) 2025-10-10 20:23:04 +00:00
Maciej Bieniek
d84fd72428 Bump brother to version 5.1.1 (#154080) 2025-10-10 20:23:03 +00:00
Simone Chemelli
e8cb386962 Bump aioamazondevices to 6.4.0 (#154071) 2025-10-10 20:23:02 +00:00
epenet
5ac726703c Filter out invalid Renault vehicles (#154070)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-10 20:23:00 +00:00
Joost Lekkerkerker
688649a799 Don't mark ZHA coordinator as via_device with itself (#154004) 2025-10-10 20:17:07 +00:00
Artur Pragacz
c5359ade3e Fix empty llm api list in chat log (#153996) 2025-10-10 20:17:05 +00:00
Michael Davie
4e60dedc1b Bump env-canada to 0.11.3 (#153967) 2025-10-10 20:17:04 +00:00
Maciej Bieniek
221d74f83a Fix update interval for AccuWeather hourly forecast (#153957) 2025-10-10 20:17:02 +00:00
G Johansson
fbbb3d6415 Bump holidays to 0.82 (#153952) 2025-10-10 20:17:01 +00:00
Josef Zweck
8297019011 Bump pylamarzocco to 2.1.2 (#153950) 2025-10-10 20:16:59 +00:00
TheJulianJES
61715dcff3 Adjust OTBR config entry name for ZBT-2 (#153940) 2025-10-10 20:16:58 +00:00
TheJulianJES
32b822ee99 Fix HA hardware configuration message for Thread without HAOS (#153933) 2025-10-10 20:16:56 +00:00
Fabien Kleinbourg
e6c2e0ad80 sharkiq dependency bump to 1.4.2 (#153931) 2025-10-10 20:16:55 +00:00
TheJulianJES
1314427dc5 Do not auto-set up ZHA zeroconf discoveries during onboarding (#153914) 2025-10-10 20:16:53 +00:00
Tom Matheussen
bf499a45f7 Add missing translation string for Satel Integra subentry type (#153905) 2025-10-10 20:16:52 +00:00
Christopher Fenner
b955e22628 fix typo in icon assignment of AccuWeather integration (#153890) 2025-10-10 20:16:50 +00:00
Simone Chemelli
1b222ff5fd Fix restore cover state for Comelit SimpleHome (#153887) 2025-10-10 20:16:49 +00:00
derytive
f0510e703f Add plate_count for Miele KM7575 (#153868) 2025-10-10 19:36:03 +00:00
G Johansson
cbe3956e15 Handle timeout errors gracefully in Nord Pool services (#153856) 2025-10-10 19:36:01 +00:00
Aaron Bach
4588e9da8d Limit SimpliSafe websocket connection attempts during startup (#153853)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-10 19:36:00 +00:00
Simone Chemelli
5445890fdf Bump aiocomelit to 1.1.1 (#153843) 2025-10-10 19:35:59 +00:00
Simone Chemelli
9b49f77f86 Fix PIN validation for Comelit SimpleHome (#153840) 2025-10-10 19:35:57 +00:00
Petro31
566c8fb786 Fix delay_on and auto_off with multiple triggers (#153839) 2025-10-10 19:35:56 +00:00
Joost Lekkerkerker
b36150c213 Add motion presets to SmartThings AC (#153830) 2025-10-10 19:35:54 +00:00
Joost Lekkerkerker
809070d2ad Catch update exception in AirGradient (#153828) 2025-10-10 19:35:53 +00:00
Joost Lekkerkerker
f4339dc031 Bump pySmartThings to 3.3.1 (#153826) 2025-10-10 19:35:51 +00:00
epenet
f3b37d24b0 Fix Tuya cover position when only control is available (#153803) 2025-10-10 19:35:50 +00:00
Paulus Schoutsen
4c8348caa7 Handle ESPHome discoveries with uninitialized Z-Wave antennas (#153790) 2025-10-10 19:35:49 +00:00
cdnninja
b9e7c102ea vesync correct fan set modes (#153761) 2025-10-10 19:35:47 +00:00
Simone Chemelli
69d9fa89b7 Remove stale entities from Alexa Devices (#153759) 2025-10-10 19:35:46 +00:00
Simone Chemelli
6f3f5a5ec1 Bump aioamazondevices to 6.2.9 (#153756) 2025-10-10 19:35:44 +00:00
Simone Chemelli
5ecfeca90a Fix sensors availability check for Alexa Devices (#153743) 2025-10-10 19:35:43 +00:00
Sander Jochems
00e0570fd4 Upgrade python-melcloud to 0.1.2 (#153742) 2025-10-10 19:35:41 +00:00
Øyvind Matheson Wergeland
5a5b94f3af Synology DSM: Don't reinitialize API during configuration (#153739) 2025-10-10 19:35:40 +00:00
Maciej Bieniek
34f00d9b33 Align Shelly presencezone entity to the new API/firmware (#153737) 2025-10-10 19:35:39 +00:00
Tom
4cabc5b368 Bump airOS to 0.5.5 using formdata for v6 firmware (#153736) 2025-10-10 19:35:37 +00:00
tronikos
4045125422 Fix missing google_assistant_sdk.send_text_command (#153735) 2025-10-10 19:35:36 +00:00
Fredrik Erlandsson
d7393af76f Version bump pydaikin to 2.17.1 (#153726) 2025-10-10 19:35:34 +00:00
Fredrik Erlandsson
ad41386b27 Version bump pydaikin to 2.17.0 (#153718) 2025-10-10 19:35:33 +00:00
tronikos
62d17ea20c Bump opower to 0.15.6 (#153714) 2025-10-10 19:35:31 +00:00
peetersch
c4954731d0 Modbus Fix message_wait_milliseconds is no longer applied (#153709) 2025-10-10 19:35:30 +00:00
cdnninja
647723d3f0 Bump pyvesync to 3.1.0 (#153693) 2025-10-10 19:35:28 +00:00
Christopher Fenner
51c500e22c Fix ViCare pressure sensors missing unit of measurement (#153691) 2025-10-10 19:35:26 +00:00
Denis Shulyaka
f6fc13c1f2 Gemini: Use default model instead of recommended where applicable (#153676) 2025-10-10 19:35:25 +00:00
Jan Bouwhuis
0009a7a042 Fix MQTT Lock state reset to unknown when a reset payload is received (#153647) 2025-10-10 19:35:24 +00:00
Luke Lashley
a3d1aa28e7 Switch Roborock to v4 of the code login api (#153593) 2025-10-10 19:35:22 +00:00
Simone Chemelli
9f53eb9b76 Bump aioamazondevices to 6.2.8 (#153592) 2025-10-10 19:35:21 +00:00
Luke Lashley
f53a205ff3 Bump python-roborock to 2.50.2 (#153561) 2025-10-10 19:35:19 +00:00
NANI
d08517c3df Updated VRM client and accounted for missing forecasts (#153464) 2025-10-10 19:35:18 +00:00
Kinachi249
d7398a44a1 Bump PyCync to 0.4.1 (#153401) 2025-10-10 19:35:17 +00:00
Aidan Timson
9acfc0cb88 Fix power device classes for system bridge (#153201) 2025-10-10 19:35:15 +00:00
Hessel
1b3d21523a Wallbox fix Rate Limit issue for multiple chargers (#153074) 2025-10-10 19:35:14 +00:00
puddly
1d407d1326 Prevent reloading the ZHA integration while adapter firmware is being updated (#152626) 2025-10-10 19:35:12 +00:00
Franck Nijhof
013346cead 2025.10.1 (#153582) 2025-10-03 20:08:44 +02:00
Franck Nijhof
5abaabc9da Bump version to 2025.10.1 2025-10-03 17:26:37 +00:00
Paulus Schoutsen
32481312c3 When discovering a Z-Wave adapter, always configure add-on in config flow (#153575) 2025-10-03 17:26:16 +00:00
Paulus Schoutsen
bdc9eb37d3 Z-Wave to support migrating from USB to socket with same home ID (#153522) 2025-10-03 17:26:15 +00:00
Abílio Costa
e0afcbc02b Debounce updates in Idasen Desk (#153503) 2025-10-03 17:26:13 +00:00
puddly
cd56a6a98d Bump universal-silabs-flasher to 0.0.35 (#153500) 2025-10-03 17:26:11 +00:00
cdnninja
9d85893bbb Fix VeSync zero fan speed handling (#153493)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-03 17:26:10 +00:00
starkillerOG
9e8a70225f Bump reolink-aio to 0.16.1 (#153489) 2025-10-03 17:26:08 +00:00
Daniel Hjelseth Høyer
96ec795d5e Bump pyTibber to 0.32.2 (#153484) 2025-10-03 17:26:07 +00:00
Josef Zweck
65b796070d Fix missing parameter pass in onedrive (#153478) 2025-10-03 17:26:05 +00:00
Aidan Timson
32994812e5 Update OVOEnergy to 3.0.1 (#153476) 2025-10-03 17:26:04 +00:00
G Johansson
66ff9d63a3 Fix next event in workday calendar (#153465) 2025-10-03 17:26:02 +00:00
Joost Lekkerkerker
b2a63d4996 Add translation for turbo fan mode in SmartThings (#153445)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 17:26:00 +00:00
puddly
f9f37b7f2a Disable baudrate bootloader reset for ZBT-2 (#153443) 2025-10-03 17:25:59 +00:00
Stefan Agner
7bdd9dd38a Update Home Assistant base image to 2025.10.0 (#153441) 2025-10-03 17:25:58 +00:00
Joost Lekkerkerker
1e8aae0a89 Fix missing powerconsumptionreport in Smartthings (#153438) 2025-10-03 17:25:56 +00:00
Aidan Timson
cf668e9dc2 Add missing translation for media browser default title (#153430)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-10-03 17:25:55 +00:00
Norbert Rittel
2e91c8700f Fix sentence-casing in user-facing strings of slack (#153427) 2025-10-03 17:25:53 +00:00
J. Nick Koston
9d14627daa Bump aiohomekit to 3.2.19 (#153423) 2025-10-03 17:25:52 +00:00
TheJulianJES
73b8283748 Fix Z-Wave RGB light turn on causing rare ZeroDivisionError (#153422) 2025-10-03 17:25:50 +00:00
Manu
edeaaa2e63 Update markdown field description in ntfy integration (#153421) 2025-10-03 17:25:49 +00:00
Tom Matheussen
d26dd8fc39 Fix Satel Integra creating new binary sensors on YAML import (#153419) 2025-10-03 17:25:47 +00:00
Denis Shulyaka
34640ea735 Disable thinking for unsupported gemini models (#153415) 2025-10-03 17:25:46 +00:00
Erwin Douna
46a2e21ef0 Bump pyportainer 1.0.3 (#153413) 2025-10-03 17:25:45 +00:00
Erwin Douna
508af53e72 Bump pyportainer 1.0.2 (#153326) 2025-10-03 17:25:43 +00:00
Josef Zweck
5f7440608c Increase onedrive upload chunk size (#153406) 2025-10-03 17:22:10 +00:00
Michael J. Kidd
0d1aa38a26 Pushover: Handle empty data section properly (#153397) 2025-10-03 17:22:08 +00:00
Luke Lashley
929f8c148a Bump python-roborock to 2.49.1 (#153396) 2025-10-03 17:22:07 +00:00
Joakim Plate
92db1f5a04 Correct blocking update in ToGrill with lack of notifications (#153387) 2025-10-03 17:22:05 +00:00
starkillerOG
e66b5ce0bf Add Roborock mop intensity translations (#153380) 2025-10-03 17:22:03 +00:00
Michael
1e17150e9f Explicit pass in the config entry to coordinator in airtouch4 (#153361)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-03 17:22:02 +00:00
Michael
792902de3d Set config entry to None in ProxmoxVE (#153357) 2025-10-03 17:22:00 +00:00
Andre Lengwenus
04d78c3dd5 Explicitly check for None in raw value processing of modbus (#153352) 2025-10-03 17:21:59 +00:00
G Johansson
5c8d5bfb84 Fix Nord Pool 15 minute interval (#153350) 2025-10-03 17:21:57 +00:00
puddly
99bff31869 Do not reset the adapter twice during ZHA options flow migration (#153345) 2025-10-03 17:21:56 +00:00
Stefan Agner
d949119fb0 Bump aiohasupervisor to 0.3.3 (#153344) 2025-10-03 17:21:54 +00:00
Tom
e7b737ece5 Bump airOS module for alternative login url (#153317) 2025-10-03 17:21:52 +00:00
Tom
fb8ddac2e8 Bump airOS dependency (#153065) 2025-10-03 17:21:51 +00:00
188 changed files with 7765 additions and 2638 deletions

4
CODEOWNERS generated
View File

@@ -760,8 +760,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz /homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz /tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/homeassistant/components/intesishome/ @jnimmo /homeassistant/components/intesishome/ @jnimmo
/homeassistant/components/iometer/ @MaestroOnICe /homeassistant/components/iometer/ @jukrebs
/tests/components/iometer/ @MaestroOnICe /tests/components/iometer/ @jukrebs
/homeassistant/components/ios/ @robbiet480 /homeassistant/components/ios/ @robbiet480
/tests/components/ios/ @robbiet480 /tests/components/ios/ @robbiet480
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard /homeassistant/components/iotawatt/ @gtdiehl @jyavenard

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant image: ghcr.io/home-assistant/{arch}-homeassistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.3 aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.3 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.3 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.3 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.3 i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io

View File

@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
} }
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10) UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30) UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)

View File

@@ -1,6 +1,9 @@
{ {
"entity": { "entity": {
"sensor": { "sensor": {
"air_quality": {
"default": "mdi:air-filter"
},
"cloud_ceiling": { "cloud_ceiling": {
"default": "mdi:weather-fog" "default": "mdi:weather-fog"
}, },
@@ -34,9 +37,6 @@
"thunderstorm_probability_night": { "thunderstorm_probability_night": {
"default": "mdi:weather-lightning" "default": "mdi:weather-lightning"
}, },
"translation_key": {
"default": "mdi:air-filter"
},
"tree_pollen": { "tree_pollen": {
"default": "mdi:tree-outline" "default": "mdi:tree-outline"
}, },

View File

@@ -1,7 +1,9 @@
"""Airgradient Update platform.""" """Airgradient Update platform."""
from datetime import timedelta from datetime import timedelta
import logging
from airgradient import AirGradientConnectionError
from propcache.api import cached_property from propcache.api import cached_property
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
SCAN_INTERVAL = timedelta(hours=1) SCAN_INTERVAL = timedelta(hours=1)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
@@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update.""" """Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE _attr_device_class = UpdateDeviceClass.FIRMWARE
_server_unreachable_logged = False
def __init__(self, coordinator: AirGradientCoordinator) -> None: def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize the entity.""" """Initialize the entity."""
@@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Return the installed version of the entity.""" """Return the installed version of the entity."""
return self.coordinator.data.measures.firmware_version return self.coordinator.data.measures.firmware_version
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._attr_available
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the entity.""" """Update the entity."""
self._attr_latest_version = ( try:
await self.coordinator.client.get_latest_firmware_version( self._attr_latest_version = (
self.coordinator.serial_number await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
)
) )
) except AirGradientConnectionError:
self._attr_latest_version = None
self._attr_available = False
if not self._server_unreachable_logged:
_LOGGER.error(
"Unable to connect to AirGradient server to check for updates"
)
self._server_unreachable_logged = True
else:
self._server_unreachable_logged = False
self._attr_available = True

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos", "documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["airos==0.5.1"] "requirements": ["airos==0.5.5"]
} }

View File

@@ -2,17 +2,14 @@
from airtouch4pyapi import AirTouch from airtouch4pyapi import AirTouch
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import AirtouchDataUpdateCoordinator from .coordinator import AirTouch4ConfigEntry, AirtouchDataUpdateCoordinator
PLATFORMS = [Platform.CLIMATE] PLATFORMS = [Platform.CLIMATE]
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
"""Set up AirTouch4 from a config entry.""" """Set up AirTouch4 from a config entry."""
@@ -22,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) ->
info = airtouch.GetAcs() info = airtouch.GetAcs()
if not info: if not info:
raise ConfigEntryNotReady raise ConfigEntryNotReady
coordinator = AirtouchDataUpdateCoordinator(hass, airtouch) coordinator = AirtouchDataUpdateCoordinator(hass, entry, airtouch)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator entry.runtime_data = coordinator

View File

@@ -2,26 +2,34 @@
import logging import logging
from airtouch4pyapi import AirTouch
from airtouch4pyapi.airtouch import AirTouchStatus from airtouch4pyapi.airtouch import AirTouchStatus
from homeassistant.components.climate import SCAN_INTERVAL from homeassistant.components.climate import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator): class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Airtouch data.""" """Class to manage fetching Airtouch data."""
def __init__(self, hass, airtouch): def __init__(
self, hass: HomeAssistant, entry: AirTouch4ConfigEntry, airtouch: AirTouch
) -> None:
"""Initialize global Airtouch data updater.""" """Initialize global Airtouch data updater."""
self.airtouch = airtouch self.airtouch = airtouch
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
config_entry=entry,
name=DOMAIN, name=DOMAIN,
update_interval=SCAN_INTERVAL, update_interval=SCAN_INTERVAL,
) )

View File

@@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.helpers.entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonConfigEntry from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity from .entity import AmazonEntity
from .utils import async_update_unique_id from .utils import async_update_unique_id
@@ -51,11 +53,47 @@ BINARY_SENSORS: Final = (
), ),
is_supported=lambda device, key: device.sensors.get(key) is not None, is_supported=lambda device, key: device.sensors.get(key) is not None,
is_available_fn=lambda device, key: ( is_available_fn=lambda device, key: (
device.online and device.sensors[key].error is False device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
), ),
), ),
) )
DEPRECATED_BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="babyCryDetectionState",
translation_key="baby_cry_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="beepingApplianceDetectionState",
translation_key="beeping_appliance_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="coughDetectionState",
translation_key="cough_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="dogBarkDetectionState",
translation_key="dog_bark_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="waterSoundsDetectionState",
translation_key="water_sounds_detection",
is_on_fn=lambda device, key: False,
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -66,6 +104,8 @@ async def async_setup_entry(
coordinator = entry.runtime_data coordinator = entry.runtime_data
entity_registry = er.async_get(hass)
# Replace unique id for "detectionState" binary sensor # Replace unique id for "detectionState" binary sensor
await async_update_unique_id( await async_update_unique_id(
hass, hass,
@@ -75,6 +115,16 @@ async def async_setup_entry(
"detectionState", "detectionState",
) )
# Clean up deprecated sensors
for sensor_desc in DEPRECATED_BINARY_SENSORS:
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{sensor_desc.key}"
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
):
_LOGGER.debug("Removing deprecated entity %s", entity_id)
entity_registry.async_remove(entity_id)
known_devices: set[str] = set() known_devices: set[str] = set()
def _check_device() -> None: def _check_device() -> None:

View File

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

View File

@@ -32,7 +32,9 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online and device.sensors[key].error is False device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
) )
@@ -40,9 +42,9 @@ SENSORS: Final = (
AmazonSensorEntityDescription( AmazonSensorEntityDescription(
key="temperature", key="temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device, _key: ( native_unit_of_measurement_fn=lambda device, key: (
UnitOfTemperature.CELSIUS UnitOfTemperature.CELSIUS
if device.sensors[_key].scale == "CELSIUS" if key in device.sensors and device.sensors[key].scale == "CELSIUS"
else UnitOfTemperature.FAHRENHEIT else UnitOfTemperature.FAHRENHEIT
), ),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,

View File

@@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity from .entity import AmazonEntity
from .utils import alexa_api_call, async_update_unique_id from .utils import (
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@@ -29,7 +33,9 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
is_on_fn: Callable[[AmazonDevice], bool] is_on_fn: Callable[[AmazonDevice], bool]
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online and device.sensors[key].error is False device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
) )
method: str method: str
@@ -58,6 +64,9 @@ async def async_setup_entry(
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd" hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
) )
# Remove DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator)
known_devices: set[str] = set() known_devices: set[str] = set()
def _check_device() -> None: def _check_device() -> None:

View File

@@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps from functools import wraps
from typing import Any, Concatenate from typing import Any, Concatenate
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
@@ -61,3 +63,21 @@ async def async_update_unique_id(
# Update the registry with the new unique_id # Update the registry with the new unique_id
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_remove_dnd_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
) -> None:
"""Remove entity DND from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-do_not_disturb"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)

View File

@@ -8,7 +8,7 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"requirements": ["brother==5.1.0"], "requirements": ["brother==5.1.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_printer._tcp.local.", "type": "_printer._tcp.local.",

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from asyncio.exceptions import TimeoutError from asyncio.exceptions import TimeoutError
from collections.abc import Mapping from collections.abc import Mapping
import re
from typing import Any from typing import Any
from aiocomelit import ( from aiocomelit import (
@@ -27,25 +28,20 @@ from .utils import async_client_session
DEFAULT_HOST = "192.168.1.252" DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = "111111" DEFAULT_PIN = "111111"
pin_regex = r"^[0-9]{4,10}$"
USER_SCHEMA = vol.Schema( USER_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex), vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST), vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
} }
) )
STEP_REAUTH_DATA_SCHEMA = vol.Schema( STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
)
STEP_RECONFIGURE = vol.Schema( STEP_RECONFIGURE = vol.Schema(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port, vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex), vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
} }
) )
@@ -55,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
api: ComelitCommonApi api: ComelitCommonApi
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
raise InvalidPin
session = await async_client_session(hass) session = await async_client_session(hass)
if data.get(CONF_TYPE, BRIDGE) == BRIDGE: if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
api = ComeliteSerialBridgeApi( api = ComeliteSerialBridgeApi(
@@ -105,6 +104,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -146,6 +147,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -189,6 +192,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@@ -210,3 +215,7 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError): class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth.""" """Error to indicate there is invalid auth."""
class InvalidPin(HomeAssistantError):
"""Error to indicate an invalid pin."""

View File

@@ -161,7 +161,7 @@ class ComelitSerialBridge(
entry: ComelitConfigEntry, entry: ComelitConfigEntry,
host: str, host: str,
port: int, port: int,
pin: int, pin: str,
session: ClientSession, session: ClientSession,
) -> None: ) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
entry: ComelitConfigEntry, entry: ComelitConfigEntry,
host: str, host: str,
port: int, port: int,
pin: int, pin: str,
session: ClientSession, session: ClientSession,
) -> None: ) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""

View File

@@ -7,7 +7,14 @@ from typing import Any, cast
from aiocomelit import ComelitSerialBridgeObject from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.components.cover import (
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@@ -62,7 +69,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
super().__init__(coordinator, device, config_entry_entry_id) super().__init__(coordinator, device, config_entry_entry_id)
# Device doesn't provide a status so we assume UNKNOWN at first startup # Device doesn't provide a status so we assume UNKNOWN at first startup
self._last_action: int | None = None self._last_action: int | None = None
self._last_state: str | None = None
def _current_action(self, action: str) -> bool: def _current_action(self, action: str) -> bool:
"""Return the current cover action.""" """Return the current cover action."""
@@ -98,7 +104,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@bridge_api_call @bridge_api_call
async def _cover_set_state(self, action: int, state: int) -> None: async def _cover_set_state(self, action: int, state: int) -> None:
"""Set desired cover state.""" """Set desired cover state."""
self._last_state = self.state
await self.coordinator.api.set_device_status(COVER, self._device.index, action) await self.coordinator.api.set_device_status(COVER, self._device.index, action)
self.coordinator.data[COVER][self._device.index].status = state self.coordinator.data[COVER][self._device.index].status = state
self.async_write_ha_state() self.async_write_ha_state()
@@ -124,5 +129,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
await super().async_added_to_hass() await super().async_added_to_hass()
if last_state := await self.async_get_last_state(): if (state := await self.async_get_last_state()) is not None:
self._last_state = last_state.state if state.state == STATE_CLOSED:
self._last_action = STATE_COVER.index(STATE_CLOSING)
if state.state == STATE_OPEN:
self._last_action = STATE_COVER.index(STATE_OPENING)
self._attr_is_closed = state.state == STATE_CLOSED

View File

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

View File

@@ -43,11 +43,13 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
} }
}, },

View File

@@ -514,7 +514,7 @@ class ChatLog:
"""Set the LLM system prompt.""" """Set the LLM system prompt."""
llm_api: llm.APIInstance | None = None llm_api: llm.APIInstance | None = None
if user_llm_hass_api is None: if not user_llm_hass_api:
pass pass
elif isinstance(user_llm_hass_api, llm.API): elif isinstance(user_llm_hass_api, llm.API):
llm_api = await user_llm_hass_api.async_get_api_instance(llm_context) llm_api = await user_llm_hass_api.async_get_api_instance(llm_context)

View File

@@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pycync==0.4.0"] "requirements": ["pycync==0.4.1"]
} }

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin", "documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pydaikin"], "loggers": ["pydaikin"],
"requirements": ["pydaikin==2.16.0"], "requirements": ["pydaikin==2.17.1"],
"zeroconf": ["_dkapi._tcp.local."] "zeroconf": ["_dkapi._tcp.local."]
} }

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["env_canada"], "loggers": ["env_canada"],
"requirements": ["env-canada==0.11.2"] "requirements": ["env-canada==0.11.3"]
} }

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251001.0"] "requirements": ["home-assistant-frontend==20251001.2"]
} }

View File

@@ -76,10 +76,6 @@ async def async_unload_entry(
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
) -> bool: ) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
for service_name in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service_name)
conversation.async_unset_agent(hass, entry) conversation.async_unset_agent(hass, entry)
return True return True

View File

@@ -26,7 +26,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
@@ -68,7 +68,13 @@ async def async_send_text_commands(
) -> list[CommandResponse]: ) -> list[CommandResponse]:
"""Send text commands to Google Assistant Service.""" """Send text commands to Google Assistant Service."""
# There can only be 1 entry (config_flow has single_instance_allowed) # There can only be 1 entry (config_flow has single_instance_allowed)
entry: GoogleAssistantSDKConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] entries = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
)
entry: GoogleAssistantSDKConfigEntry = entries[0]
session = entry.runtime_data.session session = entry.runtime_data.session
try: try:

View File

@@ -1,4 +1,4 @@
"""Support for Google Assistant SDK.""" """Services for the Google Assistant SDK integration."""
from __future__ import annotations from __future__ import annotations

View File

@@ -65,6 +65,9 @@
} }
}, },
"exceptions": { "exceptions": {
"entry_not_loaded": {
"message": "Entry not loaded"
},
"grpc_error": { "grpc_error": {
"message": "Failed to communicate with Google Assistant" "message": "Failed to communicate with Google Assistant"
} }

View File

@@ -456,6 +456,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
"""Initialize the agent.""" """Initialize the agent."""
self.entry = entry self.entry = entry
self.subentry = subentry self.subentry = subentry
self.default_model = default_model
self._attr_name = subentry.title self._attr_name = subentry.title
self._genai_client = entry.runtime_data self._genai_client = entry.runtime_data
self._attr_unique_id = subentry.subentry_id self._attr_unique_id = subentry.subentry_id
@@ -489,7 +490,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
tools = tools or [] tools = tools or []
tools.append(Tool(google_search=GoogleSearch())) tools.append(Tool(google_search=GoogleSearch()))
model_name = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL) model_name = options.get(CONF_CHAT_MODEL, self.default_model)
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model> # Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
supports_system_instruction = ( supports_system_instruction = (
"gemma" not in model_name "gemma" not in model_name
@@ -620,6 +621,13 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
def create_generate_content_config(self) -> GenerateContentConfig: def create_generate_content_config(self) -> GenerateContentConfig:
"""Create the GenerateContentConfig for the LLM.""" """Create the GenerateContentConfig for the LLM."""
options = self.subentry.data options = self.subentry.data
model = options.get(CONF_CHAT_MODEL, self.default_model)
thinking_config: ThinkingConfig | None = None
if model.startswith("models/gemini-2.5") and not model.endswith(
("tts", "image", "image-preview")
):
thinking_config = ThinkingConfig(include_thoughts=True)
return GenerateContentConfig( return GenerateContentConfig(
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE), temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
top_k=options.get(CONF_TOP_K, RECOMMENDED_TOP_K), top_k=options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
@@ -652,7 +660,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
), ),
), ),
], ],
thinking_config=ThinkingConfig(include_thoughts=True), thinking_config=thinking_config,
) )

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hassio", "documentation": "https://www.home-assistant.io/integrations/hassio",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["aiohasupervisor==0.3.3b0"], "requirements": ["aiohasupervisor==0.3.3"],
"single_config_entry": true "single_config_entry": true
} }

View File

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

View File

@@ -67,11 +67,7 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
"""Mixin for Home Assistant Connect ZBT-2 firmware methods.""" """Mixin for Home Assistant Connect ZBT-2 firmware methods."""
context: ConfigFlowContext context: ConfigFlowContext
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
# `rts_dtr` targets older adapters, `baudrate` works for newer ones. The reason we
# try them in this order is that on older adapters `baudrate` entered the ESP32-S3
# bootloader instead of the MG24 bootloader.
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
async def async_step_install_zigbee_firmware( async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None

View File

@@ -157,7 +157,7 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Connect ZBT-2 firmware update entity.""" """Connect ZBT-2 firmware update entity."""
bootloader_reset_methods = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE] bootloader_reset_methods = [ResetTarget.RTS_DTR]
def __init__( def __init__(
self, self,

View File

@@ -1,15 +1,20 @@
"""Home Assistant Hardware integration helpers.""" """Home Assistant Hardware integration helpers."""
from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from collections.abc import AsyncIterator, Awaitable, Callable from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager
import logging import logging
from typing import Protocol from typing import TYPE_CHECKING, Protocol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
from . import DATA_COMPONENT from . import DATA_COMPONENT
from .util import FirmwareInfo
if TYPE_CHECKING:
from .util import FirmwareInfo
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -51,6 +56,7 @@ class HardwareInfoDispatcher:
self._notification_callbacks: defaultdict[ self._notification_callbacks: defaultdict[
str, set[Callable[[FirmwareInfo], None]] str, set[Callable[[FirmwareInfo], None]]
] = defaultdict(set) ] = defaultdict(set)
self._active_firmware_updates: dict[str, str] = {}
def register_firmware_info_provider( def register_firmware_info_provider(
self, domain: str, platform: HardwareFirmwareInfoModule self, domain: str, platform: HardwareFirmwareInfoModule
@@ -118,6 +124,36 @@ class HardwareInfoDispatcher:
if fw_info is not None: if fw_info is not None:
yield fw_info yield fw_info
def register_firmware_update_in_progress(
self, device: str, source_domain: str
) -> None:
"""Register that a firmware update is in progress for a device."""
if device in self._active_firmware_updates:
current_domain = self._active_firmware_updates[device]
raise ValueError(
f"Firmware update already in progress for {device} by {current_domain}"
)
self._active_firmware_updates[device] = source_domain
def unregister_firmware_update_in_progress(
self, device: str, source_domain: str
) -> None:
"""Unregister a firmware update for a device."""
if device not in self._active_firmware_updates:
raise ValueError(f"No firmware update in progress for {device}")
if self._active_firmware_updates[device] != source_domain:
current_domain = self._active_firmware_updates[device]
raise ValueError(
f"Firmware update for {device} is owned by {current_domain}, not {source_domain}"
)
del self._active_firmware_updates[device]
def is_firmware_update_in_progress(self, device: str) -> bool:
"""Check if a firmware update is in progress for a device."""
return device in self._active_firmware_updates
@hass_callback @hass_callback
def async_register_firmware_info_provider( def async_register_firmware_info_provider(
@@ -141,3 +177,42 @@ def async_notify_firmware_info(
) -> Awaitable[None]: ) -> Awaitable[None]:
"""Notify the dispatcher of new firmware information.""" """Notify the dispatcher of new firmware information."""
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info) return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info)
@hass_callback
def async_register_firmware_update_in_progress(
hass: HomeAssistant, device: str, source_domain: str
) -> None:
"""Register that a firmware update is in progress for a device."""
return hass.data[DATA_COMPONENT].register_firmware_update_in_progress(
device, source_domain
)
@hass_callback
def async_unregister_firmware_update_in_progress(
hass: HomeAssistant, device: str, source_domain: str
) -> None:
"""Unregister a firmware update for a device."""
return hass.data[DATA_COMPONENT].unregister_firmware_update_in_progress(
device, source_domain
)
@hass_callback
def async_is_firmware_update_in_progress(hass: HomeAssistant, device: str) -> bool:
"""Check if a firmware update is in progress for a device."""
return hass.data[DATA_COMPONENT].is_firmware_update_in_progress(device)
@asynccontextmanager
async def async_firmware_update_context(
hass: HomeAssistant, device: str, source_domain: str
) -> AsyncIterator[None]:
"""Register a device as having its firmware being actively updated."""
async_register_firmware_update_in_progress(hass, device, source_domain)
try:
yield
finally:
async_unregister_firmware_update_in_progress(hass, device, source_domain)

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware", "documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system", "integration_type": "system",
"requirements": [ "requirements": [
"universal-silabs-flasher==0.0.34", "universal-silabs-flasher==0.0.35",
"ha-silabs-firmware-client==0.2.0" "ha-silabs-firmware-client==0.2.0"
] ]
} }

View File

@@ -67,7 +67,7 @@
} }
}, },
"abort": { "abort": {
"not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.", "not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please manually set up OpenThread Border Router to communicate with it.",
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.", "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.", "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",

View File

@@ -275,6 +275,7 @@ class BaseFirmwareUpdateEntity(
expected_installed_firmware_type=self.entity_description.expected_firmware_type, expected_installed_firmware_type=self.entity_description.expected_firmware_type,
bootloader_reset_methods=self.bootloader_reset_methods, bootloader_reset_methods=self.bootloader_reset_methods,
progress_callback=self._update_progress, progress_callback=self._update_progress,
domain=self._config_entry.domain,
) )
finally: finally:
self._attr_in_progress = False self._attr_in_progress = False

View File

@@ -26,6 +26,7 @@ from homeassistant.helpers.singleton import singleton
from . import DATA_COMPONENT from . import DATA_COMPONENT
from .const import ( from .const import (
DOMAIN,
OTBR_ADDON_MANAGER_DATA, OTBR_ADDON_MANAGER_DATA,
OTBR_ADDON_NAME, OTBR_ADDON_NAME,
OTBR_ADDON_SLUG, OTBR_ADDON_SLUG,
@@ -33,6 +34,7 @@ from .const import (
ZIGBEE_FLASHER_ADDON_NAME, ZIGBEE_FLASHER_ADDON_NAME,
ZIGBEE_FLASHER_ADDON_SLUG, ZIGBEE_FLASHER_ADDON_SLUG,
) )
from .helpers import async_firmware_update_context
from .silabs_multiprotocol_addon import ( from .silabs_multiprotocol_addon import (
WaitingAddonManager, WaitingAddonManager,
get_multiprotocol_addon_manager, get_multiprotocol_addon_manager,
@@ -359,45 +361,50 @@ async def async_flash_silabs_firmware(
expected_installed_firmware_type: ApplicationType, expected_installed_firmware_type: ApplicationType,
bootloader_reset_methods: Sequence[ResetTarget] = (), bootloader_reset_methods: Sequence[ResetTarget] = (),
progress_callback: Callable[[int, int], None] | None = None, progress_callback: Callable[[int, int], None] | None = None,
*,
domain: str = DOMAIN,
) -> FirmwareInfo: ) -> FirmwareInfo:
"""Flash firmware to the SiLabs device.""" """Flash firmware to the SiLabs device."""
firmware_info = await guess_firmware_info(hass, device) async with async_firmware_update_context(hass, device, domain):
_LOGGER.debug("Identified firmware info: %s", firmware_info) firmware_info = await guess_firmware_info(hass, device)
_LOGGER.debug("Identified firmware info: %s", firmware_info)
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data) fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
flasher = Flasher( flasher = Flasher(
device=device, device=device,
probe_methods=( probe_methods=(
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(), ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
ApplicationType.EZSP.as_flasher_application_type(), ApplicationType.EZSP.as_flasher_application_type(),
ApplicationType.SPINEL.as_flasher_application_type(), ApplicationType.SPINEL.as_flasher_application_type(),
ApplicationType.CPC.as_flasher_application_type(), ApplicationType.CPC.as_flasher_application_type(),
), ),
bootloader_reset=tuple( bootloader_reset=tuple(
m.as_flasher_reset_target() for m in bootloader_reset_methods m.as_flasher_reset_target() for m in bootloader_reset_methods
), ),
)
async with AsyncExitStack() as stack:
for owner in firmware_info.owners:
await stack.enter_async_context(owner.temporarily_stop(hass))
try:
# Enter the bootloader with indeterminate progress
await flasher.enter_bootloader()
# Flash the firmware, with progress
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
except Exception as err:
raise HomeAssistantError("Failed to flash firmware") from err
probed_firmware_info = await probe_silabs_firmware_info(
device,
probe_methods=(expected_installed_firmware_type,),
) )
if probed_firmware_info is None: async with AsyncExitStack() as stack:
raise HomeAssistantError("Failed to probe the firmware after flashing") for owner in firmware_info.owners:
await stack.enter_async_context(owner.temporarily_stop(hass))
return probed_firmware_info try:
# Enter the bootloader with indeterminate progress
await flasher.enter_bootloader()
# Flash the firmware, with progress
await flasher.flash_firmware(
fw_image, progress_callback=progress_callback
)
except Exception as err:
raise HomeAssistantError("Failed to flash firmware") from err
probed_firmware_info = await probe_silabs_firmware_info(
device,
probe_methods=(expected_installed_firmware_type,),
)
if probed_firmware_info is None:
raise HomeAssistantError("Failed to probe the firmware after flashing")
return probed_firmware_info

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.18"], "requirements": ["aiohomekit==3.2.19"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@@ -8,13 +8,16 @@ from idasen_ha import Desk
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator] type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator]
UPDATE_DEBOUNCE_TIME = 0.2
class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
"""Class to manage updates for the Idasen Desk.""" """Class to manage updates for the Idasen Desk."""
@@ -33,9 +36,22 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
hass, _LOGGER, config_entry=config_entry, name=config_entry.title hass, _LOGGER, config_entry=config_entry, name=config_entry.title
) )
self.address = address self.address = address
self._expected_connected = False self.desk = Desk(self._async_handle_update)
self.desk = Desk(self.async_set_updated_data) self._expected_connected = False
self._height: int | None = None
@callback
def async_update_data() -> None:
self.async_set_updated_data(self._height)
self._debouncer = Debouncer(
hass=self.hass,
logger=_LOGGER,
cooldown=UPDATE_DEBOUNCE_TIME,
immediate=True,
function=async_update_data,
)
async def async_connect(self) -> bool: async def async_connect(self) -> bool:
"""Connect to desk.""" """Connect to desk."""
@@ -60,3 +76,9 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
"""Ensure that the desk is connected if that is the expected state.""" """Ensure that the desk is connected if that is the expected state."""
if self._expected_connected: if self._expected_connected:
await self.async_connect() await self.async_connect()
@callback
def _async_handle_update(self, height: int | None) -> None:
"""Handle an update from the desk."""
self._height = height
self._debouncer.async_schedule_call()

View File

@@ -1,12 +1,12 @@
{ {
"domain": "iometer", "domain": "iometer",
"name": "IOmeter", "name": "IOmeter",
"codeowners": ["@MaestroOnICe"], "codeowners": ["@jukrebs"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iometer", "documentation": "https://www.home-assistant.io/integrations/iometer",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["iometer==0.1.0"], "requirements": ["iometer==0.2.0"],
"zeroconf": ["_iometer._tcp.local."] "zeroconf": ["_iometer._tcp.local."]
} }

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylamarzocco"], "loggers": ["pylamarzocco"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pylamarzocco==2.1.1"] "requirements": ["pylamarzocco==2.1.2"]
} }

View File

@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.translation import async_get_cached_translations
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
@@ -62,12 +63,15 @@ class MediaSourceItem:
async def async_browse(self) -> BrowseMediaSource: async def async_browse(self) -> BrowseMediaSource:
"""Browse this item.""" """Browse this item."""
if self.domain is None: if self.domain is None:
title = async_get_cached_translations(
self.hass, self.hass.config.language, "common", "media_source"
).get("component.media_source.common.sources_default", "Media Sources")
base = BrowseMediaSource( base = BrowseMediaSource(
domain=None, domain=None,
identifier=None, identifier=None,
media_class=MediaClass.APP, media_class=MediaClass.APP,
media_content_type=MediaType.APPS, media_content_type=MediaType.APPS,
title="Media Sources", title=title,
can_play=False, can_play=False,
can_expand=True, can_expand=True,
children_media_class=MediaClass.APP, children_media_class=MediaClass.APP,

View File

@@ -9,5 +9,8 @@
"unknown_media_source": { "unknown_media_source": {
"message": "Unknown media source: {domain}" "message": "Unknown media source: {domain}"
} }
},
"common": {
"sources_default": "Media sources"
} }
} }

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/melcloud", "documentation": "https://www.home-assistant.io/integrations/melcloud",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pymelcloud"], "loggers": ["pymelcloud"],
"requirements": ["python-melcloud==0.1.0"] "requirements": ["python-melcloud==0.1.2"]
} }

View File

@@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PLATE_COUNT = 4 DEFAULT_PLATE_COUNT = 4
PLATE_COUNT = { PLATE_COUNT = {
"KM7575": 6,
"KM7678": 6, "KM7678": 6,
"KM7697": 6, "KM7697": 6,
"KM7878": 6, "KM7878": 6,

View File

@@ -208,7 +208,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
def __process_raw_value(self, entry: float | str | bytes) -> str | None: def __process_raw_value(self, entry: float | str | bytes) -> str | None:
"""Process value from sensor with NaN handling, scaling, offset, min/max etc.""" """Process value from sensor with NaN handling, scaling, offset, min/max etc."""
if self._nan_value and entry in (self._nan_value, -self._nan_value): if self._nan_value is not None and entry in (self._nan_value, -self._nan_value):
return None return None
if isinstance(entry, bytes): if isinstance(entry, bytes):
return entry.decode() return entry.decode()

View File

@@ -253,6 +253,7 @@ class ModbusHub:
self._client: ( self._client: (
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
) = None ) = None
self._lock = asyncio.Lock()
self.event_connected = asyncio.Event() self.event_connected = asyncio.Event()
self.hass = hass self.hass = hass
self.name = client_config[CONF_NAME] self.name = client_config[CONF_NAME]
@@ -415,7 +416,9 @@ class ModbusHub:
"""Convert async to sync pymodbus call.""" """Convert async to sync pymodbus call."""
if not self._client: if not self._client:
return None return None
result = await self.low_level_pb_call(unit, address, value, use_call) async with self._lock:
if self._msg_wait: result = await self.low_level_pb_call(unit, address, value, use_call)
await asyncio.sleep(self._msg_wait) if self._msg_wait:
return result # small delay until next request/response
await asyncio.sleep(self._msg_wait)
return result

View File

@@ -188,7 +188,10 @@ class MqttLock(MqttEntity, LockEntity):
return return
if payload == self._config[CONF_PAYLOAD_RESET]: if payload == self._config[CONF_PAYLOAD_RESET]:
# Reset the state to `unknown` # Reset the state to `unknown`
self._attr_is_locked = None self._attr_is_locked = self._attr_is_locking = None
self._attr_is_unlocking = None
self._attr_is_open = self._attr_is_opening = None
self._attr_is_jammed = None
elif payload in self._valid_states: elif payload in self._valid_states:
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED] self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING] self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]

View File

@@ -34,6 +34,7 @@ async def async_setup_entry(
coordinator = NordPoolDataUpdateCoordinator(hass, config_entry) coordinator = NordPoolDataUpdateCoordinator(hass, config_entry)
await coordinator.fetch_data(dt_util.utcnow(), True) await coordinator.fetch_data(dt_util.utcnow(), True)
await coordinator.update_listeners(dt_util.utcnow())
if not coordinator.last_update_success: if not coordinator.last_update_success:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_domain=DOMAIN,

View File

@@ -44,9 +44,10 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
name=DOMAIN, name=DOMAIN,
) )
self.client = NordPoolClient(session=async_get_clientsession(hass)) self.client = NordPoolClient(session=async_get_clientsession(hass))
self.unsub: Callable[[], None] | None = None self.data_unsub: Callable[[], None] | None = None
self.listener_unsub: Callable[[], None] | None = None
def get_next_interval(self, now: datetime) -> datetime: def get_next_data_interval(self, now: datetime) -> datetime:
"""Compute next time an update should occur.""" """Compute next time an update should occur."""
next_hour = dt_util.utcnow() + timedelta(hours=1) next_hour = dt_util.utcnow() + timedelta(hours=1)
next_run = datetime( next_run = datetime(
@@ -56,23 +57,45 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
next_hour.hour, next_hour.hour,
tzinfo=dt_util.UTC, tzinfo=dt_util.UTC,
) )
LOGGER.debug("Next update at %s", next_run) LOGGER.debug("Next data update at %s", next_run)
return next_run
def get_next_15_interval(self, now: datetime) -> datetime:
"""Compute next time we need to notify listeners."""
next_run = dt_util.utcnow() + timedelta(minutes=15)
next_minute = next_run.minute // 15 * 15
next_run = next_run.replace(
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
)
LOGGER.debug("Next listener update at %s", next_run)
return next_run return next_run
async def async_shutdown(self) -> None: async def async_shutdown(self) -> None:
"""Cancel any scheduled call, and ignore new runs.""" """Cancel any scheduled call, and ignore new runs."""
await super().async_shutdown() await super().async_shutdown()
if self.unsub: if self.data_unsub:
self.unsub() self.data_unsub()
self.unsub = None self.data_unsub = None
if self.listener_unsub:
self.listener_unsub()
self.listener_unsub = None
async def update_listeners(self, now: datetime) -> None:
"""Update entity listeners."""
self.listener_unsub = async_track_point_in_utc_time(
self.hass,
self.update_listeners,
self.get_next_15_interval(dt_util.utcnow()),
)
self.async_update_listeners()
async def fetch_data(self, now: datetime, initial: bool = False) -> None: async def fetch_data(self, now: datetime, initial: bool = False) -> None:
"""Fetch data from Nord Pool.""" """Fetch data from Nord Pool."""
self.unsub = async_track_point_in_utc_time( self.data_unsub = async_track_point_in_utc_time(
self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow()) self.hass, self.fetch_data, self.get_next_data_interval(dt_util.utcnow())
) )
if self.config_entry.pref_disable_polling and not initial: if self.config_entry.pref_disable_polling and not initial:
self.async_update_listeners()
return return
try: try:
data = await self.handle_data(initial) data = await self.handle_data(initial)

View File

@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
) from error ) from error
except NordPoolEmptyResponseError: except NordPoolEmptyResponseError:
return {area: [] for area in areas} return {area: [] for area in areas}
except NordPoolError as error: except (NordPoolError, TimeoutError) as error:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="connection_error", translation_key="connection_error",

View File

@@ -307,7 +307,7 @@
}, },
"markdown": { "markdown": {
"name": "Format as Markdown", "name": "Format as Markdown",
"description": "Enable Markdown formatting for the message body (Web app only). See the Markdown guide for syntax details: https://www.markdownguide.org/basic-syntax/." "description": "Enable Markdown formatting for the message body. See the Markdown guide for syntax details: https://www.markdownguide.org/basic-syntax/."
}, },
"tags": { "tags": {
"name": "Tags/Emojis", "name": "Tags/Emojis",

View File

@@ -35,7 +35,7 @@ from .const import CONF_DELETE_PERMANENTLY, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .coordinator import OneDriveConfigEntry from .coordinator import OneDriveConfigEntry
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UPLOAD_CHUNK_SIZE = 16 * 320 * 1024 # 5.2MB UPLOAD_CHUNK_SIZE = 32 * 320 * 1024 # 10.4MB
TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours
METADATA_VERSION = 2 METADATA_VERSION = 2
CACHE_TTL = 300 CACHE_TTL = 300
@@ -163,7 +163,10 @@ class OneDriveBackupAgent(BackupAgent):
) )
try: try:
backup_file = await LargeFileUploadClient.upload( backup_file = await LargeFileUploadClient.upload(
self._token_function, file, session=async_get_clientsession(self._hass) self._token_function,
file,
upload_chunk_size=UPLOAD_CHUNK_SIZE,
session=async_get_clientsession(self._hass),
) )
except HashMismatchError as err: except HashMismatchError as err:
raise BackupAgentError( raise BackupAgentError(

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["opower"], "loggers": ["opower"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["opower==0.15.5"] "requirements": ["opower==0.15.6"]
} }

View File

@@ -75,6 +75,9 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str:
if device and ("Connect_ZBT-1" in device or "SkyConnect" in device): if device and ("Connect_ZBT-1" in device or "SkyConnect" in device):
return f"Home Assistant Connect ZBT-1 ({discovery_info.name})" return f"Home Assistant Connect ZBT-1 ({discovery_info.name})"
if device and "Nabu_Casa_ZBT-2" in device:
return f"Home Assistant Connect ZBT-2 ({discovery_info.name})"
return discovery_info.name return discovery_info.name

View File

@@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["ovoenergy"], "loggers": ["ovoenergy"],
"requirements": ["ovoenergy==2.0.1"] "requirements": ["ovoenergy==3.0.1"]
} }

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/portainer", "documentation": "https://www.home-assistant.io/integrations/portainer",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pyportainer==0.1.7"] "requirements": ["pyportainer==1.0.3"]
} }

View File

@@ -215,6 +215,7 @@ def create_coordinator_container_vm(
return DataUpdateCoordinator( return DataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
config_entry=None,
name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}", name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
update_method=async_update_data, update_method=async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL), update_interval=timedelta(seconds=UPDATE_INTERVAL),

View File

@@ -16,7 +16,6 @@ ATTR_HTML: Final = "html"
ATTR_CALLBACK_URL: Final = "callback_url" ATTR_CALLBACK_URL: Final = "callback_url"
ATTR_EXPIRE: Final = "expire" ATTR_EXPIRE: Final = "expire"
ATTR_TTL: Final = "ttl" ATTR_TTL: Final = "ttl"
ATTR_DATA: Final = "data"
ATTR_TIMESTAMP: Final = "timestamp" ATTR_TIMESTAMP: Final = "timestamp"
CONF_USER_KEY: Final = "user_key" CONF_USER_KEY: Final = "user_key"

View File

@@ -67,7 +67,7 @@ class PushoverNotificationService(BaseNotificationService):
# Extract params from data dict # Extract params from data dict
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA, {}) data = kwargs.get(ATTR_DATA) or {}
url = data.get(ATTR_URL) url = data.get(ATTR_URL)
url_title = data.get(ATTR_URL_TITLE) url_title = data.get(ATTR_URL_TITLE)
priority = data.get(ATTR_PRIORITY) priority = data.get(ATTR_PRIORITY)

View File

@@ -39,6 +39,23 @@ from .renault_vehicle import COORDINATORS, RenaultVehicleProxy
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
async def _get_filtered_vehicles(account: RenaultAccount) -> list[KamereonVehiclesLink]:
"""Filter out vehicles with missing details.
May be due to new purchases, or issue with the Renault servers.
"""
vehicles = await account.get_vehicles()
if not vehicles.vehicleLinks:
return []
result: list[KamereonVehiclesLink] = []
for link in vehicles.vehicleLinks:
if link.vehicleDetails is None:
LOGGER.warning("Ignoring vehicle with missing details: %s", link.vin)
continue
result.append(link)
return result
class RenaultHub: class RenaultHub:
"""Handle account communication with Renault servers.""" """Handle account communication with Renault servers."""
@@ -84,49 +101,48 @@ class RenaultHub:
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID] account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
self._account = await self._client.get_api_account(account_id) self._account = await self._client.get_api_account(account_id)
vehicles = await self._account.get_vehicles() vehicle_links = await _get_filtered_vehicles(self._account)
if vehicles.vehicleLinks: if not vehicle_links:
if any( LOGGER.debug(
vehicle_link.vehicleDetails is None "No valid vehicle details found for account_id: %s", account_id
for vehicle_link in vehicles.vehicleLinks )
): raise ConfigEntryNotReady(
raise ConfigEntryNotReady( "Failed to retrieve vehicle details from Renault servers"
"Failed to retrieve vehicle details from Renault servers"
)
num_call_per_scan = len(COORDINATORS) * len(vehicles.vehicleLinks)
scan_interval = timedelta(
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
) )
device_registry = dr.async_get(self._hass) num_call_per_scan = len(COORDINATORS) * len(vehicle_links)
await asyncio.gather( scan_interval = timedelta(
*( seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
self.async_initialise_vehicle( )
vehicle_link,
self._account,
scan_interval,
config_entry,
device_registry,
)
for vehicle_link in vehicles.vehicleLinks
)
)
# all vehicles have been initiated with the right number of active coordinators device_registry = dr.async_get(self._hass)
num_call_per_scan = 0 await asyncio.gather(
for vehicle_link in vehicles.vehicleLinks: *(
self.async_initialise_vehicle(
vehicle_link,
self._account,
scan_interval,
config_entry,
device_registry,
)
for vehicle_link in vehicle_links
)
)
# all vehicles have been initiated with the right number of active coordinators
num_call_per_scan = 0
for vehicle_link in vehicle_links:
vehicle = self._vehicles[str(vehicle_link.vin)]
num_call_per_scan += len(vehicle.coordinators)
new_scan_interval = timedelta(
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
)
if new_scan_interval != scan_interval:
# we need to change the vehicles with the right scan interval
for vehicle_link in vehicle_links:
vehicle = self._vehicles[str(vehicle_link.vin)] vehicle = self._vehicles[str(vehicle_link.vin)]
num_call_per_scan += len(vehicle.coordinators) vehicle.update_scan_interval(new_scan_interval)
new_scan_interval = timedelta(
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
)
if new_scan_interval != scan_interval:
# we need to change the vehicles with the right scan interval
for vehicle_link in vehicles.vehicleLinks:
vehicle = self._vehicles[str(vehicle_link.vin)]
vehicle.update_scan_interval(new_scan_interval)
async def async_initialise_vehicle( async def async_initialise_vehicle(
self, self,
@@ -164,10 +180,10 @@ class RenaultHub:
"""Get Kamereon account ids.""" """Get Kamereon account ids."""
accounts = [] accounts = []
for account in await self._client.get_api_accounts(): for account in await self._client.get_api_accounts():
vehicles = await account.get_vehicles() vehicle_links = await _get_filtered_vehicles(account)
# Only add the account if it has linked vehicles. # Only add the account if it has linked vehicles.
if vehicles.vehicleLinks: if vehicle_links:
accounts.append(account.account_id) accounts.append(account.account_id)
return accounts return accounts

View File

@@ -19,5 +19,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["reolink-aio==0.16.0"] "requirements": ["reolink-aio==0.16.1"]
} }

View File

@@ -82,7 +82,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
assert self._client assert self._client
errors: dict[str, str] = {} errors: dict[str, str] = {}
try: try:
await self._client.request_code() await self._client.request_code_v4()
except RoborockAccountDoesNotExist: except RoborockAccountDoesNotExist:
errors["base"] = "invalid_email" errors["base"] = "invalid_email"
except RoborockUrlException: except RoborockUrlException:
@@ -111,7 +111,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
code = user_input[CONF_ENTRY_CODE] code = user_input[CONF_ENTRY_CODE]
_LOGGER.debug("Logging into Roborock account using email provided code") _LOGGER.debug("Logging into Roborock account using email provided code")
try: try:
user_data = await self._client.code_login(code) user_data = await self._client.code_login_v4(code)
except RoborockInvalidCode: except RoborockInvalidCode:
errors["base"] = "invalid_code" errors["base"] = "invalid_code"
except RoborockException: except RoborockException:
@@ -129,7 +129,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()} reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()}
) )
self._abort_if_unique_id_configured(error="already_configured_account") self._abort_if_unique_id_configured(error="already_configured_account")
return self._create_entry(self._client, self._username, user_data) return await self._create_entry(self._client, self._username, user_data)
return self.async_show_form( return self.async_show_form(
step_id="code", step_id="code",
@@ -176,7 +176,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_code() return await self.async_step_code()
return self.async_show_form(step_id="reauth_confirm", errors=errors) return self.async_show_form(step_id="reauth_confirm", errors=errors)
def _create_entry( async def _create_entry(
self, client: RoborockApiClient, username: str, user_data: UserData self, client: RoborockApiClient, username: str, user_data: UserData
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Finished config flow and create entry.""" """Finished config flow and create entry."""
@@ -185,7 +185,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
data={ data={
CONF_USERNAME: username, CONF_USERNAME: username,
CONF_USER_DATA: user_data.as_dict(), CONF_USER_DATA: user_data.as_dict(),
CONF_BASE_URL: client.base_url, CONF_BASE_URL: await client.base_url,
}, },
) )

View File

@@ -19,7 +19,7 @@
"loggers": ["roborock"], "loggers": ["roborock"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [ "requirements": [
"python-roborock==2.47.1", "python-roborock==2.50.2",
"vacuum-map-parser-roborock==0.1.4" "vacuum-map-parser-roborock==0.1.4"
] ]
} }

View File

@@ -377,8 +377,10 @@
"max": "Max", "max": "Max",
"high": "[%key:common::state::high%]", "high": "[%key:common::state::high%]",
"intense": "Intense", "intense": "Intense",
"extreme": "Extreme",
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]", "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
"custom_water_flow": "Custom water flow", "custom_water_flow": "Custom water flow",
"vac_followed_by_mop": "Vacuum followed by mop",
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]" "smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]"
} }
}, },

View File

@@ -13,8 +13,10 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
CONF_OUTPUT_NUMBER, CONF_OUTPUT_NUMBER,
CONF_OUTPUTS,
CONF_ZONE_NUMBER, CONF_ZONE_NUMBER,
CONF_ZONE_TYPE, CONF_ZONE_TYPE,
CONF_ZONES,
SIGNAL_OUTPUTS_UPDATED, SIGNAL_OUTPUTS_UPDATED,
SIGNAL_ZONES_UPDATED, SIGNAL_ZONES_UPDATED,
SUBENTRY_TYPE_OUTPUT, SUBENTRY_TYPE_OUTPUT,
@@ -49,7 +51,7 @@ async def async_setup_entry(
zone_num, zone_num,
zone_name, zone_name,
zone_type, zone_type,
SUBENTRY_TYPE_ZONE, CONF_ZONES,
SIGNAL_ZONES_UPDATED, SIGNAL_ZONES_UPDATED,
) )
], ],
@@ -73,7 +75,7 @@ async def async_setup_entry(
output_num, output_num,
output_name, output_name,
ouput_type, ouput_type,
SUBENTRY_TYPE_OUTPUT, CONF_OUTPUTS,
SIGNAL_OUTPUTS_UPDATED, SIGNAL_OUTPUTS_UPDATED,
) )
], ],

View File

@@ -24,6 +24,7 @@
}, },
"config_subentries": { "config_subentries": {
"partition": { "partition": {
"entry_type": "Partition",
"initiate_flow": { "initiate_flow": {
"user": "Add partition" "user": "Add partition"
}, },
@@ -57,6 +58,7 @@
} }
}, },
"zone": { "zone": {
"entry_type": "Zone",
"initiate_flow": { "initiate_flow": {
"user": "Add zone" "user": "Add zone"
}, },
@@ -91,6 +93,7 @@
} }
}, },
"output": { "output": {
"entry_type": "Output",
"initiate_flow": { "initiate_flow": {
"user": "Add output" "user": "Add output"
}, },
@@ -125,6 +128,7 @@
} }
}, },
"switchable_output": { "switchable_output": {
"entry_type": "Switchable output",
"initiate_flow": { "initiate_flow": {
"user": "Add switchable output" "user": "Add switchable output"
}, },

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sharkiq", "documentation": "https://www.home-assistant.io/integrations/sharkiq",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["sharkiq"], "loggers": ["sharkiq"],
"requirements": ["sharkiq==1.4.0"] "requirements": ["sharkiq==1.4.2"]
} }

View File

@@ -319,7 +319,7 @@ RPC_SENSORS: Final = {
), ),
"presencezone_state": RpcBinarySensorDescription( "presencezone_state": RpcBinarySensorDescription(
key="presencezone", key="presencezone",
sub_key="state", sub_key="value",
name="Occupancy", name="Occupancy",
device_class=BinarySensorDeviceClass.OCCUPANCY, device_class=BinarySensorDeviceClass.OCCUPANCY,
entity_class=RpcPresenceBinarySensor, entity_class=RpcPresenceBinarySensor,

View File

@@ -226,6 +226,8 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
def _update_callback(self) -> None: def _update_callback(self) -> None:
"""Handle device update. Use a task when opening/closing is in progress.""" """Handle device update. Use a task when opening/closing is in progress."""
super()._update_callback() super()._update_callback()
if not self.coordinator.device.initialized:
return
if self.is_closing or self.is_opening: if self.is_closing or self.is_opening:
self.launch_update_task() self.launch_update_task()

View File

@@ -692,27 +692,25 @@ def async_remove_orphaned_entities(
"""Remove orphaned entities.""" """Remove orphaned entities."""
orphaned_entities = [] orphaned_entities = []
entity_reg = er.async_get(hass) entity_reg = er.async_get(hass)
device_reg = dr.async_get(hass)
if not ( entities = er.async_entries_for_config_entry(entity_reg, config_entry_id)
devices := device_reg.devices.get_devices_for_config_entry_id(config_entry_id) for entity in entities:
): if not entity.entity_id.startswith(platform):
return continue
if key_suffix is not None and key_suffix not in entity.unique_id:
continue
# we are looking for the component ID, e.g. boolean:201, em1data:1
if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
continue
for device in devices: key = match.group()
entities = er.async_entries_for_device(entity_reg, device.id, True) if key not in keys:
for entity in entities: LOGGER.debug(
if not entity.entity_id.startswith(platform): "Found orphaned Shelly entity: %s, unique id: %s",
continue entity.entity_id,
if key_suffix is not None and key_suffix not in entity.unique_id: entity.unique_id,
continue )
# we are looking for the component ID, e.g. boolean:201, em1data:1 orphaned_entities.append(entity.unique_id.split("-", 1)[1])
if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
continue
key = match.group()
if key not in keys:
orphaned_entities.append(entity.unique_id.split("-", 1)[1])
if orphaned_entities: if orphaned_entities:
async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities) async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)

View File

@@ -100,8 +100,9 @@ ATTR_PIN_VALUE = "pin"
ATTR_TIMESTAMP = "timestamp" ATTR_TIMESTAMP = "timestamp"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
DEFAULT_SOCKET_MIN_RETRY = 15
WEBSOCKET_RECONNECT_RETRIES = 3
WEBSOCKET_RETRY_DELAY = 2
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
@@ -419,6 +420,7 @@ class SimpliSafe:
self._api = api self._api = api
self._hass = hass self._hass = hass
self._system_notifications: dict[int, set[SystemNotification]] = {} self._system_notifications: dict[int, set[SystemNotification]] = {}
self._websocket_reconnect_retries: int = 0
self._websocket_reconnect_task: asyncio.Task | None = None self._websocket_reconnect_task: asyncio.Task | None = None
self.entry = entry self.entry = entry
self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.initial_event_to_use: dict[int, dict[str, Any]] = {}
@@ -469,6 +471,8 @@ class SimpliSafe:
"""Start a websocket reconnection loop.""" """Start a websocket reconnection loop."""
assert self._api.websocket assert self._api.websocket
self._websocket_reconnect_retries += 1
try: try:
await self._api.websocket.async_connect() await self._api.websocket.async_connect()
await self._api.websocket.async_listen() await self._api.websocket.async_listen()
@@ -479,9 +483,21 @@ class SimpliSafe:
LOGGER.error("Failed to connect to websocket: %s", err) LOGGER.error("Failed to connect to websocket: %s", err)
except Exception as err: # noqa: BLE001 except Exception as err: # noqa: BLE001
LOGGER.error("Unknown exception while connecting to websocket: %s", err) LOGGER.error("Unknown exception while connecting to websocket: %s", err)
else:
self._websocket_reconnect_retries = 0
LOGGER.debug("Reconnecting to websocket") if self._websocket_reconnect_retries >= WEBSOCKET_RECONNECT_RETRIES:
await self._async_cancel_websocket_loop() LOGGER.error("Max websocket connection retries exceeded")
return
delay = WEBSOCKET_RETRY_DELAY * (2 ** (self._websocket_reconnect_retries - 1))
LOGGER.info(
"Retrying websocket connection in %s seconds (attempt %s/%s)",
delay,
self._websocket_reconnect_retries,
WEBSOCKET_RECONNECT_RETRIES,
)
await asyncio.sleep(delay)
self._websocket_reconnect_task = self._hass.async_create_task( self._websocket_reconnect_task = self._hass.async_create_task(
self._async_start_websocket_loop() self._async_start_websocket_loop()
) )

View File

@@ -5,14 +5,14 @@
"description": "Refer to the documentation on getting your Slack API key.", "description": "Refer to the documentation on getting your Slack API key.",
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
"default_channel": "Default Channel", "default_channel": "Default channel",
"icon": "Icon", "icon": "Icon",
"username": "[%key:common::config_flow::data::username%]" "username": "[%key:common::config_flow::data::username%]"
}, },
"data_description": { "data_description": {
"api_key": "The Slack API token to use for sending Slack messages.", "api_key": "The Slack API token to use for sending Slack messages.",
"default_channel": "The channel to post to if no channel is specified when sending a message.", "default_channel": "The channel to post to if no channel is specified when sending a message.",
"icon": "Use one of the Slack emojis as an Icon for the supplied username.", "icon": "Use one of the Slack emojis as an icon for the supplied username.",
"username": "Home Assistant will post to Slack using the username specified." "username": "Home Assistant will post to Slack using the username specified."
} }
} }

View File

@@ -109,6 +109,8 @@ PRESET_MODE_TO_HA = {
"quiet": "quiet", "quiet": "quiet",
"longWind": "long_wind", "longWind": "long_wind",
"smart": "smart", "smart": "smart",
"motionIndirect": "motion_indirect",
"motionDirect": "motion_direct",
} }
HA_MODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_HA.items()} HA_MODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_HA.items()}

View File

@@ -31,6 +31,17 @@
"default": "mdi:stop" "default": "mdi:stop"
} }
}, },
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"turbo": "mdi:wind-power"
}
}
}
}
},
"number": { "number": {
"washer_rinse_cycles": { "washer_rinse_cycles": {
"default": "mdi:waves-arrow-up" "default": "mdi:waves-arrow-up"

View File

@@ -30,5 +30,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmartthings"], "loggers": ["pysmartthings"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pysmartthings==3.3.0"] "requirements": ["pysmartthings==3.3.1"]
} }

View File

@@ -1151,8 +1151,11 @@ async def async_setup_entry(
) )
and ( and (
not description.exists_fn not description.exists_fn
or description.exists_fn( or (
device.status[MAIN][capability][attribute] component == MAIN
and description.exists_fn(
device.status[MAIN][capability][attribute]
)
) )
) )
and ( and (

View File

@@ -87,7 +87,14 @@
"wind_free_sleep": "WindFree sleep", "wind_free_sleep": "WindFree sleep",
"quiet": "Quiet", "quiet": "Quiet",
"long_wind": "Long wind", "long_wind": "Long wind",
"smart": "Smart" "smart": "Smart",
"motion_direct": "Motion direct",
"motion_indirect": "Motion indirect"
}
},
"fan_mode": {
"state": {
"turbo": "Turbo"
} }
} }
} }

View File

@@ -10,6 +10,28 @@
"zigbee_type": { "zigbee_type": {
"default": "mdi:zigbee" "default": "mdi:zigbee"
} }
},
"switch": {
"disable_led": {
"default": "mdi:led-off"
},
"auto_zigbee_update": {
"default": "mdi:autorenew"
},
"night_mode": {
"default": "mdi:lightbulb-night"
},
"vpn_enabled": {
"default": "mdi:shield-lock"
}
},
"button": {
"zigbee_flash_mode": {
"default": "mdi:memory-arrow-down"
},
"reconnect_zigbee_router": {
"default": "mdi:connection"
}
} }
} }
} }

View File

@@ -51,7 +51,6 @@ SWITCHES: list[SmSwitchEntityDescription] = [
SmSwitchEntityDescription( SmSwitchEntityDescription(
key="auto_zigbee_update", key="auto_zigbee_update",
translation_key="auto_zigbee_update", translation_key="auto_zigbee_update",
entity_category=EntityCategory.CONFIG,
setting=Settings.ZB_AUTOUPDATE, setting=Settings.ZB_AUTOUPDATE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_fn=lambda x: x.auto_zigbee, state_fn=lambda x: x.auto_zigbee,
@@ -83,6 +82,7 @@ class SmSwitch(SmEntity, SwitchEntity):
coordinator: SmDataUpdateCoordinator coordinator: SmDataUpdateCoordinator
entity_description: SmSwitchEntityDescription entity_description: SmSwitchEntityDescription
_attr_device_class = SwitchDeviceClass.SWITCH _attr_device_class = SwitchDeviceClass.SWITCH
_attr_entity_category = EntityCategory.CONFIG
def __init__( def __init__(
self, self,

View File

@@ -193,7 +193,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
if player.player_id in entry.runtime_data.known_player_ids: if player.player_id in entry.runtime_data.known_player_ids:
await player.async_update() await player.async_update()
async_dispatcher_send( async_dispatcher_send(
hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected hass,
SIGNAL_PLAYER_REDISCOVERED + entry.entry_id,
player.player_id,
player.connected,
) )
else: else:
_LOGGER.debug("Adding new entity: %s", player) _LOGGER.debug("Adding new entity: %s", player)
@@ -203,7 +206,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
await player_coordinator.async_refresh() await player_coordinator.async_refresh()
entry.runtime_data.known_player_ids.add(player.player_id) entry.runtime_data.known_player_ids.add(player.player_id)
async_dispatcher_send( async_dispatcher_send(
hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, player_coordinator
) )
if players := await lms.async_get_players(): if players := await lms.async_get_players():

View File

@@ -132,7 +132,9 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
entry.async_on_unload( entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered) async_dispatcher_connect(
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
)
) )

View File

@@ -117,7 +117,9 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# start listening for restored players # start listening for restored players
self._remove_dispatcher = async_dispatcher_connect( self._remove_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered self.hass,
SIGNAL_PLAYER_REDISCOVERED + self.config_entry.entry_id,
self.rediscovered,
) )
alarm_dict: dict[str, Alarm] = ( alarm_dict: dict[str, Alarm] = (

View File

@@ -175,7 +175,9 @@ async def async_setup_entry(
async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)]) async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)])
entry.async_on_unload( entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered) async_dispatcher_connect(
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
)
) )
# Register entity services # Register entity services

View File

@@ -89,7 +89,9 @@ async def async_setup_entry(
async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)]) async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)])
entry.async_on_unload( entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered) async_dispatcher_connect(
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
)
) )

View File

@@ -143,6 +143,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
self.reauth_conf: Mapping[str, Any] = {} self.reauth_conf: Mapping[str, Any] = {}
self.reauth_reason: str | None = None self.reauth_reason: str | None = None
self.shares: list[SynoFileSharedFolder] | None = None self.shares: list[SynoFileSharedFolder] | None = None
self.api: SynologyDSM | None = None
def _show_form( def _show_form(
self, self,
@@ -156,6 +157,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
description_placeholders = {} description_placeholders = {}
data_schema = None data_schema = None
self.api = None
if step_id == "link": if step_id == "link":
user_input.update(self.discovered_conf) user_input.update(self.discovered_conf)
@@ -194,14 +196,21 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
else: else:
port = DEFAULT_PORT port = DEFAULT_PORT
session = async_get_clientsession(self.hass, verify_ssl) if self.api is None:
api = SynologyDSM( session = async_get_clientsession(self.hass, verify_ssl)
session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT self.api = SynologyDSM(
) session,
host,
port,
username,
password,
use_ssl,
timeout=DEFAULT_TIMEOUT,
)
errors = {} errors = {}
try: try:
serial = await _login_and_fetch_syno_info(api, otp_code) serial = await _login_and_fetch_syno_info(self.api, otp_code)
except SynologyDSMLogin2SARequiredException: except SynologyDSMLogin2SARequiredException:
return await self.async_step_2sa(user_input) return await self.async_step_2sa(user_input)
except SynologyDSMLogin2SAFailedException: except SynologyDSMLogin2SAFailedException:
@@ -221,10 +230,11 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "missing_data" errors["base"] = "missing_data"
if errors: if errors:
self.api = None
return self._show_form(step_id, user_input, errors) return self._show_form(step_id, user_input, errors)
with suppress(*SYNOLOGY_CONNECTION_EXCEPTIONS): with suppress(*SYNOLOGY_CONNECTION_EXCEPTIONS):
self.shares = await api.file.get_shared_folders(only_writable=True) self.shares = await self.api.file.get_shared_folders(only_writable=True)
if self.shares and not backup_path: if self.shares and not backup_path:
return await self.async_step_backup_share(user_input) return await self.async_step_backup_share(user_input)
@@ -239,14 +249,14 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_VERIFY_SSL: verify_ssl, CONF_VERIFY_SSL: verify_ssl,
CONF_USERNAME: username, CONF_USERNAME: username,
CONF_PASSWORD: password, CONF_PASSWORD: password,
CONF_MAC: api.network.macs, CONF_MAC: self.api.network.macs,
} }
config_options = { config_options = {
CONF_BACKUP_PATH: backup_path, CONF_BACKUP_PATH: backup_path,
CONF_BACKUP_SHARE: backup_share, CONF_BACKUP_SHARE: backup_share,
} }
if otp_code: if otp_code:
config_data[CONF_DEVICE_TOKEN] = api.device_token config_data[CONF_DEVICE_TOKEN] = self.api.device_token
if user_input.get(CONF_DISKS): if user_input.get(CONF_DISKS):
config_data[CONF_DISKS] = user_input[CONF_DISKS] config_data[CONF_DISKS] = user_input[CONF_DISKS]
if user_input.get(CONF_VOLUMES): if user_input.get(CONF_VOLUMES):

View File

@@ -336,6 +336,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
key="power_usage", key="power_usage",
translation_key="power_usage", translation_key="power_usage",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT, native_unit_of_measurement=UnitOfPower.WATT,
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:power-plug", icon="mdi:power-plug",
@@ -577,7 +578,6 @@ async def async_setup_entry(
key=f"gpu_{gpu.id}_power_usage", key=f"gpu_{gpu.id}_power_usage",
name=f"{gpu.name} power usage", name=f"{gpu.name} power usage",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT, native_unit_of_measurement=UnitOfPower.WATT,
value=lambda data, k=index: gpu_power_usage(data, k), value=lambda data, k=index: gpu_power_usage(data, k),

View File

@@ -372,6 +372,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
def _set_state(self, state, _=None): def _set_state(self, state, _=None):
"""Set up auto off.""" """Set up auto off."""
self._attr_is_on = state self._attr_is_on = state
self._delay_cancel = None
self.async_write_ha_state() self.async_write_ha_state()
if not state: if not state:

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tibber", "documentation": "https://www.home-assistant.io/integrations/tibber",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tibber"], "loggers": ["tibber"],
"requirements": ["pyTibber==0.32.1"] "requirements": ["pyTibber==0.32.2"]
} }

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
import logging import logging
@@ -149,8 +150,9 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
raise DeviceNotFound("Unable to connect to device") from exc raise DeviceNotFound("Unable to connect to device") from exc
try: try:
packet_a0 = await client.read(PacketA0Notify) async with asyncio.timeout(10):
except (BleakError, DecodeError) as exc: packet_a0 = await client.read(PacketA0Notify)
except (BleakError, DecodeError, TimeoutError) as exc:
await client.disconnect() await client.disconnect()
raise DeviceFailed(f"Device failed {exc}") from exc raise DeviceFailed(f"Device failed {exc}") from exc
@@ -215,9 +217,19 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
@callback @callback
def _async_request_refresh_soon(self) -> None: def _async_request_refresh_soon(self) -> None:
self.config_entry.async_create_task( """Request a refresh in the near future.
self.hass, self.async_request_refresh(), eager_start=False
) This way have been called during an update and
would be ignored by debounce logic, so we delay
it by a slight amount to hopefully let the current
update finish first.
"""
async def _delayed_refresh() -> None:
await asyncio.sleep(0.5)
await self.async_request_refresh()
self.config_entry.async_create_task(self.hass, _delayed_refresh())
@callback @callback
def _disconnected_callback(self) -> None: def _disconnected_callback(self) -> None:

View File

@@ -300,9 +300,10 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
self._current_state is not None self._current_state is not None
and (current_state := self.device.status.get(self._current_state)) and (current_state := self.device.status.get(self._current_state))
is not None is not None
and current_state != "stop"
): ):
return self.entity_description.current_state_inverse is not ( return self.entity_description.current_state_inverse is not (
current_state in (True, "fully_close") current_state in (True, "close", "fully_close")
) )
return None return None

View File

@@ -100,8 +100,9 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
"""Return the currently set speed.""" """Return the currently set speed."""
current_level = self.device.state.fan_level current_level = self.device.state.fan_level
if self.device.state.mode == VS_FAN_MODE_MANUAL and current_level is not None: if self.device.state.mode == VS_FAN_MODE_MANUAL and current_level is not None:
if current_level == 0:
return 0
return ordered_list_item_to_percentage( return ordered_list_item_to_percentage(
self.device.fan_levels, current_level self.device.fan_levels, current_level
) )
@@ -211,17 +212,17 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
await self.device.turn_on() await self.device.turn_on()
if preset_mode == VS_FAN_MODE_AUTO: if preset_mode == VS_FAN_MODE_AUTO:
success = await self.device.auto_mode() success = await self.device.set_auto_mode()
elif preset_mode == VS_FAN_MODE_SLEEP: elif preset_mode == VS_FAN_MODE_SLEEP:
success = await self.device.sleep_mode() success = await self.device.set_sleep_mode()
elif preset_mode == VS_FAN_MODE_ADVANCED_SLEEP: elif preset_mode == VS_FAN_MODE_ADVANCED_SLEEP:
success = await self.device.advanced_sleep_mode() success = await self.device.set_advanced_sleep_mode()
elif preset_mode == VS_FAN_MODE_PET: elif preset_mode == VS_FAN_MODE_PET:
success = await self.device.pet_mode() success = await self.device.set_pet_mode()
elif preset_mode == VS_FAN_MODE_TURBO: elif preset_mode == VS_FAN_MODE_TURBO:
success = await self.device.turbo_mode() success = await self.device.set_turbo_mode()
elif preset_mode == VS_FAN_MODE_NORMAL: elif preset_mode == VS_FAN_MODE_NORMAL:
success = await self.device.normal_mode() success = await self.device.set_normal_mode()
if not success: if not success:
raise HomeAssistantError(self.device.last_response.message) raise HomeAssistantError(self.device.last_response.message)

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/vesync", "documentation": "https://www.home-assistant.io/integrations/vesync",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyvesync"], "loggers": ["pyvesync"],
"requirements": ["pyvesync==3.0.0"] "requirements": ["pyvesync==3.1.0"]
} }

View File

@@ -34,12 +34,13 @@ CONF_HEATING_TYPE = "heating_type"
DEFAULT_CACHE_DURATION = 60 DEFAULT_CACHE_DURATION = 60
VICARE_BAR = "bar"
VICARE_CUBIC_METER = "cubicMeter"
VICARE_KW = "kilowatt"
VICARE_KWH = "kilowattHour"
VICARE_PERCENT = "percent" VICARE_PERCENT = "percent"
VICARE_W = "watt" VICARE_W = "watt"
VICARE_KW = "kilowatt"
VICARE_WH = "wattHour" VICARE_WH = "wattHour"
VICARE_KWH = "kilowattHour"
VICARE_CUBIC_METER = "cubicMeter"
class HeatingType(enum.Enum): class HeatingType(enum.Enum):

View File

@@ -41,6 +41,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
VICARE_BAR,
VICARE_CUBIC_METER, VICARE_CUBIC_METER,
VICARE_KW, VICARE_KW,
VICARE_KWH, VICARE_KWH,
@@ -62,20 +63,22 @@ from .utils import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VICARE_UNIT_TO_DEVICE_CLASS = { VICARE_UNIT_TO_DEVICE_CLASS = {
VICARE_WH: SensorDeviceClass.ENERGY, VICARE_BAR: SensorDeviceClass.PRESSURE,
VICARE_KWH: SensorDeviceClass.ENERGY,
VICARE_W: SensorDeviceClass.POWER,
VICARE_KW: SensorDeviceClass.POWER,
VICARE_CUBIC_METER: SensorDeviceClass.GAS, VICARE_CUBIC_METER: SensorDeviceClass.GAS,
VICARE_KW: SensorDeviceClass.POWER,
VICARE_KWH: SensorDeviceClass.ENERGY,
VICARE_WH: SensorDeviceClass.ENERGY,
VICARE_W: SensorDeviceClass.POWER,
} }
VICARE_UNIT_TO_HA_UNIT = { VICARE_UNIT_TO_HA_UNIT = {
VICARE_BAR: UnitOfPressure.BAR,
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
VICARE_KW: UnitOfPower.KILO_WATT,
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
VICARE_PERCENT: PERCENTAGE, VICARE_PERCENT: PERCENTAGE,
VICARE_W: UnitOfPower.WATT, VICARE_W: UnitOfPower.WATT,
VICARE_KW: UnitOfPower.KILO_WATT,
VICARE_WH: UnitOfEnergy.WATT_HOUR, VICARE_WH: UnitOfEnergy.WATT_HOUR,
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
} }

View File

@@ -13,7 +13,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
SelectOptionDict, SelectOptionDict,
SelectSelector, SelectSelector,
@@ -69,7 +69,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
""" """
client = VictronVRMClient( client = VictronVRMClient(
token=api_token, token=api_token,
client_session=get_async_client(self.hass), client_session=async_get_clientsession(self.hass),
) )
try: try:
sites = await client.users.list_sites() sites = await client.users.list_sites()
@@ -86,7 +86,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
"""Validate access to the selected site and return its data.""" """Validate access to the selected site and return its data."""
client = VictronVRMClient( client = VictronVRMClient(
token=api_token, token=api_token,
client_session=get_async_client(self.hass), client_session=async_get_clientsession(self.hass),
) )
try: try:
site_data = await client.users.get_site(site_id) site_data = await client.users.get_site(site_id)

View File

@@ -11,7 +11,7 @@ from victron_vrm.utils import dt_now
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, LOGGER from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, LOGGER
@@ -26,8 +26,8 @@ class VRMForecastStore:
"""Class to hold the forecast data.""" """Class to hold the forecast data."""
site_id: int site_id: int
solar: ForecastAggregations solar: ForecastAggregations | None
consumption: ForecastAggregations consumption: ForecastAggregations | None
async def get_forecast(client: VictronVRMClient, site_id: int) -> VRMForecastStore: async def get_forecast(client: VictronVRMClient, site_id: int) -> VRMForecastStore:
@@ -75,7 +75,7 @@ class VictronRemoteMonitoringDataUpdateCoordinator(
"""Initialize.""" """Initialize."""
self.client = VictronVRMClient( self.client = VictronVRMClient(
token=config_entry.data[CONF_API_TOKEN], token=config_entry.data[CONF_API_TOKEN],
client_session=get_async_client(hass), client_session=async_get_clientsession(hass),
) )
self.site_id = config_entry.data[CONF_SITE_ID] self.site_id = config_entry.data[CONF_SITE_ID]
super().__init__( super().__init__(

View File

@@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["victron-vrm==0.1.7"] "requirements": ["victron-vrm==0.1.8"]
} }

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