Compare commits

...

134 Commits

Author SHA1 Message Date
Bram Kragten
62ec64c3fe 2025.12.4 (#159460) 2025-12-19 18:54:49 +01:00
Bram Kragten
cbc6306963 Merge branch 'master' into rc 2025-12-19 18:27:05 +01:00
Bram Kragten
e098acfa69 Bump version to 2025.12.4 2025-12-19 18:12:22 +01:00
Bram Kragten
52630ccca1 Update frontend to 20251203.3 (#159451) 2025-12-19 18:10:28 +01:00
Robert Resch
3001dcb8ff Remove users refresh tokens when the user get's deactivated (#159443) 2025-12-19 18:10:27 +01:00
Allen Porter
cec5134369 Bump python-roborock to 3.19.0 (#159404) 2025-12-19 18:10:26 +01:00
puddly
80f2889e1f Bump ZHA to 0.0.81 (#159396) 2025-12-19 18:10:25 +01:00
Simone Chemelli
188c98fd08 Align format of voltmeter strings for Shelly (#159394) 2025-12-19 18:10:25 +01:00
Artur Pragacz
e086e013d5 Do not trigger reauth for addon in Music Assistant (#159372) 2025-12-19 18:10:24 +01:00
Simone Chemelli
3c20df961e Add missing strings for Shelly voltmeter sensor (#159332) 2025-12-19 18:10:23 +01:00
Allen Porter
9f31d95940 Fix AttributeError in Roborock Empty Mode entity (#159278)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 18:10:22 +01:00
Andre Lengwenus
d5cbc6efca Bump pypck to 0.9.8 (#159277) 2025-12-19 18:10:21 +01:00
Luke Lashley
793877bfeb Bump python-roborock to 3.18.0 (#159271) 2025-12-19 18:10:21 +01:00
Andre Lengwenus
692847d9a8 Fix incorrect status updates for lcn (#159251) 2025-12-19 18:10:19 +01:00
Richard Polzer
31785bf68f Bump ekey-bionyxpy to version 1.0.1 (#159196) 2025-12-19 18:10:18 +01:00
Åke Strandberg
d17ed3ed95 Handle missing Miele status codes gracefully (#159124) 2025-12-19 18:10:17 +01:00
Pete Sage
7bbeb2a006 Bump soco to 0.30.13 for Sonos (#159123) 2025-12-19 18:10:16 +01:00
Jordan Harvey
7275be4629 Bump pynintendoparental 2.1.3 (#159120)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-19 18:10:16 +01:00
Pete Sage
37a32bf27d Sonos increase wait for groups timeout (#159108) 2025-12-19 18:10:14 +01:00
Pete Sage
00b7138c43 Sonos fix media player join to avoid race condition (#159106) 2025-12-19 18:10:13 +01:00
PaulCavill
1b464e799b Improve icloud reauth flow (#159081) 2025-12-19 18:10:12 +01:00
TimL
1a56855158 Bump pysmlight to v0.2.13 (#159075)
Co-authored-by: Tim Lunn <tim@feathertop.org>
2025-12-19 18:10:11 +01:00
Bram Kragten
0dac52cbe4 Bump aiodns to 3.6.1 (#159073) 2025-12-19 18:09:13 +01:00
Allen Porter
63cb220a8f Fix slow event state updates for remote calendar (#159058) 2025-12-19 18:02:13 +01:00
Kevin Fronczak
af72bc4d2a Bump blinkpy to 0.25.2 (#159049) 2025-12-19 18:02:12 +01:00
Xidorn Quan
108d94ab06 Bump aioasuswrt to 1.5.4 (#159038) 2025-12-19 18:02:11 +01:00
Allen Porter
d64313cd28 Add exception handling for rate limited or unauthorized MQTT requests (#158997) 2025-12-19 18:02:10 +01:00
Petro31
b608dcb2eb Update unnecessary error logging of unknown and unavailable source states from mold indicator (#158979) 2025-12-19 18:02:10 +01:00
Allen Porter
e0fa5db218 Bump ical to 12.1.2 (#158965) 2025-12-19 18:02:09 +01:00
Jan Bouwhuis
96d2ecf250 Assume cover or valve is always "running" in google assistant when the state is assumed or the position is reported to allow it to be be stopped (#158919) 2025-12-19 18:02:08 +01:00
Aidan Timson
b0fac94666 Update systembridgeconnector to 5.2.4, fix media source (#158917) 2025-12-19 18:02:07 +01:00
Andrew Jackson
8902ba9f1d Bump aiomealie to 1.1.1 and statically define mealplan entry types (#158907) 2025-12-19 18:02:06 +01:00
Bouwe Westerdijk
581919ccb4 Revert adding entity_category to Plugwise thermostat schedule select (#158901) 2025-12-19 18:02:05 +01:00
Magnus
7714b51c21 Bump aioasuswrt 1.5.3 (#158882) 2025-12-19 18:02:04 +01:00
Jordan Harvey
8ee94f829a Bump pynintendoparental to 2.1.1 (#158779) 2025-12-19 18:02:03 +01:00
Paul Tarjan
73734d2ff2 Fix Sonos speaker async_offline assertion failure (#158764) 2025-12-19 18:02:02 +01:00
Paul Tarjan
b7d4c3c5d1 Suppress verbose UPnP subscription error logs (#158677) 2025-12-19 18:02:01 +01:00
Allen Porter
5d30fc3436 Suppress roborock failures under some unavailability threshold (#158673)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 18:02:00 +01:00
Jordan Harvey
4cced81f86 Update pynintendoparental to 2.1.0 (#158487) 2025-12-19 18:01:58 +01:00
Thomas D
81d10d02de Enable volvo engine status for all engine types (#158437) 2025-12-19 18:01:57 +01:00
Jordan Harvey
73484cb8fb Update pynintendoparental to 2.0.0 (#158285) 2025-12-19 18:01:56 +01:00
starkillerOG
d0aaac0382 Do not check Reolink firmware at start (#158275) 2025-12-19 18:01:55 +01:00
Federico Imberti
67550731b3 Prevent empty aliases in registries (#156061)
Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-19 18:01:54 +01:00
Franck Nijhof
04746b6843 2025.12.3 (#158811) 2025-12-12 19:10:33 +01:00
Magnus
0547153730 Bump aioasuswrt to 1.5.2 (#158727) 2025-12-12 17:37:17 +00:00
Franck Nijhof
eb024b4dde Bump version to 2025.12.3 2025-12-12 17:23:29 +00:00
Joost Lekkerkerker
1d4817608e Bump pySmartThings to 3.5.1 (#158795) 2025-12-12 17:23:16 +00:00
Manu
a37ca293e1 Increase Xbox update interval to 15 seconds and refactor title data handling (#158780) 2025-12-12 17:23:15 +00:00
Josef Zweck
f3dbddee16 Bump pylamarzocco to 2.2.4 (#158774) 2025-12-12 17:20:51 +00:00
Josef Zweck
b26681ee88 Bump pylamarzocco to 2.2.3 (#158104) 2025-12-12 17:20:49 +00:00
Allen Porter
effe72bfda Bump ical to 12.1.1 (#158770) 2025-12-12 17:19:13 +00:00
cdutr
076835ca1c Migrate Blink component to use hardware_id instead of device_id (#158765) 2025-12-12 17:19:12 +00:00
Thomas55555
4b9b1e611a Bump google air quality api to 2.0.2 (#158742) 2025-12-12 17:19:11 +00:00
ndrwrbgs
0b4ea42810 Update advanced_options display text for MQTT (#158728) 2025-12-12 17:19:09 +00:00
johanzander
8907608345 Add state_class to Growatt power and energy sensors (#158705)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 17:19:08 +00:00
J. Nick Koston
356ee07e22 Pin pycares to 4.11.0 (#158695) 2025-12-12 17:19:07 +00:00
Allen Porter
bee3ee6320 Bump python-roborock to 3.12.2 (#158572) 2025-12-12 17:19:05 +00:00
Andrew Jackson
fb72ff9bd0 Add measurement state class to ohme sensors (#158541) 2025-12-12 17:19:04 +00:00
bestycame
412e05d8da Bump hanna-cloud to version 0.0.7 (#158536)
Co-authored-by: Olivier d'Otreppe <odotreppe@abbove.com>
2025-12-12 17:19:03 +00:00
Yevhenii Vaskivskyi
58ee8e863e Bump asusrouter to 1.21.3 (#158492) 2025-12-12 17:19:01 +00:00
Ludovic BOUÉ
e3a47bfc51 Fix Matter Door Lock Operating Mode select entity (#158468) 2025-12-12 17:19:00 +00:00
Allen Porter
a6cdacc8fe Improve Roborock exception logging behavior for Zeo/Dyad devices (#158465)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 17:18:58 +00:00
epenet
dd0425ab8e Add Tuya local_strategy to Tuya diagnostic (#158450) 2025-12-12 17:18:57 +00:00
Samuel Xiao
1d289c0083 Switchbot Cloud: Fixed binary sensors didn't update automatically (#158434)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 17:18:56 +00:00
Allen Porter
70786a1d90 Fix roborock off peak electricity timer (#158292) 2025-12-12 17:18:54 +00:00
Michel D'Astous
293eb69788 Fix webhook exception when empty json data is sent (#158254) 2025-12-12 17:18:53 +00:00
Kira
71d92291d1 Bump blinkpy to 0.25.1 (#158135)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 17:18:52 +00:00
Andre Lengwenus
726de64394 Bump pypck to 0.9.7 (#158089) 2025-12-12 17:18:50 +00:00
epenet
de04f22f89 Improve Tuya HVACMode handling (#158042)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 17:18:49 +00:00
Jan Bouwhuis
9e8cc3a65b Move translatable URL out of strings.json for knx integration (#155244) 2025-12-12 17:04:30 +00:00
Franck Nijhof
27fa92b607 Fix Tuya BitmapTypeInformation parsing (#158475) 2025-12-10 17:06:50 +01:00
epenet
ce5c5c5eb7 Fix Tuya BitmapTypeInformation parsing 2025-12-09 16:29:25 +00:00
Franck Nijhof
88e29df8eb 2025.12.2 (#158274) 2025-12-08 22:35:39 +01:00
Franck Nijhof
a2b5744696 Bump version to 2025.12.2 2025-12-08 20:45:22 +00:00
Marcel van der Veldt
201c3785f5 Skip check for onboarding done in Music Assistant integration (#158270) 2025-12-08 20:17:05 +00:00
Paul Bottein
24de26cbf5 Update frontend to 20251203.2 (#158259) 2025-12-08 20:17:04 +00:00
andreimoraru
ac0a544829 Bump yt-dlp to 2025.12.08 (#158253) 2025-12-08 20:17:03 +00:00
Petro31
1a11b92f05 Fix multiple top-level support for template integration (#158244) 2025-12-08 20:17:01 +00:00
epenet
ab0811f59f Fix teslemetry service description placeholders (#158240) 2025-12-08 20:17:00 +00:00
epenet
68711b2f21 Fix yeelight service description placeholders (#158239) 2025-12-08 20:16:59 +00:00
epenet
886e2b0af1 Fix zwave_js service description placeholders (#158236) 2025-12-08 20:16:57 +00:00
Thomas55555
7492b5be75 Bump google air quality api to 2.0.0 (#158234) 2025-12-08 20:16:56 +00:00
Jan Bouwhuis
e4f1565e3c Fix description placeholders for system_bridge (#158232) 2025-12-08 20:16:54 +00:00
Paul Bottein
7f37412199 Be more specific about winter mode in the description (#158230)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-08 20:16:53 +00:00
Allen Porter
eaef0160a2 Bump python-roborock to 3.10.10 (#158212) 2025-12-08 20:16:52 +00:00
Harvey
f049c425ba Bump HueBLE to 2.1.0 (#158197) 2025-12-08 20:16:50 +00:00
Yevhenii Vaskivskyi
50eee75b8f Bump asusrouter to 1.21.1 (#158192) 2025-12-08 20:16:48 +00:00
Åke Strandberg
81e47f6844 Bump pymiele dependency to 0.6.1 (#158177) 2025-12-08 20:16:46 +00:00
Åke Strandberg
ffebbab020 Add program id codes for Miele WQ1000 (#158175) 2025-12-08 20:16:45 +00:00
Manu
9824bdc1c9 Fix secure URLs for promotional game media in Xbox integration (#158162) 2025-12-08 20:16:44 +00:00
Allen Porter
a933d4a0eb Ensure Roborock disconnects mqtt on unload/stop (#158144)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 20:16:42 +00:00
Shay Levy
f7f7f9a2de Revert "Remove Shelly redundant device entry check for sleepy devices" (#158108) 2025-12-08 20:16:41 +00:00
Petro31
aac412f3a8 Fix legacy template entity_id field in migration (#158105)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-08 20:16:39 +00:00
omrishiv
660a14e78d fix Lutron Caseta smart away subscription (#158082)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-08 20:16:38 +00:00
Franck Nijhof
4aa3f0a400 2025.12.1 (#158071) 2025-12-05 22:09:38 +01:00
Franck Nijhof
0b52c806d4 Bump version to 2025.12.1 2025-12-05 20:32:57 +00:00
Paul Bottein
bbe27d86a1 Update frontend to 20251203.1 (#158069) 2025-12-05 20:32:28 +00:00
Raphael Hehl
fb7941df1d Bump uiprotect to 7.33.2 (#158057) 2025-12-05 20:32:27 +00:00
Petro31
c46e341941 Fix inverted kelvin issue (#158054) 2025-12-05 20:32:25 +00:00
Jan Bouwhuis
2e3a9e3a90 Move example image path out of translatable strings (#158053) 2025-12-05 20:32:24 +00:00
Jan Bouwhuis
55c5ecd28a Move lametric URLs out of strings.json (#158051) 2025-12-05 20:32:22 +00:00
Denis Shulyaka
e50e2487e1 Replace deprecated preview image model (#158048) 2025-12-05 20:32:21 +00:00
Maciej Bieniek
74e118f85c Do not create restart button for sleeping gen2+ Shelly devices (#158047) 2025-12-05 20:32:19 +00:00
Joost Lekkerkerker
39a62ec2f6 Prevent entsoe from loading (#158036)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-05 20:32:18 +00:00
Petro31
1310efcb07 Fix missing template key in deprecation repair (#158033) 2025-12-05 20:32:16 +00:00
hanwg
53af592c2c Improve action descriptions for Telegram bot (#158022) 2025-12-05 20:32:15 +00:00
TheJulianJES
023987b805 Change ZHA strings for incorrect adapter state (#158021)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-05 20:32:13 +00:00
Allen Porter
5b8fb607b4 Bump python-roborock to 3.10.2 (#158020) 2025-12-05 20:32:12 +00:00
Mark Adkins
252f6716ff SharkIQ dep upgrade v1.5.0 (#158015) 2025-12-05 20:32:11 +00:00
Paul Tarjan
bf78e28f83 Fix doorbird duplicate unique ID generation (#158013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 20:32:09 +00:00
David Bonnes
22706d02a7 Bump evohome-async to 1.0.6 (#158005) 2025-12-05 20:32:08 +00:00
Abílio Costa
5cff0e946a Bump oralb-ble to 1.0.2 (#157992) 2025-12-05 20:32:06 +00:00
Luke Lashley
6cbe2ed279 Bump python-Roborock to 3.10.0 (#157980) 2025-12-05 20:32:04 +00:00
Paul Bottein
fb0f5f52b2 Add subscribe preview feature endpoint to labs (#157976) 2025-12-05 20:32:03 +00:00
Jan Bouwhuis
5c422bb770 Move out example URL and IP of strings.json for reolink (#157970) 2025-12-05 20:32:01 +00:00
Jan Bouwhuis
fd1bc07b8c Move pilight URL out of strings.json (#157967) 2025-12-05 20:31:59 +00:00
Petro31
97a019d313 Update template deprecation to be more explicit (#157965) 2025-12-05 20:31:58 +00:00
epenet
8ae8a564c2 Fix unit parsing in Tuya climate entities (#157964) 2025-12-05 20:31:56 +00:00
Jan Bouwhuis
2f72f57bb7 Move out zwave_js api docs url from strings.json (#157959) 2025-12-05 20:31:55 +00:00
Jan Bouwhuis
e928e3cb54 Move Yeelight URLs out of translatable strings for action descriptions (#157957) 2025-12-05 20:31:53 +00:00
Petro31
b0e2109e15 Fix template migration errors (#157949) 2025-12-05 20:31:51 +00:00
Jordan Harvey
b449c6673f Add pyanglianwater to Anglian Water loggers (#157947) 2025-12-05 20:31:50 +00:00
Manu
877ad38ac3 Convert image URLs to secure URLs in Xbox integration (#157945) 2025-12-05 20:31:48 +00:00
Jan Bouwhuis
229f45feae Move translatable URL from rainmachine push_weather_data action description (#157941)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-05 20:31:47 +00:00
Jordan Harvey
a535d1f4eb Set account number as required for Anglian Water config entry (#157939) 2025-12-05 20:31:46 +00:00
Jan Bouwhuis
d4adc00ae6 Move out URL of Xiaomy_aquara from strings.json (#157937)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 20:31:44 +00:00
starkillerOG
ba141f9d1d Bump reolink_aio to 0.17.1 (#157929) 2025-12-05 20:31:41 +00:00
cdnninja
72be9793a4 Fix VeSync binary sensor discovery (#157898) 2025-12-05 20:31:40 +00:00
Luke Lashley
5ae7cc5f84 Correctly pass MopParserConfig for Roborock (#157891) 2025-12-05 20:31:39 +00:00
Jan Bouwhuis
d01a469b46 Move teslemetry time-of-use URL out of strings.json (#157874) 2025-12-05 20:31:37 +00:00
TheJulianJES
9f07052874 Display error when forming new ZHA network fails (#157863) 2025-12-05 20:31:35 +00:00
David Rapan
b9bc9d3fc2 Fix Starlink's ever updating uptime (#155574)
Signed-off-by: David Rapan <david@rapan.cz>
2025-12-05 20:31:34 +00:00
Max Michels
1e180cd5ee Move telegram-bot URLs out of strings.json (#155130)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: jbouwh <jan@jbsoft.nl>
2025-12-05 20:31:32 +00:00
Quentin Ulmer
dc9cdd13b1 Fix Rituals Perfume Genie (#151537)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 20:31:30 +00:00
249 changed files with 5201 additions and 1360 deletions

4
CODEOWNERS generated
View File

@@ -1354,8 +1354,8 @@ build.json @home-assistant/supervisor
/tests/components/ring/ @sdb9696
/homeassistant/components/risco/ @OnFreund
/tests/components/risco/ @OnFreund
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @Lash-L @allenporter

View File

@@ -402,6 +402,8 @@ class AuthManager:
if user.is_owner:
raise ValueError("Unable to deactivate the owner")
await self._store.async_deactivate_user(user)
for refresh_token in list(user.refresh_tokens.values()):
self.async_remove_refresh_token(refresh_token)
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""

View File

@@ -30,6 +30,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
@@ -68,34 +69,19 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
account_number=user_input[CONF_ACCOUNT_NUMBER],
)
)
if isinstance(validation_response, BaseAuth):
account_number = (
user_input.get(CONF_ACCOUNT_NUMBER)
or validation_response.account_number
)
await self.async_set_unique_id(account_number)
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=account_number,
title=user_input[CONF_ACCOUNT_NUMBER],
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
},
)
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form(

View File

@@ -5,6 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==2.1.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
"requirements": ["aioasuswrt==1.5.4", "asusrouter==1.21.3"]
}

View File

@@ -64,6 +64,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b
if entry.version == 2:
await _reauth_flow_wrapper(hass, entry, data)
return False
if entry.version == 3:
# Migrate device_id to hardware_id for blinkpy 0.25.x OAuth2 compatibility
if "device_id" in data:
data["hardware_id"] = data.pop("device_id")
hass.config_entries.async_update_entry(entry, data=data, version=4)
return True
return True

View File

@@ -21,7 +21,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEVICE_ID, DOMAIN
from .const import DOMAIN, HARDWARE_ID
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ async def _send_blink_2fa_pin(blink: Blink, pin: str | None) -> bool:
class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow."""
VERSION = 3
VERSION = 4
def __init__(self) -> None:
"""Initialize the blink flow."""
@@ -53,7 +53,7 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
async def _handle_user_input(self, user_input: dict[str, Any]):
"""Handle user input."""
self.auth = Auth(
{**user_input, "device_id": DEVICE_ID},
{**user_input, "hardware_id": HARDWARE_ID},
no_prompt=True,
session=async_get_clientsession(self.hass),
)

View File

@@ -3,7 +3,7 @@
from homeassistant.const import Platform
DOMAIN = "blink"
DEVICE_ID = "Home Assistant"
HARDWARE_ID = "Home Assistant"
CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera"

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.24.1"]
"requirements": ["blinkpy==0.25.2"]
}

View File

@@ -65,8 +65,10 @@ def websocket_create_area(
data.pop("id")
if "aliases" in data:
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
if "labels" in data:
# Convert labels to a set
@@ -133,8 +135,10 @@ def websocket_update_area(
data.pop("id")
if "aliases" in data:
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
if "labels" in data:
# Convert labels to a set

View File

@@ -227,8 +227,10 @@ def websocket_update_entity(
changes[key] = msg[key]
if "aliases" in msg:
# Convert aliases to a set
changes["aliases"] = set(msg["aliases"])
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
changes["aliases"] = {s_strip for s in msg["aliases"] if (s_strip := s.strip())}
if "labels" in msg:
# Convert labels to a set

View File

@@ -61,8 +61,10 @@ def websocket_create_floor(
data.pop("id")
if "aliases" in data:
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
try:
entry = registry.async_create(**data)
@@ -117,8 +119,10 @@ def websocket_update_floor(
data.pop("id")
if "aliases" in data:
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
try:
entry = registry.async_update(**data)

View File

@@ -9,7 +9,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.46.0", "getmac==0.9.5"],
"requirements": ["async-upnp-client==0.46.1", "getmac==0.9.5"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["async-upnp-client==0.46.0"],
"requirements": ["async-upnp-client==0.46.1"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.5.0"]
"requirements": ["aiodns==3.6.1"]
}

View File

@@ -102,6 +102,12 @@ class ConfiguredDoorBird:
"""Get token for device."""
return self._token
def _get_hass_url(self) -> str:
"""Get the Home Assistant URL for this device."""
if custom_url := self.custom_url:
return custom_url
return get_url(self._hass, prefer_external=False)
async def async_register_events(self) -> None:
"""Register events on device."""
if not self.door_station_events:
@@ -146,13 +152,7 @@ class ConfiguredDoorBird:
async def _async_register_events(self) -> dict[str, Any]:
"""Register events on device."""
# Override url if another is specified in the configuration
if custom_url := self.custom_url:
hass_url = custom_url
else:
# Get the URL of this server
hass_url = get_url(self._hass, prefer_external=False)
hass_url = self._get_hass_url()
http_fav = await self._async_get_http_favorites()
if any(
# Note that a list comp is used here to ensure all
@@ -191,10 +191,14 @@ class ConfiguredDoorBird:
self._get_event_name(event): event_type
for event, event_type in DEFAULT_EVENT_TYPES
}
hass_url = self._get_hass_url()
for identifier, data in http_fav.items():
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
value: str | None = data.get("value")
if not value or not value.startswith(hass_url):
continue # Not our favorite - different HA instance or stale
event = title.partition("(")[2].strip(")")
if input_type := favorite_input_type.get(identifier):
events.append(DoorbirdEvent(event, input_type))

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/ekeybionyx",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["ekey-bionyxpy==1.0.0"]
"requirements": ["ekey-bionyxpy==1.0.1"]
}

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.5"]
"requirements": ["evohome-async==1.0.6"]
}

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.0"]
"requirements": ["home-assistant-frontend==20251203.3"]
}

View File

@@ -1,9 +1,9 @@
{
"preview_features": {
"winter_mode": {
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️",
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in labs settings.",
"enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in labs settings.",
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️\n\nIf you have animations disabled in your device accessibility settings, this feature will not work.",
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in Labs settings.",
"enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in Labs settings.",
"name": "Winter mode"
}
},

View File

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

View File

@@ -51,9 +51,9 @@ async def _validate_input(
description_placeholders: dict[str, str],
) -> bool:
try:
await api.async_air_quality(
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
long=user_input[CONF_LOCATION][CONF_LONGITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
except GoogleAirQualityApiError as err:
errors["base"] = "cannot_connect"

View File

@@ -7,7 +7,7 @@ from typing import Final
from google_air_quality_api.api import GoogleAirQualityApi
from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.model import AirQualityData
from google_air_quality_api.model import AirQualityCurrentConditionsData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
@@ -23,7 +23,9 @@ UPDATE_INTERVAL: Final = timedelta(hours=1)
type GoogleAirQualityConfigEntry = ConfigEntry[GoogleAirQualityRuntimeData]
class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]):
class GoogleAirQualityUpdateCoordinator(
DataUpdateCoordinator[AirQualityCurrentConditionsData]
):
"""Coordinator for fetching Google AirQuality data."""
config_entry: GoogleAirQualityConfigEntry
@@ -48,10 +50,10 @@ class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]):
self.lat = subentry.data[CONF_LATITUDE]
self.long = subentry.data[CONF_LONGITUDE]
async def _async_update_data(self) -> AirQualityData:
async def _async_update_data(self) -> AirQualityCurrentConditionsData:
"""Fetch air quality data for this coordinate."""
try:
return await self.client.async_air_quality(self.lat, self.long)
return await self.client.async_get_current_conditions(self.lat, self.long)
except GoogleAirQualityApiError as ex:
_LOGGER.debug("Cannot fetch air quality data: %s", str(ex))
raise UpdateFailed(

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==1.1.3"]
"requirements": ["google_air_quality_api==2.0.2"]
}

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass
import logging
from google_air_quality_api.model import AirQualityData
from google_air_quality_api.model import AirQualityCurrentConditionsData
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -33,15 +33,17 @@ PARALLEL_UPDATES = 0
class AirQualitySensorEntityDescription(SensorEntityDescription):
"""Describes Air Quality sensor entity."""
exists_fn: Callable[[AirQualityData], bool] = lambda _: True
options_fn: Callable[[AirQualityData], list[str] | None] = lambda _: None
value_fn: Callable[[AirQualityData], StateType]
native_unit_of_measurement_fn: Callable[[AirQualityData], str | None] = (
exists_fn: Callable[[AirQualityCurrentConditionsData], bool] = lambda _: True
options_fn: Callable[[AirQualityCurrentConditionsData], list[str] | None] = (
lambda _: None
)
translation_placeholders_fn: Callable[[AirQualityData], dict[str, str]] | None = (
None
)
value_fn: Callable[[AirQualityCurrentConditionsData], StateType]
native_unit_of_measurement_fn: Callable[
[AirQualityCurrentConditionsData], str | None
] = lambda _: None
translation_placeholders_fn: (
Callable[[AirQualityCurrentConditionsData], dict[str, str]] | None
) = None
AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (

View File

@@ -88,16 +88,16 @@
"1b_good_air_quality": "1B - Good air quality",
"2_cyan": "2 - Cyan",
"2_light_green": "2 - Light green",
"2_orange": "4 - Orange",
"2_red": "5 - Red",
"2_yellow": "3 - Yellow",
"2a_acceptable_air_quality": "2A - Acceptable air quality",
"2b_acceptable_air_quality": "2B - Acceptable air quality",
"3_green": "3 - Green",
"3_yellow": "3 - Yellow",
"3a_aggravated_air_quality": "3A - Aggravated air quality",
"3b_bad_air_quality": "3B - Bad air quality",
"4_orange": "4 - Orange",
"4_yellow_watch": "4 - Yellow/Watch",
"5_orange_alert": "5 - Orange/Alert",
"5_red": "5 - Red",
"6_red_alert": "6 - Red/Alert+",
"10_33": "10-33% of guideline",
"33_66": "33-66% of guideline",

View File

@@ -908,12 +908,21 @@ class StartStopTrait(_Trait):
}
if domain in COVER_VALVE_DOMAINS:
assumed_state_or_set_position = bool(
(
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& COVER_VALVE_SET_POSITION_FEATURE[domain]
)
or self.state.attributes.get(ATTR_ASSUMED_STATE)
)
return {
"isRunning": state
in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
or assumed_state_or_set_position
}
raise NotImplementedError(f"Unsupported domain {domain}")
@@ -975,11 +984,23 @@ class StartStopTrait(_Trait):
"""Execute a StartStop command."""
domain = self.state.domain
if command == COMMAND_START_STOP:
assumed_state_or_set_position = bool(
(
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& COVER_VALVE_SET_POSITION_FEATURE[domain]
)
or self.state.attributes.get(ATTR_ASSUMED_STATE)
)
if params["start"] is False:
if self.state.state in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
) or self.state.attributes.get(ATTR_ASSUMED_STATE):
if (
self.state.state
in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
or assumed_state_or_set_position
):
await self.hass.services.async_call(
domain,
SERVICE_STOP_COVER_VALVE[domain],
@@ -992,7 +1013,14 @@ class StartStopTrait(_Trait):
ERR_ALREADY_STOPPED,
f"{FRIENDLY_DOMAIN[domain]} is already stopped",
)
else:
elif (
self.state.state
in (
COVER_VALVE_STATES[domain]["open"],
COVER_VALVE_STATES[domain]["closed"],
)
or assumed_state_or_set_position
):
await self.hass.services.async_call(
domain,
SERVICE_TOGGLE_COVER_VALVE[domain],

View File

@@ -149,6 +149,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}
),
supports_response=SupportsResponse.ONLY,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)
return True

View File

@@ -23,7 +23,7 @@ CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image-preview"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_TOP_P = "top_p"

View File

@@ -162,7 +162,7 @@
"fields": {
"filenames": {
"description": "Attachments to add to the prompt (images, PDFs, etc)",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Attachment filenames"
},
"prompt": {

View File

@@ -159,4 +159,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -92,7 +92,7 @@
},
"filename": {
"description": "Path to the image or video to upload.",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Filename"
}
},

View File

@@ -27,6 +27,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eBatChargeToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_battery_charge_lifetime",
@@ -42,6 +43,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eBatDisChargeToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_battery_discharge_lifetime",
@@ -57,6 +59,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="epvToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_solar_generation_lifetime",
@@ -72,6 +75,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pDischarge1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_battery_voltage",
@@ -101,6 +105,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="elocalLoadToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_load_consumption_lifetime",
@@ -116,6 +121,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="etoGridToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_export_to_grid_lifetime",
@@ -132,6 +138,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="chargePower",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_load_consumption",
@@ -139,6 +146,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pLocalLoad",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_wattage_pv_1",
@@ -146,6 +154,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pPv1",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_wattage_pv_2",
@@ -153,6 +162,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pPv2",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_wattage_pv_all",
@@ -160,6 +170,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_export_to_grid",
@@ -167,6 +178,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pactogrid",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_import_from_grid",
@@ -174,6 +186,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pactouser",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_battery_discharge_kw",
@@ -181,6 +194,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pdisCharge1",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_grid_voltage",
@@ -196,6 +210,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eCharge",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_load_consumption_solar_today",
@@ -203,6 +218,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eChargeToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_self_consumption_today",
@@ -210,6 +226,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eChargeToday1",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_load_consumption_battery_today",
@@ -217,6 +234,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="echarge1",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_import_from_grid_today",
@@ -224,6 +242,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="etouser",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# This sensor is manually created using the most recent X-Axis value from the chartData
GrowattSensorEntityDescription(

View File

@@ -79,6 +79,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -122,6 +123,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -165,6 +167,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv3",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -208,6 +211,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv4",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -234,6 +238,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -258,6 +263,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pac",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -323,6 +329,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc1DischargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_1_discharge_total",
@@ -339,6 +346,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc2DischargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_2_discharge_total",
@@ -372,6 +380,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc1ChargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_1_charge_total",
@@ -388,6 +397,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc2ChargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_2_charge_total",
@@ -445,6 +455,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToLocalLoad",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -453,6 +464,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToUserTotal",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -461,6 +473,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToGridTotal",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -545,6 +558,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="psystem",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -553,6 +567,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pself",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
)

View File

@@ -50,5 +50,6 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="nominalPower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hanna",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["hanna-cloud==0.0.6"]
"requirements": ["hanna-cloud==0.0.7"]
}

View File

@@ -2,7 +2,7 @@
import logging
from HueBLE import HueBleLight
from HueBLE import ConnectionError, HueBleError, HueBleLight
from homeassistant.components.bluetooth import (
async_ble_device_from_address,
@@ -38,8 +38,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bo
light = HueBleLight(ble_device)
if not await light.connect() or not await light.poll_state():
raise ConfigEntryNotReady("Device found but unable to connect.")
try:
await light.connect()
await light.poll_state()
except ConnectionError as e:
raise ConfigEntryNotReady("Device found but unable to connect.") from e
except HueBleError as e:
raise ConfigEntryNotReady(
"Device found and connected but unable to poll values from it."
) from e
entry.runtime_data = light

View File

@@ -6,7 +6,7 @@ from enum import Enum
import logging
from typing import Any
from HueBLE import HueBleLight
from HueBLE import ConnectionError, HueBleError, HueBleLight, PairingError
import voluptuous as vol
from homeassistant.components import bluetooth
@@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN, URL_PAIRING_MODE
from .const import DOMAIN, URL_FACTORY_RESET, URL_PAIRING_MODE
from .light import get_available_color_modes
_LOGGER = logging.getLogger(__name__)
@@ -41,32 +41,22 @@ async def validate_input(hass: HomeAssistant, address: str) -> Error | None:
try:
light = HueBleLight(ble_device)
await light.connect()
get_available_color_modes(light)
await light.poll_state()
if light.authenticated is None:
_LOGGER.warning(
"Unable to determine if light authenticated, proceeding anyway"
)
elif not light.authenticated:
return Error.INVALID_AUTH
if not light.connected:
return Error.CANNOT_CONNECT
try:
get_available_color_modes(light)
except HomeAssistantError:
return Error.NOT_SUPPORTED
_, errors = await light.poll_state()
if len(errors) != 0:
_LOGGER.warning("Errors raised when connecting to light: %s", errors)
return Error.CANNOT_CONNECT
except Exception:
except ConnectionError as e:
_LOGGER.exception("Error connecting to light")
return (
Error.INVALID_AUTH
if type(e.__cause__) is PairingError
else Error.CANNOT_CONNECT
)
except HueBleError:
_LOGGER.exception("Unexpected error validating light connection")
return Error.UNKNOWN
except HomeAssistantError:
return Error.NOT_SUPPORTED
else:
return None
finally:
@@ -129,6 +119,7 @@ class HueBleConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_NAME: self._discovery_info.name,
CONF_MAC: self._discovery_info.address,
"url_pairing_mode": URL_PAIRING_MODE,
"url_factory_reset": URL_FACTORY_RESET,
},
)

View File

@@ -2,3 +2,4 @@
DOMAIN = "hue_ble"
URL_PAIRING_MODE = "https://www.home-assistant.io/integrations/hue_ble#initial-setup"
URL_FACTORY_RESET = "https://www.philips-hue.com/en-gb/support/article/how-to-factory-reset-philips-hue-lights/000004"

View File

@@ -113,7 +113,7 @@ class HueBLELight(LightEntity):
async def async_update(self) -> None:
"""Fetch latest state from light and make available via properties."""
await self._api.poll_state(run_callbacks=True)
await self._api.poll_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Set properties then turn the light on."""

View File

@@ -15,5 +15,5 @@
"iot_class": "local_push",
"loggers": ["bleak", "HueBLE"],
"quality_scale": "bronze",
"requirements": ["HueBLE==1.0.8"]
"requirements": ["HueBLE==2.1.0"]
}

View File

@@ -14,7 +14,7 @@
},
"step": {
"confirm": {
"description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode})."
"description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode}) or has been [factory reset]({url_factory_reset})."
}
}
}

View File

@@ -16,7 +16,7 @@ from pyicloud.exceptions import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.storage import Store
@@ -155,8 +155,8 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold,
}
# If this is a password update attempt, update the entry instead of creating one
if step_id == "user":
# If this is a password update attempt, don't try and creating one
if self.source == SOURCE_USER:
return self.async_create_entry(title=self._username, data=data)
entry = await self.async_set_unique_id(self.unique_id)

View File

@@ -39,6 +39,10 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
_DESCRIPTION_PLACEHOLDERS = {
"sensor_value_types_url": "https://www.home-assistant.io/integrations/knx/#value-types"
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
@@ -48,6 +52,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_KNX_SEND,
service_send_to_knx_bus,
schema=SERVICE_KNX_SEND_SCHEMA,
description_placeholders=_DESCRIPTION_PLACEHOLDERS,
)
hass.services.async_register(
@@ -63,6 +68,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_KNX_EVENT_REGISTER,
service_event_register_modify,
schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA,
description_placeholders=_DESCRIPTION_PLACEHOLDERS,
)
async_register_admin_service(
@@ -71,6 +77,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_KNX_EXPOSURE_REGISTER,
service_exposure_register_modify,
schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
description_placeholders=_DESCRIPTION_PLACEHOLDERS,
)
async_register_admin_service(

View File

@@ -674,7 +674,7 @@
"name": "Remove event registration"
},
"type": {
"description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types).",
"description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see {sensor_value_types_url}).",
"name": "Value type"
}
},
@@ -704,7 +704,7 @@
"name": "Remove exposure"
},
"type": {
"description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types).",
"description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see {sensor_value_types_url}).",
"name": "Value type"
}
},
@@ -740,7 +740,7 @@
"name": "Send as Response"
},
"type": {
"description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types).",
"description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see {sensor_value_types_url}).",
"name": "Value type"
}
},

View File

@@ -7,11 +7,10 @@ in the Home Assistant Labs UI for users to enable or disable.
from __future__ import annotations
from collections.abc import Callable
import logging
from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.generated.labs import LABS_PREVIEW_FEATURES
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.storage import Store
@@ -19,6 +18,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_custom_components
from .const import DOMAIN, LABS_DATA, STORAGE_KEY, STORAGE_VERSION
from .helpers import async_is_preview_feature_enabled, async_listen
from .models import (
EventLabsUpdatedData,
LabPreviewFeature,
@@ -135,55 +135,3 @@ async def _async_scan_all_preview_features(
_LOGGER.debug("Loaded %d total lab preview features", len(preview_features))
return preview_features
@callback
def async_is_preview_feature_enabled(
hass: HomeAssistant, domain: str, preview_feature: str
) -> bool:
"""Check if a lab preview feature is enabled.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
Returns:
True if the preview feature is enabled, False otherwise
"""
if LABS_DATA not in hass.data:
return False
labs_data = hass.data[LABS_DATA]
return (domain, preview_feature) in labs_data.data.preview_feature_status
@callback
def async_listen(
hass: HomeAssistant,
domain: str,
preview_feature: str,
listener: Callable[[], None],
) -> Callable[[], None]:
"""Listen for changes to a specific preview feature.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
listener: Callback to invoke when the preview feature is toggled
Returns:
Callable to unsubscribe from the listener
"""
@callback
def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None:
"""Handle labs feature update event."""
if (
event.data["domain"] == domain
and event.data["preview_feature"] == preview_feature
):
listener()
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)

View File

@@ -0,0 +1,63 @@
"""Helper functions for the Home Assistant Labs integration."""
from __future__ import annotations
from collections.abc import Callable
from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import Event, HomeAssistant, callback
from .const import LABS_DATA
from .models import EventLabsUpdatedData
@callback
def async_is_preview_feature_enabled(
hass: HomeAssistant, domain: str, preview_feature: str
) -> bool:
"""Check if a lab preview feature is enabled.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
Returns:
True if the preview feature is enabled, False otherwise
"""
if LABS_DATA not in hass.data:
return False
labs_data = hass.data[LABS_DATA]
return (domain, preview_feature) in labs_data.data.preview_feature_status
@callback
def async_listen(
hass: HomeAssistant,
domain: str,
preview_feature: str,
listener: Callable[[], None],
) -> Callable[[], None]:
"""Listen for changes to a specific preview feature.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
listener: Callback to invoke when the preview feature is toggled
Returns:
Callable to unsubscribe from the listener
"""
@callback
def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None:
"""Handle labs feature update event."""
if (
event.data["domain"] == domain
and event.data["preview_feature"] == preview_feature
):
listener()
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)

View File

@@ -12,6 +12,7 @@ from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import HomeAssistant, callback
from .const import LABS_DATA
from .helpers import async_is_preview_feature_enabled, async_listen
from .models import EventLabsUpdatedData
@@ -20,6 +21,7 @@ def async_setup(hass: HomeAssistant) -> None:
"""Set up the number websocket API."""
websocket_api.async_register_command(hass, websocket_list_preview_features)
websocket_api.async_register_command(hass, websocket_update_preview_feature)
websocket_api.async_register_command(hass, websocket_subscribe_feature)
@callback
@@ -108,3 +110,52 @@ async def websocket_update_preview_feature(
hass.bus.async_fire(EVENT_LABS_UPDATED, event_data)
connection.send_result(msg["id"])
@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "labs/subscribe",
vol.Required("domain"): str,
vol.Required("preview_feature"): str,
}
)
def websocket_subscribe_feature(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to a specific lab preview feature updates."""
domain = msg["domain"]
preview_feature_key = msg["preview_feature"]
labs_data = hass.data[LABS_DATA]
preview_feature_id = f"{domain}.{preview_feature_key}"
if preview_feature_id not in labs_data.preview_features:
connection.send_error(
msg["id"],
websocket_api.ERR_NOT_FOUND,
f"Preview feature {preview_feature_id} not found",
)
return
preview_feature = labs_data.preview_features[preview_feature_id]
@callback
def send_event() -> None:
"""Send feature state to client."""
enabled = async_is_preview_feature_enabled(hass, domain, preview_feature_key)
connection.send_message(
websocket_api.event_message(
msg["id"],
preview_feature.to_dict(enabled=enabled),
)
)
connection.subscriptions[msg["id"]] = async_listen(
hass, domain, preview_feature_key, send_event
)
connection.send_result(msg["id"])
send_event()

View File

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

View File

@@ -108,6 +108,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_MESSAGE,
_async_service_message,
schema=SERVICE_MESSAGE_SCHEMA,
description_placeholders={"icons_url": "https://developer.lametric.com/icons"},
)

View File

@@ -211,7 +211,7 @@
"name": "[%key:common::config_flow::data::device%]"
},
"icon": {
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: https://developer.lametric.com/icons.",
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: {icons_url}.",
"name": "Icon ID"
},
"icon_type": {

View File

@@ -19,8 +19,8 @@ from .const import CONF_DOMAIN_DATA
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(minutes=1)
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(minutes=10)
def add_lcn_entities(

View File

@@ -37,7 +37,7 @@ from .const import (
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(minutes=1)

View File

@@ -28,7 +28,7 @@ from .const import (
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(minutes=1)

View File

@@ -33,8 +33,8 @@ from .helpers import InputType, LcnConfigEntry
BRIGHTNESS_SCALE = (1, 100)
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(minutes=1)
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(minutes=10)
def add_lcn_entities(

View File

@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["pypck"],
"quality_scale": "silver",
"requirements": ["pypck==0.9.5", "lcn-frontend==0.2.7"]
"requirements": ["pypck==0.9.8", "lcn-frontend==0.2.7"]
}

View File

@@ -22,7 +22,7 @@ from .const import (
from .entity import LcnEntity
from .helpers import LcnConfigEntry
PARALLEL_UPDATES = 0
PARALLEL_UPDATES = 2
def add_lcn_entities(

View File

@@ -40,7 +40,7 @@ from .const import (
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(minutes=1)

View File

@@ -17,8 +17,8 @@ from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS, RELAY_PORTS, SET
from .entity import LcnEntity
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(minutes=1)
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(minutes=10)
def add_lcn_switch_entities(

View File

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

View File

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

View File

@@ -98,7 +98,11 @@ class LutronCasetaSmartAwaySwitch(LutronCasetaEntity, SwitchEntity):
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
self._smartbridge.add_smart_away_subscriber(self._handle_bridge_update)
self._smartbridge.add_smart_away_subscriber(self._handle_smart_away_update)
def _handle_smart_away_update(self, smart_away_state: str | None = None) -> None:
"""Handle updated smart away state from the bridge."""
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn Smart Away on."""

View File

@@ -183,6 +183,48 @@ class MatterModeSelectEntity(MatterAttributeSelectEntity):
self._attr_name = desc
class MatterDoorLockOperatingModeSelectEntity(MatterAttributeSelectEntity):
"""Representation of a Door Lock Operating Mode select entity.
This entity dynamically filters available operating modes based on the device's
`SupportedOperatingModes` bitmap attribute. In this bitmap, bit=0 indicates a
supported mode and bit=1 indicates unsupported (inverted from typical bitmap conventions).
If the bitmap is unavailable, only mandatory modes are included. The mapping from
bitmap bits to operating mode values is defined by the Matter specification.
"""
entity_description: MatterMapSelectEntityDescription
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# Get the bitmap of supported operating modes
supported_modes_bitmap = self.get_matter_attribute_value(
self.entity_description.list_attribute
)
# Convert bitmap to list of supported mode values
# NOTE: The Matter spec inverts the usual meaning: bit=0 means supported,
# bit=1 means not supported, undefined bits must be 1. Mandatory modes are
# bits 0 (Normal) and 3 (NoRemoteLockUnlock).
num_mode_bits = supported_modes_bitmap.bit_length()
supported_mode_values = [
bit_position
for bit_position in range(num_mode_bits)
if not supported_modes_bitmap & (1 << bit_position)
]
# Map supported mode values to their string representations
self._attr_options = [
mapped_value
for mode_value in supported_mode_values
if (mapped_value := self.entity_description.device_to_ha(mode_value))
]
# Use base implementation to set the current option
super()._update_from_device()
class MatterListSelectEntity(MatterEntity, SelectEntity):
"""Representation of a select entity from Matter list and selected item Cluster attribute(s)."""
@@ -594,15 +636,18 @@ DISCOVERY_SCHEMAS = [
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
entity_description=MatterMapSelectEntityDescription(
key="DoorLockOperatingMode",
entity_category=EntityCategory.CONFIG,
translation_key="door_lock_operating_mode",
options=list(DOOR_LOCK_OPERATING_MODE_MAP.values()),
list_attribute=clusters.DoorLock.Attributes.SupportedOperatingModes,
device_to_ha=DOOR_LOCK_OPERATING_MODE_MAP.get,
ha_to_device=DOOR_LOCK_OPERATING_MODE_MAP_REVERSE.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(clusters.DoorLock.Attributes.OperatingMode,),
entity_class=MatterDoorLockOperatingModeSelectEntity,
required_attributes=(
clusters.DoorLock.Attributes.OperatingMode,
clusters.DoorLock.Attributes.SupportedOperatingModes,
),
),
]

View File

@@ -15,6 +15,13 @@ from .entity import MealieEntity
PARALLEL_UPDATES = 0
SUPPORTED_MEALPLAN_ENTRY_TYPES = [
MealplanEntryType.BREAKFAST,
MealplanEntryType.DINNER,
MealplanEntryType.LUNCH,
MealplanEntryType.SIDE,
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -26,7 +33,7 @@ async def async_setup_entry(
async_add_entities(
MealieMealplanCalendarEntity(coordinator, entry_type)
for entry_type in MealplanEntryType
for entry_type in SUPPORTED_MEALPLAN_ENTRY_TYPES
)

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["aiomealie==1.1.0"]
"requirements": ["aiomealie==1.1.1"]
}

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2025.11.12"],
"requirements": ["yt-dlp[default]==2025.12.08"],
"single_config_entry": true
}

View File

@@ -98,50 +98,28 @@ DEVICE_TYPE_TAGS = {
}
class StateStatus(IntEnum):
class StateStatus(MieleEnum, missing_to_none=True):
"""Define appliance states."""
RESERVED = 0
OFF = 1
ON = 2
PROGRAMMED = 3
WAITING_TO_START = 4
IN_USE = 5
PAUSE = 6
PROGRAM_ENDED = 7
FAILURE = 8
PROGRAM_INTERRUPTED = 9
IDLE = 10
RINSE_HOLD = 11
SERVICE = 12
SUPERFREEZING = 13
SUPERCOOLING = 14
SUPERHEATING = 15
SUPERCOOLING_SUPERFREEZING = 146
AUTOCLEANING = 147
NOT_CONNECTED = 255
STATE_STATUS_TAGS = {
StateStatus.OFF: "off",
StateStatus.ON: "on",
StateStatus.PROGRAMMED: "programmed",
StateStatus.WAITING_TO_START: "waiting_to_start",
StateStatus.IN_USE: "in_use",
StateStatus.PAUSE: "pause",
StateStatus.PROGRAM_ENDED: "program_ended",
StateStatus.FAILURE: "failure",
StateStatus.PROGRAM_INTERRUPTED: "program_interrupted",
StateStatus.IDLE: "idle",
StateStatus.RINSE_HOLD: "rinse_hold",
StateStatus.SERVICE: "service",
StateStatus.SUPERFREEZING: "superfreezing",
StateStatus.SUPERCOOLING: "supercooling",
StateStatus.SUPERHEATING: "superheating",
StateStatus.SUPERCOOLING_SUPERFREEZING: "supercooling_superfreezing",
StateStatus.AUTOCLEANING: "autocleaning",
StateStatus.NOT_CONNECTED: "not_connected",
}
reserved = 0
off = 1
on = 2
programmed = 3
waiting_to_start = 4
in_use = 5
pause = 6
program_ended = 7
failure = 8
program_interrupted = 9
idle = 10
rinse_hold = 11
service = 12
superfreezing = 13
supercooling = 14
superheating = 15
supercooling_superfreezing = 146
autocleaning = 147
not_connected = 255
class MieleActions(IntEnum):
@@ -191,6 +169,7 @@ class ProgramPhaseWashingMachine(MieleEnum, missing_to_none=True):
drying = 280
disinfecting = 285
flex_load_active = 11047
automatic_start = 11044
class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
@@ -451,19 +430,19 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for washing machines."""
no_program = 0, -1
cottons = 1
cottons = 1, 10001
minimum_iron = 3
delicates = 4
woollens = 8
silks = 9
delicates = 4, 10022
woollens = 8, 10040
silks = 9, 10042
starch = 17
rinse = 18
drain_spin = 21
curtains = 22
shirts = 23
rinse = 18, 10058
drain_spin = 21, 10036
curtains = 22, 10055
shirts = 23, 10038
denim = 24, 123
proofing = 27
sportswear = 29
proofing = 27, 10057
sportswear = 29, 10052
automatic_plus = 31
outerwear = 37
pillows = 39
@@ -472,19 +451,29 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
rinse_out_lint = 48 # washer-dryer
dark_garments = 50
separate_rinse_starch = 52
first_wash = 53
first_wash = 53, 10053
cottons_hygiene = 69
steam_care = 75 # washer-dryer
freshen_up = 76 # washer-dryer
trainers = 77
clean_machine = 91
down_duvets = 95
express_20 = 122
trainers = 77, 10056
clean_machine = 91, 10067
down_duvets = 95, 10050
express_20 = 122, 10029
down_filled_items = 129
cottons_eco = 133
quick_power_wash = 146, 10031
eco_40_60 = 190, 10007
normal = 10001
bed_linen = 10047
easy_care = 10016
dark_jeans = 10048
outdoor_garments = 10049
game_pieces = 10070
stuffed_toys = 10069
pre_ironing = 10059
trainers_refresh = 10066
smartmatic = 10068
cottonrepair = 10065
powerfresh = 10075
class DishWasherProgramId(MieleEnum, missing_to_none=True):

View File

@@ -73,5 +73,5 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
return (
super().available
and self._device_id in self.coordinator.data.devices
and (self.device.state_status is not StateStatus.NOT_CONNECTED)
and (self.device.state_status is not StateStatus.not_connected)
)

View File

@@ -9,7 +9,7 @@
"iot_class": "cloud_push",
"loggers": ["pymiele"],
"quality_scale": "platinum",
"requirements": ["pymiele==0.6.0"],
"requirements": ["pymiele==0.6.1"],
"single_config_entry": true,
"zeroconf": ["_mieleathome._tcp.local."]
}

View File

@@ -38,7 +38,6 @@ from .const import (
DOMAIN,
PROGRAM_IDS,
PROGRAM_PHASE,
STATE_STATUS_TAGS,
MieleAppliance,
PlatePowerStep,
StateDryingStep,
@@ -195,7 +194,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
translation_key="status",
value_fn=lambda value: value.state_status,
device_class=SensorDeviceClass.ENUM,
options=sorted(set(STATE_STATUS_TAGS.values())),
options=sorted(set(StateStatus.keys())),
),
),
MieleSensorDefinition(
@@ -930,7 +929,7 @@ class MieleStatusSensor(MieleSensor):
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return STATE_STATUS_TAGS.get(StateStatus(self.device.state_status))
return StateStatus(self.device.state_status).name
@property
def available(self) -> bool:
@@ -998,11 +997,11 @@ class MieleTimeSensor(MieleRestorableSensor):
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
current_status = StateStatus(self.device.state_status).name
# report end-specific value when program ends (some devices are immediately reporting 0...)
if (
current_status == StateStatus.PROGRAM_ENDED
current_status == StateStatus.program_ended.name
and self.entity_description.end_value_fn is not None
):
self._attr_native_value = self.entity_description.end_value_fn(
@@ -1010,11 +1009,15 @@ class MieleTimeSensor(MieleRestorableSensor):
)
# keep value when program ends if no function is specified
elif current_status == StateStatus.PROGRAM_ENDED:
elif current_status == StateStatus.program_ended.name:
pass
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
elif current_status in (
StateStatus.off.name,
StateStatus.on.name,
StateStatus.idle.name,
):
self._attr_native_value = None
# otherwise, cache value and return it
@@ -1030,7 +1033,7 @@ class MieleAbsoluteTimeSensor(MieleRestorableSensor):
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
current_status = StateStatus(self.device.state_status).name
# The API reports with minute precision, to avoid changing
# the value too often, we keep the cached value if it differs
@@ -1043,11 +1046,15 @@ class MieleAbsoluteTimeSensor(MieleRestorableSensor):
< current_value
< self._previous_value + timedelta(seconds=90)
)
) or current_status == StateStatus.PROGRAM_ENDED:
) or current_status == StateStatus.program_ended.name:
return
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
if current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
if current_status in (
StateStatus.off.name,
StateStatus.on.name,
StateStatus.idle.name,
):
self._attr_native_value = None
# otherwise, cache value and return it
@@ -1064,7 +1071,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
current_status = StateStatus(self.device.state_status).name
# Guard for corrupt restored value
restored_value = (
self._attr_native_value
@@ -1079,12 +1086,12 @@ class MieleConsumptionSensor(MieleRestorableSensor):
# Force unknown when appliance is not able to report consumption
if current_status in (
StateStatus.ON,
StateStatus.OFF,
StateStatus.PROGRAMMED,
StateStatus.WAITING_TO_START,
StateStatus.IDLE,
StateStatus.SERVICE,
StateStatus.on.name,
StateStatus.off.name,
StateStatus.programmed.name,
StateStatus.waiting_to_start.name,
StateStatus.idle.name,
StateStatus.service.name,
):
self._is_reporting = False
self._attr_native_value = None
@@ -1093,7 +1100,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
# we already saw a valid value in this cycle from cache
elif (
current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
current_status in (StateStatus.in_use.name, StateStatus.pause.name)
and not self._is_reporting
and last_value > 0
):
@@ -1101,7 +1108,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
self._is_reporting = True
elif (
current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
current_status in (StateStatus.in_use.name, StateStatus.pause.name)
and not self._is_reporting
and current_value is not None
and cast(int, current_value) > 0
@@ -1109,7 +1116,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
self._attr_native_value = 0
# keep value when program ends
elif current_status == StateStatus.PROGRAM_ENDED:
elif current_status == StateStatus.program_ended.name:
pass
else:

View File

@@ -411,6 +411,7 @@
"cook_bacon": "Cook bacon",
"cool_air": "Cool air",
"corn_on_the_cob": "Corn on the cob",
"cottonrepair": "CottonRepair",
"cottons": "Cottons",
"cottons_eco": "Cottons ECO",
"cottons_hygiene": "Cottons hygiene",
@@ -440,6 +441,7 @@
"custom_program_8": "Custom program 8",
"custom_program_9": "Custom program 9",
"dark_garments": "Dark garments",
"dark_jeans": "Dark/jeans",
"dark_mixed_grain_bread": "Dark mixed grain bread",
"decrystallise_honey": "Decrystallize honey",
"defrost": "Defrost",
@@ -457,6 +459,7 @@
"drop_cookies_2_trays": "Drop cookies (2 trays)",
"duck": "Duck",
"dutch_hash": "Dutch hash",
"easy_care": "Easy care",
"eco": "ECO",
"eco_40_60": "ECO 40-60",
"eco_fan_heat": "ECO fan heat",
@@ -487,6 +490,7 @@
"fruit_streusel_cake": "Fruit streusel cake",
"fruit_tea": "Fruit tea",
"full_grill": "Full grill",
"game_pieces": "Game pieces",
"gentle": "Gentle",
"gentle_denim": "Gentle denim",
"gentle_minimum_iron": "Gentle minimum iron",
@@ -607,6 +611,7 @@
"oats_cracked": "Oats (cracked)",
"oats_whole": "Oats (whole)",
"osso_buco": "Osso buco",
"outdoor_garments": "Outdoor garments",
"outerwear": "Outerwear",
"oyster_mushroom_diced": "Oyster mushroom (diced)",
"oyster_mushroom_strips": "Oyster mushroom (strips)",
@@ -713,8 +718,10 @@
"potatoes_waxy_whole_small": "Potatoes (waxy, whole, small)",
"poularde_breast": "Poularde breast",
"poularde_whole": "Poularde (whole)",
"power_fresh": "PowerFresh",
"power_wash": "PowerWash",
"prawns": "Prawns",
"pre_ironing": "Pre-ironing",
"proofing": "Proofing",
"prove_15_min": "Prove for 15 min",
"prove_30_min": "Prove for 30 min",
@@ -807,6 +814,7 @@
"simiao_rapid_steam_cooking": "Simiao (rapid steam cooking)",
"simiao_steam_cooking": "Simiao (steam cooking)",
"small_shrimps": "Small shrimps",
"smartmatic": "SmartMatic",
"smoothing": "Smoothing",
"snow_pea": "Snow pea",
"soak": "Soak",
@@ -833,6 +841,7 @@
"sterilize_crockery": "Sterilize crockery",
"stollen": "Stollen",
"stuffed_cabbage": "Stuffed cabbage",
"stuffed_toys": "Stuffed toys",
"sweat_onions": "Sweat onions",
"swede_cut_into_batons": "Swede (cut into batons)",
"swede_diced": "Swede (diced)",
@@ -855,6 +864,7 @@
"top_heat": "Top heat",
"tortellini_fresh": "Tortellini (fresh)",
"trainers": "Trainers",
"trainers_refresh": "Trainers refresh",
"treacle_sponge_pudding_one_large": "Treacle sponge pudding (one large)",
"treacle_sponge_pudding_several_small": "Treacle sponge pudding (several small)",
"trout": "Trout",
@@ -935,6 +945,7 @@
"2nd_grinding": "2nd grinding",
"2nd_pre_brewing": "2nd pre-brewing",
"anti_crease": "Anti-crease",
"automatic_start": "Automatic start",
"blocked_brushes": "Brushes blocked",
"blocked_drive_wheels": "Drive wheels blocked",
"blocked_front_wheel": "Front wheel blocked",
@@ -1050,6 +1061,7 @@
"program_ended": "Program ended",
"program_interrupted": "Program interrupted",
"programmed": "Programmed",
"reserved": "Reserved",
"rinse_hold": "Rinse hold",
"service": "Service",
"supercooling": "Supercooling",

View File

@@ -58,7 +58,7 @@ SWITCH_TYPES: Final[tuple[MieleSwitchDefinition, ...]] = (
description=MieleSwitchDescription(
key="supercooling",
value_fn=lambda value: value.state_status,
on_value=StateStatus.SUPERCOOLING,
on_value=StateStatus.supercooling,
translation_key="supercooling",
on_cmd_data={PROCESS_ACTION: MieleActions.START_SUPERCOOL},
off_cmd_data={PROCESS_ACTION: MieleActions.STOP_SUPERCOOL},
@@ -73,7 +73,7 @@ SWITCH_TYPES: Final[tuple[MieleSwitchDefinition, ...]] = (
description=MieleSwitchDescription(
key="superfreezing",
value_fn=lambda value: value.state_status,
on_value=StateStatus.SUPERFREEZING,
on_value=StateStatus.superfreezing,
translation_key="superfreezing",
on_cmd_data={PROCESS_ACTION: MieleActions.START_SUPERFREEZE},
off_cmd_data={PROCESS_ACTION: MieleActions.STOP_SUPERFREEZE},

View File

@@ -315,7 +315,7 @@ class MoldIndicator(SensorEntity):
# Return an error if the sensor change its state to Unknown.
if state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
_LOGGER.error(
_LOGGER.debug(
"Unable to parse temperature sensor %s with state: %s",
state.entity_id,
state.state,
@@ -352,7 +352,7 @@ class MoldIndicator(SensorEntity):
# Return an error if the sensor change its state to Unknown.
if state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
_LOGGER.error(
_LOGGER.debug(
"Unable to parse humidity sensor %s, state: %s",
state.entity_id,
state.state,

View File

@@ -46,7 +46,7 @@
"ws_path": "WebSocket path"
},
"data_description": {
"advanced_options": "Enable and select **Next** to set advanced options.",
"advanced_options": "Enable and select **Submit** to set advanced options.",
"broker": "The hostname or IP address of your MQTT broker.",
"certificate": "The custom CA certificate file to validate your MQTT brokers certificate.",
"client_cert": "The client certificate to authenticate against your MQTT broker.",

View File

@@ -27,7 +27,11 @@ from music_assistant_models.player import Player
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import (
@@ -101,6 +105,15 @@ async def async_setup_entry( # noqa: C901
)
raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
except (AuthenticationRequired, AuthenticationFailed, InvalidToken) as err:
assert mass.server_info is not None
# Users cannot reauthenticate when running as Home Assistant addon,
# so raising ConfigEntryAuthFailed in that case would be incorrect.
# Instead we should wait until the addon discovery is completed,
# as that will set up authentication and reload the entry automatically.
if mass.server_info.homeassistant_addon:
raise ConfigEntryError(
"Authentication failed, addon discovery not completed yet"
) from err
raise ConfigEntryAuthFailed(
f"Authentication failed for {mass_url}: {err}"
) from err

View File

@@ -163,9 +163,6 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
LOGGER.exception("Unexpected exception during add-on discovery")
return self.async_abort(reason="unknown")
if not server_info.onboard_done:
return self.async_abort(reason="server_not_ready")
# We trust the token from hassio discovery and validate it during setup
self.token = discovery_info.config["auth_token"]
@@ -182,6 +179,7 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
ConfigEntryState.LOADED,
ConfigEntryState.SETUP_ERROR,
ConfigEntryState.SETUP_RETRY,
ConfigEntryState.SETUP_IN_PROGRESS,
):
self.hass.config_entries.async_schedule_reload(entry.entry_id)
@@ -226,11 +224,6 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
LOGGER.debug("Ignoring add-on server in zeroconf discovery")
return self.async_abort(reason="already_discovered_addon")
# Ignore servers that have not completed onboarding yet
if not server_info.onboard_done:
LOGGER.debug("Ignoring server that hasn't completed onboarding")
return self.async_abort(reason="server_not_ready")
self.url = server_info.base_url
self.server_info = server_info

View File

@@ -2,11 +2,11 @@
from __future__ import annotations
from pynintendoparental import Authenticator
from pynintendoparental.exceptions import (
from pynintendoauth.exceptions import (
InvalidOAuthConfigurationException,
InvalidSessionTokenException,
)
from pynintendoparental import Authenticator
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -39,13 +39,12 @@ async def async_setup_entry(
hass: HomeAssistant, entry: NintendoParentalControlsConfigEntry
) -> bool:
"""Set up Nintendo Switch parental controls from a config entry."""
nintendo_auth = Authenticator(
session_token=entry.data[CONF_SESSION_TOKEN],
client_session=async_get_clientsession(hass),
)
try:
nintendo_auth = await Authenticator.complete_login(
auth=None,
response_token=entry.data[CONF_SESSION_TOKEN],
is_session_token=True,
client_session=async_get_clientsession(hass),
)
await nintendo_auth.async_complete_login(use_session_token=True)
except (InvalidSessionTokenException, InvalidOAuthConfigurationException) as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -6,9 +6,9 @@ from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
from pynintendoauth.exceptions import HttpException, InvalidSessionTokenException
from pynintendoparental import Authenticator
from pynintendoparental.api import Api
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -33,18 +33,14 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors = {}
if self.auth is None:
self.auth = Authenticator.generate_login(
client_session=async_get_clientsession(self.hass)
)
self.auth = Authenticator(client_session=async_get_clientsession(self.hass))
if user_input is not None:
nintendo_api = Api(
self.auth, self.hass.config.time_zone, self.hass.config.language
)
try:
await self.auth.complete_login(
self.auth, user_input[CONF_API_TOKEN], False
)
await self.auth.async_complete_login(user_input[CONF_API_TOKEN])
except (ValueError, InvalidSessionTokenException, HttpException):
errors["base"] = "invalid_auth"
else:
@@ -67,7 +63,7 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title=self.auth.account_id,
data={
CONF_SESSION_TOKEN: self.auth.get_session_token,
CONF_SESSION_TOKEN: self.auth.session_token,
},
)
return self.async_show_form(
@@ -90,14 +86,10 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if self.auth is None:
self.auth = Authenticator.generate_login(
client_session=async_get_clientsession(self.hass)
)
self.auth = Authenticator(client_session=async_get_clientsession(self.hass))
if user_input is not None:
try:
await self.auth.complete_login(
self.auth, user_input[CONF_API_TOKEN], False
)
await self.auth.async_complete_login(user_input[CONF_API_TOKEN])
except (ValueError, InvalidSessionTokenException, HttpException):
errors["base"] = "invalid_auth"
else:
@@ -105,7 +97,7 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry,
data={
**reauth_entry.data,
CONF_SESSION_TOKEN: self.auth.get_session_token,
CONF_SESSION_TOKEN: self.auth.session_token,
},
)
return self.async_show_form(

View File

@@ -5,11 +5,9 @@ from __future__ import annotations
from datetime import timedelta
import logging
from pynintendoauth.exceptions import InvalidOAuthConfigurationException
from pynintendoparental import Authenticator, NintendoParental
from pynintendoparental.exceptions import (
InvalidOAuthConfigurationException,
NoDevicesFoundException,
)
from pynintendoparental.exceptions import NoDevicesFoundException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

View File

@@ -5,7 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nintendo_parental_controls",
"iot_class": "cloud_polling",
"loggers": ["pynintendoparental"],
"loggers": ["pynintendoauth", "pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoparental==1.1.3"]
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.1.3"]
}

View File

@@ -49,6 +49,7 @@ SENSORS = [
key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda client: client.power.amps,
),
OhmeSensorDescription(
@@ -57,6 +58,7 @@ SENSORS = [
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda client: client.power.watts,
),
OhmeSensorDescription(
@@ -81,6 +83,7 @@ SENSORS = [
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda client: client.battery,
),
OhmeSensorDescription(

View File

@@ -129,4 +129,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -156,7 +156,7 @@
},
"filename": {
"description": "Path to the file to upload.",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Filename"
}
},

View File

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

View File

@@ -25,6 +25,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
@@ -101,7 +102,18 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
except OSError:
_LOGGER.error("Pilight send failed for %s", str(message_data))
hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
def _register_service() -> None:
hass.services.async_register(
DOMAIN,
SERVICE_NAME,
send_code,
schema=RF_CODE_SCHEMA,
description_placeholders={
"pilight_protocols_docs_url": "https://manual.pilight.org/protocols/index.html"
},
)
run_callback_threadsafe(hass.loop, _register_service).result()
# Publish received codes on the HA event bus
# A whitelist of codes to be published in the event bus

View File

@@ -4,7 +4,7 @@
"description": "Sends RF code to Pilight device.",
"fields": {
"protocol": {
"description": "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports.",
"description": "Protocol that Pilight recognizes. See {pilight_protocols_docs_url} for supported protocols and additional parameters that each protocol supports.",
"name": "Protocol"
}
},

View File

@@ -37,7 +37,6 @@ SELECT_TYPES = (
PlugwiseSelectEntityDescription(
key=SELECT_SCHEDULE,
translation_key=SELECT_SCHEDULE,
entity_category=EntityCategory.CONFIG,
options_key="available_schedules",
),
PlugwiseSelectEntityDescription(

View File

@@ -54,8 +54,11 @@ from .const import (
)
from .coordinator import RainMachineDataUpdateCoordinator
DEFAULT_SSL = True
API_URL_REFERENCE = (
"https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post"
)
DEFAULT_SSL = True
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -455,7 +458,15 @@ async def async_setup_entry( # noqa: C901
):
if hass.services.has_service(DOMAIN, service_name):
continue
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
hass.services.async_register(
DOMAIN,
service_name,
method,
schema=schema,
description_placeholders={
"api_url": API_URL_REFERENCE,
},
)
return True

View File

@@ -128,7 +128,7 @@
"name": "Push flow meter data"
},
"push_weather_data": {
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post.",
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: {api_url}",
"fields": {
"condition": {
"description": "Current weather condition code (WNUM).",

View File

@@ -4,7 +4,6 @@ from datetime import datetime
import logging
from ical.event import Event
from ical.timeline import Timeline
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
@@ -49,18 +48,12 @@ class RemoteCalendarEntity(
super().__init__(coordinator)
self._attr_name = entry.data[CONF_CALENDAR_NAME]
self._attr_unique_id = entry.entry_id
self._timeline: Timeline | None = None
self._event: CalendarEvent | None = None
@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
if self._timeline is None:
return None
now = dt_util.now()
events = self._timeline.active_after(now)
if event := next(events, None):
return _get_calendar_event(event)
return None
return self._event
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
@@ -86,12 +79,14 @@ class RemoteCalendarEntity(
"""
await super().async_update()
def _get_timeline() -> Timeline | None:
"""Return the next active event."""
def next_event() -> CalendarEvent | None:
now = dt_util.now()
return self.coordinator.data.timeline_tz(now.tzinfo)
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
if event := next(events, None):
return _get_calendar_event(event)
return None
self._timeline = await self.hass.async_add_executor_job(_get_timeline)
self._event = await self.hass.async_add_executor_job(next_event)
def _get_calendar_event(event: Event) -> CalendarEvent:

View File

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

View File

@@ -4,8 +4,9 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import timedelta
from datetime import UTC, datetime, timedelta
import logging
from random import uniform
from time import time
from typing import Any
@@ -34,6 +35,7 @@ from .const import (
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
CONF_BC_ONLY,
CONF_BC_PORT,
CONF_FIRMWARE_CHECK_TIME,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DOMAIN,
@@ -212,15 +214,41 @@ async def async_setup_entry(
config_entry=config_entry,
name=f"reolink.{host.api.nvr_name}.firmware",
update_method=async_check_firmware_update,
update_interval=FIRMWARE_UPDATE_INTERVAL,
update_interval=None, # Do not fetch data automatically, resume 24h schedule
)
async def first_firmware_check(*args: Any) -> None:
"""Start first firmware check delayed to continue 24h schedule."""
firmware_coordinator.update_interval = FIRMWARE_UPDATE_INTERVAL
await firmware_coordinator.async_refresh()
host.cancel_first_firmware_check = None
# get update time from config entry
check_time_sec = config_entry.data.get(CONF_FIRMWARE_CHECK_TIME)
if check_time_sec is None:
check_time_sec = uniform(0, 86400)
data = {
**config_entry.data,
CONF_FIRMWARE_CHECK_TIME: check_time_sec,
}
hass.config_entries.async_update_entry(config_entry, data=data)
# If camera WAN blocked, firmware check fails and takes long, do not prevent setup
config_entry.async_create_background_task(
hass,
firmware_coordinator.async_refresh(),
f"Reolink firmware check {config_entry.entry_id}",
now = datetime.now(UTC)
check_time = timedelta(seconds=check_time_sec)
delta_midnight = now - now.replace(hour=0, minute=0, second=0, microsecond=0)
firmware_check_delay = check_time - delta_midnight
if firmware_check_delay < timedelta(0):
firmware_check_delay += timedelta(days=1)
_LOGGER.debug(
"Scheduling first Reolink %s firmware check in %s",
host.api.nvr_name,
firmware_check_delay,
)
host.cancel_first_firmware_check = async_call_later(
hass, firmware_check_delay, first_firmware_check
)
# Fetch initial data so we have data when entities subscribe
try:
await device_coordinator.async_config_entry_first_refresh()
@@ -312,6 +340,8 @@ async def async_unload_entry(
host.api.baichuan.unregister_callback(f"camera_{channel}_wake")
if host.cancel_refresh_privacy_mode is not None:
host.cancel_refresh_privacy_mode()
if host.cancel_first_firmware_check is not None:
host.cancel_first_firmware_check()
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

View File

@@ -6,6 +6,7 @@ CONF_USE_HTTPS = "use_https"
CONF_BC_PORT = "baichuan_port"
CONF_BC_ONLY = "baichuan_only"
CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported"
CONF_FIRMWARE_CHECK_TIME = "firmware_check_time"
# Conserve battery by not waking the battery cameras each minute during normal update
# Most props are cached in the Home Hub and updated, but some are skipped

View File

@@ -130,6 +130,7 @@ class ReolinkHost:
self._lost_subscription_start: bool = False
self._lost_subscription: bool = False
self.cancel_refresh_privacy_mode: CALLBACK_TYPE | None = None
self.cancel_first_firmware_check: CALLBACK_TYPE | None = None
@callback
def async_register_update_cmd(self, cmd: str, channel: int | None = None) -> None:
@@ -422,6 +423,8 @@ class ReolinkHost:
"name": self._api.nvr_name,
"base_url": self._base_url,
"network_link": "https://my.home-assistant.io/redirect/network/",
"example_ip": "192.168.1.10",
"example_url": "http://192.168.1.10:8123",
},
)
@@ -436,6 +439,8 @@ class ReolinkHost:
translation_placeholders={
"base_url": self._base_url,
"network_link": "https://my.home-assistant.io/redirect/network/",
"example_ip": "192.168.1.10",
"example_url": "http://192.168.1.10:8123",
},
)
else:

View File

@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.17.0"]
"requirements": ["reolink-aio==0.17.1"]
}

View File

@@ -1004,7 +1004,7 @@
"title": "Reolink firmware update required"
},
"https_webhook": {
"description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device",
"description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `{example_url}` where `{example_ip}` is the IP of the Home Assistant device",
"title": "Reolink webhook URL uses HTTPS (SSL)"
},
"password_too_long": {
@@ -1016,7 +1016,7 @@
"title": "Reolink incompatible with global SSL certificate"
},
"webhook_url": {
"description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received.",
"description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `{example_url}` where `{example_ip}` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received.",
"title": "Reolink webhook URL unreachable"
}
},

View File

@@ -1,20 +1,23 @@
"""The Rituals Perfume Genie integration."""
import asyncio
import logging
import aiohttp
from pyrituals import Account, Diffuser
from aiohttp import ClientError, ClientResponseError
from pyrituals import Account, AuthenticationException, Diffuser
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACCOUNT_HASH, DOMAIN, UPDATE_INTERVAL
from .coordinator import RitualsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
@@ -26,12 +29,38 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Rituals Perfume Genie from a config entry."""
# Initiate reauth for old config entries which don't have username / password in the entry data
if CONF_EMAIL not in entry.data or CONF_PASSWORD not in entry.data:
raise ConfigEntryAuthFailed("Missing credentials")
session = async_get_clientsession(hass)
account = Account(session=session, account_hash=entry.data[ACCOUNT_HASH])
account = Account(
email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
session=session,
)
try:
# Authenticate first so API token/cookies are available for subsequent calls
await account.authenticate()
account_devices = await account.get_devices()
except aiohttp.ClientError as err:
except AuthenticationException as err:
# Credentials invalid/expired -> raise AuthFailed to trigger reauth flow
raise ConfigEntryAuthFailed(err) from err
except ClientResponseError as err:
_LOGGER.debug(
"HTTP error during Rituals setup: status=%s, url=%s, headers=%s",
err.status,
err.request_info,
dict(err.headers or {}),
)
raise ConfigEntryNotReady from err
except ClientError as err:
raise ConfigEntryNotReady from err
# Migrate old unique_ids to the new format
@@ -45,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Create a coordinator for each diffuser
coordinators = {
diffuser.hublot: RitualsDataUpdateCoordinator(
hass, entry, diffuser, update_interval
hass, entry, account, diffuser, update_interval
)
for diffuser in account_devices
}
@@ -106,3 +135,14 @@ def async_migrate_entities_unique_ids(
registry_entry.entity_id,
new_unique_id=f"{diffuser.hublot}-{new_unique_id}",
)
# Migration helpers for API v2
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate config entry to version 2: drop legacy ACCOUNT_HASH and bump version."""
if entry.version < 2:
data = dict(entry.data)
data.pop(ACCOUNT_HASH, None)
hass.config_entries.async_update_entry(entry, data=data, version=2)
return True
return True

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
import logging
from typing import Any
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any
from aiohttp import ClientResponseError
from aiohttp import ClientError
from pyrituals import Account, AuthenticationException
import voluptuous as vol
@@ -13,9 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACCOUNT_HASH, DOMAIN
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{
@@ -28,39 +26,88 @@ DATA_SCHEMA = vol.Schema(
class RitualsPerfumeGenieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Rituals Perfume Genie."""
VERSION = 1
VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
errors = {}
session = async_get_clientsession(self.hass)
account = Account(user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session)
try:
await account.authenticate()
except ClientResponseError:
_LOGGER.exception("Unexpected response")
errors["base"] = "cannot_connect"
except AuthenticationException:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(account.email)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=account.email,
data={ACCOUNT_HASH: account.account_hash},
errors: dict[str, str] = {}
if user_input is not None:
session = async_get_clientsession(self.hass)
account = Account(
email=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=session,
)
try:
await account.authenticate()
except AuthenticationException:
errors["base"] = "invalid_auth"
except ClientError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(user_input[CONF_EMAIL])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data=user_input,
)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Rituals."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Form to log in again."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if TYPE_CHECKING:
assert reauth_entry.unique_id is not None
if user_input:
session = async_get_clientsession(self.hass)
account = Account(
email=reauth_entry.unique_id,
password=user_input[CONF_PASSWORD],
session=session,
)
try:
await account.authenticate()
except AuthenticationException:
errors["base"] = "invalid_auth"
except ClientError:
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data={
CONF_EMAIL: reauth_entry.unique_id,
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
reauth_entry.data,
),
errors=errors,
)

View File

@@ -4,6 +4,7 @@ from datetime import timedelta
DOMAIN = "rituals_perfume_genie"
# Old (API V1)
ACCOUNT_HASH = "account_hash"
# The API provided by Rituals is currently rate limited to 30 requests

View File

@@ -3,11 +3,13 @@
from datetime import timedelta
import logging
from pyrituals import Diffuser
from aiohttp import ClientError, ClientResponseError
from pyrituals import Account, AuthenticationException, Diffuser
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -23,10 +25,12 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
account: Account,
diffuser: Diffuser,
update_interval: timedelta,
) -> None:
"""Initialize global Rituals Perfume Genie data updater."""
self.account = account
self.diffuser = diffuser
super().__init__(
hass,
@@ -37,5 +41,36 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
)
async def _async_update_data(self) -> None:
"""Fetch data from Rituals."""
await self.diffuser.update_data()
"""Fetch data from Rituals, with one silent re-auth on 401.
If silent re-auth also fails, raise ConfigEntryAuthFailed to trigger reauth flow.
Other HTTP/network errors are wrapped in UpdateFailed so HA can retry.
"""
try:
await self.diffuser.update_data()
except (AuthenticationException, ClientResponseError) as err:
# Treat 401/403 like AuthenticationException → one silent re-auth, single retry
if isinstance(err, ClientResponseError) and (status := err.status) not in (
401,
403,
):
# Non-auth HTTP error → let HA retry
raise UpdateFailed(f"HTTP {status}") from err
self.logger.debug(
"Auth issue detected (%r). Attempting silent re-auth.", err
)
try:
await self.account.authenticate()
await self.diffuser.update_data()
except AuthenticationException as err2:
# Credentials invalid → trigger HA reauth
raise ConfigEntryAuthFailed from err2
except ClientResponseError as err2:
# Still HTTP auth errors after refresh → trigger HA reauth
if err2.status in (401, 403):
raise ConfigEntryAuthFailed from err2
raise UpdateFailed(f"HTTP {err2.status}") from err2
except ClientError as err:
# Network issues (timeouts, DNS, etc.)
raise UpdateFailed(f"Network error: {err!r}") from err

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