Compare commits

...

488 Commits

Author SHA1 Message Date
Petar Petrov adeae40ce1 Strip unknown labels instead of erroring 2026-06-11 11:00:57 +03:00
Petar Petrov a6d3fb1808 Reject unknown label ids in registry websocket APIs 2026-06-10 14:01:04 +03:00
Manu 158a8b8c69 Add OptionsFlow to SMTP integration (#173386) 2026-06-10 12:00:28 +02:00
Martin Hjelmare 102cb4b69e Add pylint enforce dt.now checker (#173005) 2026-06-10 11:48:49 +02:00
Duco Sebel f3a3f4cde4 Remove positional message strings when translation_key is set in blink (#173390) 2026-06-10 11:47:00 +02:00
Jonathan Laliberte 19880b4214 Fix roomba charging state and add charging binary sensor (#173304) 2026-06-10 11:26:25 +02:00
Michael Davie 27599cbfe3 Bump env-canada to 0.15.0 (#173408)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:17:28 +02:00
Andreas Schneider 54cb4a7946 Bump babel to version 2.18.0 (#173424) 2026-06-10 11:01:43 +02:00
dependabot[bot] 4a5ee9e4ee Bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#173418)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-10 10:43:13 +02:00
Duco Sebel 8111667c1f Remove positional message strings when translation_key is set in teslemetry (#173391) 2026-06-10 10:26:47 +02:00
Crocmagnon 03eb139f6e ovhcloud_ai_endpoints: fix typo (#173410) 2026-06-10 08:15:29 +02:00
Stef Coene 426213dd29 velbus: allow device and sub-device removal (#168283)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-09 23:17:31 +02:00
Duco Sebel 8e659530da Remove 'home-assistant-exception-message-with-translation' pylint exception from mqtt (#173396) 2026-06-09 23:14:52 +02:00
Duco Sebel 76c28444e8 Remove 'home-assistant-exception-message-with-translation' pylint exception from bsblan (#173394) 2026-06-09 23:01:14 +02:00
Duco Sebel f4a9514d35 Remove 'home-assistant-exception-message-with-translation' pylint exception from nfandroidtv (#173398) 2026-06-09 23:00:48 +02:00
dependabot[bot] 6f5f59608c Bump github/codeql-action from 4.36.0 to 4.36.1 (#173333)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 22:41:59 +02:00
Will Pike fc8040784e Bump python-ecobee-api to 0.4.1 (#172601) 2026-06-09 22:18:28 +02:00
dependabot[bot] 8e77ef3e1b Bump actions/checkout from 6.0.2 to 6.0.3 (#173331)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 22:16:39 +02:00
dependabot[bot] 95257e36ef Bump github/gh-aw-actions from 0.77.3 to 0.78.1 (#173332)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 22:16:16 +02:00
Michael Hansen 89a600dc34 Only allow specific protocols with ffmpeg in Wyoming satellite announce (#173381)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-09 20:12:07 +02:00
Åke Strandberg 2b530a1dfa Add exception translations for aqvify (#173361) 2026-06-09 18:29:30 +02:00
Duco Sebel 9a01a0d758 Remove positional message strings when translation_key is set in homewizard (#173377) 2026-06-09 18:05:45 +02:00
Manu 145538c563 Improve strings in SMTP integration (#173379) 2026-06-09 18:02:50 +02:00
Nikolai Rahimi 6f09fc074d Bump mitsubishi-comfort to 0.3.1 (#173362) 2026-06-09 17:41:32 +02:00
Joost Lekkerkerker 372c6697a0 Set Zinvolt max output to 2kW if unlocked (#173367) 2026-06-09 16:41:21 +02:00
Joost Lekkerkerker bbd82ce511 Handle unavailable Zinvolt devices better (#173359) 2026-06-09 15:24:14 +02:00
Jan Bouwhuis bcb0908d0a Fix reload fails when MQTT entry is not set up (#173335)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-09 15:22:00 +02:00
Joakim Plate da5f497091 Ensure we provide strings to vol.In for philips js (#173313) 2026-06-09 14:42:38 +02:00
Åke Strandberg c28e215ae6 Add reauth flow to aqvify (#173287)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2026-06-09 12:50:19 +02:00
Manu ee2fb6e150 Add config flow to SMTP integration (#172019) 2026-06-09 12:38:22 +02:00
Charles Vestal 9bdb2e21fe Fix HomeKit crash on integer device trigger subtypes (#173334)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:15:46 +02:00
Triggs d2277ddbd7 Bump codecov/codecov-action from v6.0.1 to v7.0.0 (#173232)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-09 11:50:39 +02:00
Paul Bottein cbce4b8e76 Add reauthentication flow to Yoto (#173243) 2026-06-09 11:25:08 +02:00
renovate[bot] bb5d3fe67f Update uv to 0.11.18 (#173327) 2026-06-09 10:39:18 +02:00
Simone Chemelli 33753460ab Improve and complete exception handling for Alexa Devices (#173053) 2026-06-09 09:59:12 +02:00
Åke Strandberg d815487f7f Add diagnostics platform to aqvify (#173283) 2026-06-09 09:06:07 +02:00
Mick Vleeshouwer 5d3f7001ab Clean up redundant URL parsing in the Overkiz (#173273) 2026-06-09 08:46:02 +02:00
AlCalzone 2287c92f88 Bump zwave-js-server-python to 0.72.0 (#173309)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:44:07 +02:00
Marcello b1610163f1 Set PARALLEL_UPDATES for Fluss platforms (#173286) 2026-06-09 08:35:09 +02:00
Raphael Hehl 1cfae60c03 Bump uiprotect to 12.0.0 (#173315) 2026-06-09 06:23:58 +02:00
Åke Strandberg 49d1e0ea5b Bump pyaqvify to 0.0.9 (#173312) 2026-06-09 06:20:41 +02:00
Hai-Nam Nguyen 05f4e28c86 Bump hyponcloud to 1.0.0 (#173310) 2026-06-09 06:19:10 +02:00
renovate[bot] 2b5fc802c4 Update rf-protocols to 4.1.0 (#173328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-09 06:14:11 +02:00
tronikos 9b5038d741 Bump opower to 0.18.4 (#173323) 2026-06-09 06:08:26 +02:00
G Johansson 7c33b953d3 Remove not needed guards for integration migrations from future versions (#173301) 2026-06-08 15:57:25 -04:00
Martin Hjelmare 5ffd772868 Fix homeassistant hardware unique id migration (#173258) 2026-06-08 21:39:19 +02:00
Marcello 392c7f97c8 Add individual code owner for Fluss (#173276) 2026-06-08 21:38:49 +02:00
Allen Porter 0672c940a4 Use roboorck device capabilities to determine which entities are supported (#173282) 2026-06-08 20:36:54 +02:00
Michael Hansen 03b0b4ad8b Allow inline number ranges for sentence triggers (#173111)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-08 14:22:17 -04:00
cnico 9b84fc9dba Update dio-chacon-wifi-api to 1.3.0 (#173240) 2026-06-08 20:17:00 +02:00
Michael Hansen e38e6ecec8 Mitigate TTS ResultStream leak in pipeline (#173290) 2026-06-08 12:13:53 -04:00
Simone Chemelli 37e4f1ab32 Bump renault-api to 0.5.12 (#173289) 2026-06-08 17:52:22 +02:00
G Johansson 18d17a5346 Config entry migration error on downgrading (#173184) 2026-06-08 17:45:24 +02:00
Crocmagnon d10ede2264 data grand lyon: list stops and lines in config flow (#173117) 2026-06-08 17:25:01 +02:00
Jeef 05088bf991 Add initial quality scale for Weatherflow local (#166022)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-08 18:11:03 +03:00
epenet fbf14c63c0 Fix incorrect use of Platform in atag (#173025) 2026-06-08 16:55:50 +02:00
epenet 397c28b9b6 Use DOMAIN constant in test (async_setup_component h-n) (#173015) 2026-06-08 16:28:17 +02:00
Glenn Waters 676a8c39eb Environment Canada integration: add get_alerts action (#172393) 2026-06-08 16:16:42 +02:00
epenet d4accebb3b Use DOMAIN constant in test (async_setup_component a-g) (#173013) 2026-06-08 15:58:54 +02:00
Åke Strandberg b8bdd2c47c Add new Aqvify integration (#172936) 2026-06-08 15:45:03 +02:00
Evan Severson 828ec639dd Strip trailing slash from Jellyfin server URL (#173049) 2026-06-08 15:43:58 +02:00
Martin Claesson df4fbc91f9 Add Kiosker service platform (#171094) 2026-06-08 15:42:34 +02:00
G Johansson 1a4a95df83 Use query_dns from aiodns in dnsip (#173257) 2026-06-08 15:11:41 +02:00
Crocmagnon e4b5818b56 ovhcloud_ai_endpoints: add reconfigure flow (#172583) 2026-06-08 14:00:09 +02:00
starkillerOG c9ad482293 Adjust ONVIF event fallbacks for battery cameras (#173214)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 13:51:10 +02:00
peteS-UK 78cc155e56 Update PARALLEL_UPDATES to 0 for Squeezebox platforms (#172906) 2026-06-08 13:45:29 +02:00
Hai-Nam Nguyen 145639d048 Add load, grid, and battery sensors to Hypontech (#173150)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:40:37 +02:00
bkobus-bbx 3e3e9af30d Add state_class to blebox sensors (#173253)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-08 12:04:04 +02:00
Diogo Gomes c27e43c570 Moves V2C InstallationVoltage from Sensor to Number (#169771)
Co-authored-by: Samuel Cabrero <scabrero@suse.com>
Co-authored-by: Samuel Cabrero <samuel@orica.es>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Samuel Cabrero <samuel@orica.es>
2026-06-08 11:47:12 +02:00
cb2206 4f4aeff2b4 Lutron caseta prev brightness (#164080)
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:41:06 +02:00
Diogo Gomes 850cc27824 Bump pytrydan to v1.0.1 (#173047) 2026-06-08 11:24:47 +02:00
Erik Montnemery e19c063ef1 Improve tests of humanized error messages (#173256) 2026-06-08 11:08:05 +02:00
Colin 707742f720 Bump python-openevse-http to 1.0.1 (#172982) 2026-06-08 10:46:27 +02:00
Ronald van der Meer f58e0e5234 Fix Duco box device removal on partial node refreshes (#173186) 2026-06-08 09:48:27 +02:00
robotsnh c3d6ad029f refactor(energyid): replace datetime.now with dt_util.utcnow (#173241) 2026-06-08 09:11:48 +02:00
Mark Purcell 630f442042 Bump pydaikin to 2.18.1 (#173249) 2026-06-08 09:05:28 +02:00
Manu 62419789b9 Add version to Uptime Kuma diagnostics (#173254) 2026-06-08 08:46:55 +02:00
Joakim Plate f2f5a55165 Store product type in gardena_bluetooth config entry (#173223) 2026-06-08 08:20:47 +02:00
Mick Vleeshouwer c6a57bc81a Bump pyOverkiz to 2.0.0 in Overkiz (#173212) 2026-06-07 22:09:52 -04:00
Raphael Hehl 4171f566e9 Bump uiprotect to 11.8.0 (#173227) 2026-06-07 22:08:49 -04:00
renovate[bot] 0ac9834d93 Update syrupy to 5.3.1 (#173245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-07 22:07:48 -04:00
Paul Bottein d7673a08c8 Bump yoto-api to 4.0.2 (#173238) 2026-06-07 22:07:36 -04:00
J. Nick Koston 35cb7c6147 Bump aiohttp to 3.14.1 (#173242) 2026-06-07 22:07:05 -04:00
Paulus Schoutsen d098622021 Return all matches for duplicate names in GetLiveContext (#173157)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-07 21:50:15 -04:00
karwosts f88e757e51 Add a battery charging sensor to demo device (#173219) 2026-06-07 23:00:22 +02:00
Pierre Pinon 653e6a43fa fix(indevolt): unable to discharge at 0 (#173085) 2026-06-07 21:55:10 +02:00
Bram Kragten 1462e7a181 Update frontend to 20260527.5 (#173236) 2026-06-07 21:39:34 +02:00
Martin Claesson e34d821f7d Add Kiosker Clear Blackout Button (#173225) 2026-06-07 21:29:51 +02:00
G Johansson 02b4442a6c Fix config flow version in goodwe (#173235) 2026-06-07 21:26:40 +02:00
J. Nick Koston 809571443c Bump habluetooth to 6.8.3 (#173194) 2026-06-07 19:17:52 +02:00
mvn23 d59398e0ea Remove name fields from opentherm_gw config flow (#173159) 2026-06-07 18:21:21 +02:00
Raphael Hehl 9c9695d0ba Bump uiprotect to 11.3.0 (#173024)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-06-07 14:19:26 +02:00
Allen Porter 3fbdbb12e2 Support streaming updates for V1 Roborock devices (#173182) 2026-06-07 14:12:20 +02:00
Ronald van der Meer a29f2907f7 Use NodeType enum in Duco entity (#173189) 2026-06-07 14:07:48 +02:00
mvn23 83534f286e Ensure opentherm_gw boiler and thermostat manufacturers are strings (#173162) 2026-06-07 12:23:09 +02:00
Ronald van der Meer 4fe93f9c64 Fix uncaught Duco diagnostics client errors (#173191) 2026-06-07 07:27:02 +02:00
Shay Levy fd8789d599 Fix Shelly virtual component unit retrieval (#173183) 2026-06-07 00:34:18 +03:00
Joost Lekkerkerker d0b34dfe92 Have Plugwise handle unavailable temperature measurements (#173173) 2026-06-07 00:19:29 +03:00
Tomer 390766ba3a Bump victron-mqtt to 2026.6.1.1 (#173142) 2026-06-07 00:15:39 +03:00
Vincent Knoop Pathuis 3a46d1088b Refactor Landis+Gyr heat meter to use the HA standard SerialPortSelector (#173170) 2026-06-06 15:16:41 -04:00
epenet 26d56b8218 Use DOMAIN constant in test (async_setup_component o-z) (#173018) 2026-06-06 12:14:46 -07:00
Vincent Knoop Pathuis 6ee819cdc3 Bump to Ultraheat 0.6.1 (#173175) 2026-06-06 15:14:01 -04:00
Stefan Agner 1cf8fe4d0b Drop legacy requires_api_password from discovery announcement (#173090)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:54:36 -04:00
Michael Hansen c5f93cdd72 Validate sentences and answers (#173127)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-06-06 13:48:07 -04:00
Michael Hansen 42136f1464 Bubble up conversation response in script run (#173131)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-06 13:47:49 -04:00
J. Nick Koston 34f3452280 Wait for Shelly bluetooth proxy connection at startup (#173165) 2026-06-06 11:17:49 -05:00
Michael Hansen ba9248cc94 LLM: format numeric states with display precision (#173128) 2026-06-06 12:15:48 -04:00
Michael Hansen 018cd1333e Bump ollama library (#173129) 2026-06-06 12:14:59 -04:00
J. Nick Koston c72d723e0d Wait for ESPHome bluetooth proxy connection at startup (#173164) 2026-06-06 11:13:03 -05:00
Paul Bottein b9b36d9e12 Add card group browsing to the Yoto media browser (#173152) 2026-06-06 12:12:58 -04:00
Klaas Schoute b6f38c3cbb Update forecast_solar integration to v5.0.1 (#173151) 2026-06-06 15:26:14 +02:00
Bouwe Westerdijk a0162d2ff0 Bump plugwise to v1.11.4 (#173147) 2026-06-06 12:03:32 +02:00
robotsnh b6f018873b refactor(dwd_weather_warnings): change datetime.now to dt_util.utcnow (#173149) 2026-06-06 11:58:06 +02:00
Crocmagnon 43e21322ea Bump data-grand-lyon-ha to 0.8.0 (#173108) 2026-06-06 11:08:43 +02:00
tronikos 86ccc59a5f Bump opower to 0.18.3 (#173141) 2026-06-06 08:53:52 +02:00
Luke Lashley 2fce2547c7 Close the connection for disabled Roborock devices (#172277) 2026-06-06 08:19:23 +02:00
Luke Lashley 6b40278d08 Allow using a custom server for Roborock setup. (#171645)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 21:36:39 -07:00
jasonjhofmann 05bb8b94fa Add network MAC connection to AirVisual Pro devices (#173071)
Co-authored-by: jasonjhofmann <16144532+jasonjhofmann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:29:32 +02:00
Joakim Plate 5ac3a8cdde Switch to active scanner for gardena (#173062) 2026-06-06 04:08:10 +02:00
Paulus Schoutsen 266fccf0cf Use SerialPortSelector for DSMR serial port configuration (#171103)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 21:35:07 -04:00
Joakim Plate a1e6a6f9a2 Fix process advertisement for active scans (#173116) 2026-06-05 19:42:45 +02:00
renovate[bot] 2fe406c6ff Update uv to 0.11.17 (#173060) 2026-06-05 19:34:33 +02:00
Paul Bottein e1249fef8f Bump yoto-api to 3.2.0 (#173119) 2026-06-05 19:33:13 +02:00
Michael Hansen 6f61e97f8e Bump voip-utils to 0.4.0 (#173118) 2026-06-05 19:21:49 +02:00
Noah Husby b65751e8ac Bump aiostreammagic to 2.13.2 (#173114) 2026-06-05 19:11:55 +02:00
dependabot[bot] ef4bf77b24 Bump github/gh-aw-actions from 0.77.0 to 0.77.3 (#173073)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 19:10:35 +02:00
Markus Tuominen 977a9ecdd2 Add entity-unique-id pylint quality scale checker (#172815)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-06-05 17:35:48 +02:00
Erik Montnemery 9e79eba970 Give any connected scanner highest priority when deriving person state (#173107) 2026-06-05 17:30:39 +02:00
Martin Hjelmare 40073e598c Fix pylint utcnow checker for dt_util.UTC (#173083) 2026-06-05 16:14:57 +02:00
Erik Montnemery 627d5cc110 Do not use home zone coordinates for person when detected home by scanner (#173042) 2026-06-05 16:05:58 +02:00
Paul Bottein b1dbeca9ed Bump yoto-api to 3.1.6 (#173104) 2026-06-05 15:58:11 +02:00
Robert Resch 059bc8d676 Unify query token auth in http views (#173082) 2026-06-05 15:57:16 +02:00
Erik Montnemery 085f794407 Add test showing zone.async_active_zone prefers zone closest to center (#173099) 2026-06-05 15:27:44 +02:00
Jan Bouwhuis 3996db289d Clean up unused MQTT constants (#173095) 2026-06-05 14:33:14 +02:00
Ronald van der Meer 291585e48e Fix Duco mode end time sensor name (#173045) 2026-06-05 14:23:13 +02:00
Erwin Douna d9a125ce9b Portainer extend timeout for disk space coordinator (#173032) 2026-06-05 14:21:09 +02:00
Erik Montnemery 786c957909 Teach legacy zone condition and trigger about in_zones state attribute (#173074) 2026-06-05 14:04:23 +02:00
Markus Tuominen dd6830f1c5 Add sub-devices for Reolink dual lens cameras with per-lens sensors (#173037) 2026-06-05 14:04:10 +02:00
epenet 4dbe58afc6 Use explicit DOMAIN import in mqtt tests (#173093) 2026-06-05 14:01:28 +02:00
Nikolai Rahimi 6c72d4337d Fix Mitsubishi Comfort devices skipped due to unresolved local address (#172959) 2026-06-05 13:53:13 +02:00
epenet fcff5229d9 Fix incorrect constant usage in mqtt config flow (#172557) 2026-06-05 13:53:10 +02:00
Joost Lekkerkerker 8edd813d4b Bump pySmartThings to 4.0.1 (#173092) 2026-06-05 13:38:32 +02:00
Robert Resch 509866c0eb Bump wheels to 2026.06.0 (#173089) 2026-06-05 13:28:31 +02:00
EnjoyingM 9db5860d6b Wolflink Fix state_class for long term statistics (#173048) 2026-06-05 12:52:06 +02:00
Erwin Douna 6917223cb3 Tado refactor to utilize get_zone_states (#173075) 2026-06-05 12:39:43 +02:00
Jan Bouwhuis cc4637a703 Create certificate files before trying to migrate the MQTT config entry (#173087)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 12:12:34 +02:00
A. Gideonse 2b0d14d71e Bump api-indevolt to 1.8.5 (#173078) 2026-06-05 10:57:08 +02:00
renovate[bot] d0d85d8844 Update ruff (#173059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-05 09:48:36 +02:00
BrettLynch123 eea3d9d4c4 Bump tesla-powerwall to 0.5.3 (#173058) 2026-06-05 09:29:13 +02:00
Erik Montnemery 48a690b267 Derive zone entity state from person in_zones state attribute (#172942) 2026-06-05 08:12:22 +02:00
Michael 07dc2346de Bump py-synologydsm-api to 2.9.0 (#173041) 2026-06-04 22:26:18 +02:00
Erik Montnemery 711830b01f Add tracking_type capability attribute to device tracker (#173027) 2026-06-04 21:19:19 +02:00
Erik Montnemery f9fea56a8c Add tests of legacy device tracker states to person tests (#173023) 2026-06-04 21:12:24 +02:00
Franck Nijhof 8aac0c5b6e Convert LinkPlay configuration_url to string for device registry (#173034) 2026-06-04 20:17:50 +02:00
jdoughty04 227c43630a Add media player missing image coverage (#172641) 2026-06-04 20:09:16 +02:00
fdebrus e2f3a3232e Vistapool: add diagnostics support (#172824)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 20:05:37 +02:00
fdebrus 3173e56bf0 Fix Vistapool button test isolation by deepcopying _LED_DATA. (#172829)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 19:54:44 +02:00
bkobus-bbx e22b03f942 Add support for openSensor and drutexSmart (#169910) 2026-06-04 19:46:31 +02:00
fdebrus 467c2fdd57 Add light platform to Vistapool (#172549)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 19:43:56 +02:00
J. Diego Rodríguez Royo d825b6afa8 Sort Home Connect service.yaml programs (#172848) 2026-06-04 19:43:24 +02:00
J. Diego Rodríguez Royo 69fb1e142c Fix platfoms fixtures return type at Home Connect (#172849) 2026-06-04 19:43:04 +02:00
J. Diego Rodríguez Royo 80e71660e6 Avoid re-registering listeners at common.py from Home Connect (#172851) 2026-06-04 19:42:40 +02:00
Erwin Douna 045ba4e1dd API refactor to replace assert (#172862)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-04 19:31:46 +02:00
David Bonnes 983501406f Deprecate Evohome's refresh_system action (#169894)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 19:25:22 +02:00
Franck Nijhof 837308ba39 Merge branch 'master' into dev 2026-06-04 17:12:57 +00:00
Michael Hansen 6e53787d98 Bump hassil to 3.6.0 (#173031) 2026-06-04 18:51:02 +02:00
G Johansson 7dbce7863a Bump holidays to 0.98 (#173029) 2026-06-04 18:24:53 +02:00
Lukas e1d90fd244 Add source selection to samsung_infrared media player (#172794) 2026-06-04 18:19:59 +02:00
fdebrus bbeb2ac667 Vistapool: Add reconfiguration flow (#172836)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 17:55:08 +02:00
Jan Bouwhuis 21260bf1ab Fix value template in MQTT Fan and Siren subentry setup (#172980) 2026-06-04 17:53:06 +02:00
Marcello a0d67b80ab Fix offline devices in Fluss (#172833) 2026-06-04 17:39:34 +02:00
bkobus-bbx a6b7641d47 Add diagnostics for Blebox integration (#172556)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 17:35:27 +02:00
bkobus-bbx ad2db2ae88 Add exception translations for Blebox integration (#172560) 2026-06-04 17:22:33 +02:00
Raphael Hehl fc2b7902a5 Bump av to 17.0.1 (#172892) 2026-06-04 17:02:29 +02:00
bkobus-bbx 3aa4cbeeb0 Add icon translations for Blebox integration (#172565) 2026-06-04 16:58:51 +02:00
Yardian Support 3c2f171158 Bump pyyardian to 1.4.0 (#173020)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 16:54:51 +02:00
Erik Montnemery ffc6eeadc2 Always include template errors in trace (#172917) 2026-06-04 16:28:05 +02:00
mbo18 5cc1a0a1ef Add Motionblinds virtual integration Avosdim (#172821) 2026-06-04 16:22:06 +02:00
Erik Montnemery 1cbbce5b35 Fix person in_zones propagation from scanner in home zone (#173007) 2026-06-04 15:50:06 +02:00
Anatosun 9f5cb635f0 Upgrade Swisscom integration (#171816)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 14:54:54 +02:00
epenet 50de2c070e Use DOMAIN constant in test (hass.states.async_entity_ids) (#173009) 2026-06-04 14:01:51 +02:00
Samuel Xiao a7f012350f Switchbot Cloud: Fixed an issue where condition filtering for enabled Webhooks was abnormal (#172903) 2026-06-04 13:46:57 +02:00
Yardian Support 04d2211d1e Refactor Yardian zones into sub-devices using via_device (#172835) 2026-06-04 13:36:38 +02:00
Markus Adrario 5b4c2c6017 Homee: Add stop_tilt action for covers (#172952) 2026-06-04 13:19:09 +02:00
Joost Lekkerkerker f36a491ebd Fix double annotations for Pylint (#172477) 2026-06-04 13:18:33 +02:00
Ermanno Baschiera bde3b6e59f Add filter reset button to Helty Flow (#172866)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:15:31 +02:00
kohai-ut 53211bc37a Add tests for the envisalink integration (#172621)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:13:29 +02:00
Eric Stern 863655dce5 Fix SleepIQ 401 storm by isolating client session cookies (#172276) 2026-06-04 13:12:34 +02:00
epenet eb2a3d0ffd Prefer DOMAIN constant over config_flow.DOMAIN in tests (#172992) 2026-06-04 12:43:08 +02:00
epenet cbc7a5ae8e Use DOMAIN constant in tests (hass.services.async_call) (#172998) 2026-06-04 12:36:10 +02:00
epenet 90d45432dc Use DOMAIN constant in tests (async_mock_service) (#173002) 2026-06-04 12:35:54 +02:00
epenet 9e1abe6dfd Remove duplicate description in Tuya binary sensor (#173006) 2026-06-04 12:35:09 +02:00
epenet 682f0ba217 Use DOMAIN constant in test (config_entries.flow.async_init) (#173008) 2026-06-04 12:33:44 +02:00
Tomasz Dylewski 2b499d5aca Updated pajgps-api to version 0.4.0 (#172986) 2026-06-04 12:27:01 +02:00
Maciej Bieniek 11535f27da Bump imgw_pib to 2.2.2 (#172999) 2026-06-04 12:26:23 +02:00
Erik Montnemery f5fa2e244d Improve person tests (#172997) 2026-06-04 11:46:06 +02:00
epenet 1a63a38234 Add hass.states.async_entity_ids to domain constant checker (#172923) 2026-06-04 10:33:16 +02:00
J. Nick Koston ccb0b6a286 Bump aiodiscover to 3.3.1 (#172882) 2026-06-04 10:28:34 +02:00
bk86a 5e8f0d1078 Fix Lyric sensor crash when next_period_time is None (#167831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:19:49 +02:00
epenet af6b0879de Use DOMAIN constant in MockConfigEntry (p-z) (#172991) 2026-06-04 10:19:05 +02:00
epenet 6450e2790c Use DOMAIN constant in MockConfigEntry (a-o) (#172989) 2026-06-04 10:18:51 +02:00
Abílio Costa 8aafebc5a6 Bump idasen-ha to 2.7.0 (#172962) 2026-06-04 10:15:17 +02:00
Ronald van der Meer e9c3a65e58 Bump python-duco-connectivity to 0.6.0 (#172938) 2026-06-04 09:45:56 +02:00
Jan Čermák a30ab26dda Bump aiohasupervisor to 0.5.0 (#172933) 2026-06-04 09:38:58 +02:00
dependabot[bot] aa3ae4986b Bump actions/ai-inference from 2.1.0 to 2.1.1 (#172966)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 09:10:08 +02:00
dependabot[bot] 3235e6d458 Bump github/gh-aw-actions from 0.76.0 to 0.77.0 (#172979)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 09:05:09 +02:00
Erik Montnemery d8975b3d0d Improve test of zone entity state (#172941)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-04 09:03:57 +02:00
BrettLynch123 4261787be1 Add operation mode sensor to Powerwall (#172967) 2026-06-04 08:42:24 +02:00
renovate[bot] 2e8097cc1f Update infrared-protocols to 6.0.0 (#172968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-04 05:01:55 +02:00
Abílio Costa 9430bb2e32 Add Edifier Infrared integration (#172342)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-03 22:50:35 +01:00
J. Nick Koston 1283420fc6 Bump onvif-zeep-async to 4.2.0 (#172957) 2026-06-04 00:49:14 +03:00
starkillerOG 88e85e4325 Add more Reolink diagnostic info (#172945) 2026-06-03 23:34:18 +02:00
Erwin Douna 7cb08fdabc Incomfort refactor coordinator (#160953)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-03 23:31:18 +02:00
Tom 0cde867f93 Bump airOS to add insecure ssl detection (#172947) 2026-06-03 22:24:09 +02:00
J. Diego Rodríguez Royo 149daf4f97 Bump aiohomeconnect to 0.36.1 (#172946) 2026-06-03 22:13:25 +02:00
Thomas55555 6ffc32159b Bump aioautomower to 2.7.6 (#172937) 2026-06-03 22:00:04 +02:00
Ronald van der Meer 1c2d1013e6 Refactor Duco config flow tests to use small helpers (#172498) 2026-06-03 21:57:12 +02:00
Markus Adrario af4eaed5ed Homee: Use constants for cover states for readability (#172840) 2026-06-03 21:34:56 +02:00
Erik Montnemery dba09c334a Use zone DOMAIN constant in zone conditions (#172940) 2026-06-03 20:44:35 +02:00
Erik Montnemery c329bb4000 Don't log configuration errors when executing WS subscribe_trigger (#172918) 2026-06-03 19:58:32 +02:00
Josef Zweck 2e041dd45f Add reason for unvailability to opendisplay (#172909) 2026-06-03 19:48:56 +02:00
Franck Nijhof 836740c247 2026.6.0 (#172932) 2026-06-03 19:43:19 +02:00
rjones-gentex 8ba3e6c8c1 Upgrade HomeLink package, set integration type (#172371) 2026-06-03 19:43:17 +02:00
Chris 8db064c929 Add binary sensor platform to openevse (#172924) 2026-06-03 19:37:55 +02:00
Kurt Chrisford 8124544125 Bump actron-neo-api to 0.5.12 (#172902) 2026-06-03 19:15:09 +02:00
Mark 6291179292 Add Rabbit Air fan preset icons (#172931) 2026-06-03 19:00:45 +02:00
Joost Lekkerkerker 6f880ac8a9 Remove state attributes from OPNsense (#172930) 2026-06-03 18:29:14 +02:00
Paulus Schoutsen 7babb2423b Migrate itach to pyitachip2ir2==0.0.8 (#172908)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 18:16:42 +02:00
Rasmus Graham 7e1874ae96 Bump vsure to 2.7.0 (#172856) 2026-06-03 17:08:34 +01:00
starkillerOG 1f50582a16 Bump reolink_aio to 0.20.1 (#172927) 2026-06-03 18:04:56 +02:00
Markus Tuominen 53211759cb Document missing pylint rules in plugin README (#172925) 2026-06-03 18:54:20 +03:00
Abílio Costa 4593059db2 Add "review" claude skill and use it in "gitbhub-pr-review" (#172797)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-03 15:57:21 +01:00
epenet 593ae9eb80 Add pylint plugin for correct use of DOMAIN constants in tests (#172693)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-03 16:53:15 +02:00
Erik Montnemery 37b4bcaa39 Don't log condition errors when executing WS test_condition (#172897) 2026-06-03 16:11:54 +02:00
Bram Kragten 6bda3ea3a5 Update frontend to 20260527.4 (#172907) 2026-06-03 14:17:30 +02:00
Sören f4db5fb346 Add Avea Bluetooth reachability diagnostics (#172898) 2026-06-03 13:43:33 +02:00
Heikki Henriksen f04b0ee2c6 prusalink: guard non-string original in config_flow workaround (#172375) 2026-06-03 11:47:19 +02:00
Erik Montnemery 52c3e17de9 Add zone occupancy conditions (#172896) 2026-06-03 11:20:13 +02:00
Imou-OpenPlatform 96c286f2e0 Add Imou integration (#161412)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-03 11:18:31 +02:00
renovate[bot] 3e356de4e1 Update pytest-asyncio to 1.4.0 (#172886) 2026-06-03 11:17:32 +02:00
Erik Montnemery 90a874d81b Use zone DOMAIN constant in zone triggers (#172894) 2026-06-03 11:15:50 +02:00
Erik Montnemery 165024c6c9 Catch errors when setting up condition in WS subscribe_condition (#172895) 2026-06-03 11:06:18 +02:00
Erik Montnemery 66e4db3c0e Add zone conditions in / not in zone (#172810) 2026-06-03 10:40:43 +02:00
Franck Nijhof 0e6128c657 Fix SwitchBot Blind Tilt KeyError on idle BLE advertisements (#172816) 2026-06-03 10:37:16 +02:00
Wendelin 16febb36ba Automation choose: Add optional note to options (#172837) 2026-06-03 10:12:52 +02:00
Erik Montnemery dd7bd0c8a4 Prevent log spam when WS subscribe_condition is active (#172832) 2026-06-03 10:10:30 +02:00
Petro31 c462a1c188 Add translations for template device trackers in_zones option (#172850) 2026-06-03 08:38:18 +02:00
fdebrus 96c5110b7e Vistapool: flip docs-related quality-scale rules to done (#172827)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 08:04:52 +02:00
Colin 64e8ed2737 Add missing translation keys to openevse (#172802) 2026-06-03 08:03:30 +02:00
renovate[bot] 4171d092f7 Update coverage to 7.14.1 (#172878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-03 07:59:49 +02:00
J. Nick Koston 7af867ad4d Avoid double-decoding websocket_api TEXT frames with decode_text (#172891) 2026-06-03 07:52:02 +02:00
Paulus Schoutsen 907fe40304 Regenerate mdi_icons.py for frontend 20260527.3 (#172887) 2026-06-03 05:56:21 +02:00
Petro31 261914c592 Use dt_util.utcnow() instead of datetime.now(UTC) in template tests (#172852) 2026-06-03 05:32:12 +02:00
David Bonnes 09637c1a3a Use dt_util.utcnow() instead of datetime.now(UTC) in evohome (#172868) 2026-06-03 05:31:07 +02:00
Bram Kragten 0816385185 Bump frontend to 20260527.3 (#172873) 2026-06-03 05:20:42 +02:00
renovate[bot] 4b64b26870 Update infrared-protocols to 5.8.1 (#172870) 2026-06-02 22:43:22 +01:00
Joost Lekkerkerker b20f9ad40a Bump pySmartThings to 4.0.0 (#172858) 2026-06-02 22:36:15 +02:00
Ronald van der Meer 99d279bdd8 Simplify Duco sensor tests (#172501) 2026-06-02 22:25:34 +02:00
jameson_uk 69e0e11077 Bump aioamazondevices to 14.0.0 (#172857) 2026-06-02 20:45:55 +01:00
Pete Sage 9e3c143bd0 Log warning on unsupported announce media formats for Sonos (#172614)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-02 21:27:54 +02:00
Rayman223 45c55543e9 Add EcoSmart resume schedule button to Wallbox (#171847)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:15:40 +02:00
Chris Caron fb02e93a0c Bump version of Apprise to v1.11.0 (#172622) 2026-06-02 21:10:15 +02:00
Marc Hörsken a54b97eeca Bump pywmspro to 0.4.0 for persistence support (#172193) 2026-06-02 21:09:32 +02:00
J. Nick Koston 61c196405b Bump aiohttp to 3.14.0 (#172838) 2026-06-02 21:05:57 +02:00
Tom Schneider 9a047ad115 Type hvv_departures integration (#172595) 2026-06-02 20:55:38 +02:00
Daniel Bergmann 07a584057c Add integration for the device Envertech EVT800 (#149456)
Co-authored-by: Dani <danigta2020@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-02 19:04:26 +02:00
Michael Hansen 5873dff1d9 Bump intents to 2026.6.1 (#172842) 2026-06-02 11:31:08 -05:00
Denys Karabetskyi 30a2bd9b92 Add button event entity to SwitchBot Contact Sensor. (#171876) 2026-06-02 17:48:58 +02:00
fdebrus 1065dce882 Add number platform to Vistapool (#172542)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-02 17:13:11 +02:00
johanzander 878a39194a Promote growatt_server to Gold quality scale (#171623)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-02 13:52:57 +02:00
Tom Schneider 2e2f4a7dcb Bump pygti to 1.1.1 (#172613) 2026-06-02 13:50:16 +02:00
Denis Shulyaka 46627984f8 Use homeassistant.util.dt.utcnow instead of datetime.now(UTC) in Anthropic (#172826) 2026-06-02 13:48:20 +02:00
Tomer 5445f9e42b Bump victron-mqtt to 2026.6.1 (#172676) 2026-06-02 13:33:02 +02:00
Ermanno Baschiera 8ce2a5257d Add Helty Flow temperature and humidity sensors (#172813)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:27:05 +02:00
bkobus-bbx 787828d7de Add reconfiguration flow for Blebox integration (#172569) 2026-06-02 13:26:38 +02:00
zhangluofeng 9e96912a1e Add xthings cloud switch (#172119)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-02 13:20:43 +02:00
zhangluofeng fd578cfd4c Don't create switch entity for switch device type in XThings Cloud (#172828) 2026-06-02 13:05:00 +02:00
Simon Lamon 94de8646c6 Modify stale policies for PRs and issues (#172812) 2026-06-02 12:52:49 +02:00
Sean Dague 2d19e84d15 Use arwn-client library in arwn (#172264) 2026-06-02 12:32:16 +02:00
Erik Montnemery a17cfbc2a5 Make the renamed trigger behavior options backwards compatible (#172822) 2026-06-02 12:31:57 +02:00
fdebrus c552b0a067 Vistapool: add DHCP discovery on SugarWIFI hostname (#172820)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 12:10:48 +02:00
Samuel Xiao 80241a44d9 Switchbot Cloud: Enable Webhook for sensor devices (#172814) 2026-06-02 11:02:08 +02:00
fdebrus d8b02ea6d6 Add button platform to Vistapool (#172550)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 10:21:16 +02:00
jameson_uk 36d2e85351 alexa devices - media player code quality (#172650) 2026-06-02 10:04:20 +02:00
epenet 174ac9eafe Deprecate single-use CONCENTRATION_PARTS_PER_CUBIC_METER constant (#172553) 2026-06-02 09:42:33 +02:00
Erik Montnemery 772c426d5d Add zone triggers occupancy detected/cleared (#172438) 2026-06-02 09:34:12 +02:00
Matthias Alphart a32d028e3d Update knx-frontend to 2026.6.1.213802 (#172806) 2026-06-02 08:04:20 +02:00
renovate[bot] bc66c2610e Update infrared-protocols to 5.8.0 (#172804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 05:44:29 +02:00
johanzander c22823ff8d Use growattServer library error code constants in growatt_server (#172771) 2026-06-02 04:54:27 +02:00
Pete Sage a21212ab7e Send midpoint of fan range for mapping devices for Z-Wave (#172562) 2026-06-02 00:27:50 +02:00
johanzander 71eefdc716 Migrate async_migrate_entry test calls to async_setup in growatt_server (#172587)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-01 23:12:43 +02:00
J. Nick Koston f5819d400e Explain why a Husqvarna Automower BLE device could not be connected to (#172774) 2026-06-01 22:57:19 +02:00
J. Nick Koston 31fcbe7bce Explain why an LD2410 BLE device could not be found (#172779) 2026-06-01 22:54:46 +02:00
J. Nick Koston 3664eb4942 Explain why a Snooz device could not be found (#172780) 2026-06-01 22:54:11 +02:00
J. Nick Koston 2f03b7c427 Explain why an eQ-3 Bluetooth device could not be found (#172770) 2026-06-01 22:53:15 +02:00
Bram Kragten 2d8cebf99d Bump frontend to 20260527.2 (#172759)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-01 22:52:30 +02:00
Franck Nijhof 8ca4471418 Cancel iCloud polling timer on config entry unload (#172793) 2026-06-01 22:45:58 +02:00
dependabot[bot] 02720605ae Bump dessant/lock-threads from 6.0.1 to 6.0.2 (#172776)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 22:23:33 +02:00
Mick Vleeshouwer fb28825f1f Add tests for Overkiz siren platform (#171900) 2026-06-01 22:23:28 +02:00
dependabot[bot] 25ce81732b Bump github/gh-aw-actions from 0.75.0 to 0.76.0 (#172777)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 22:23:04 +02:00
Chris 9c1cc55482 Add OpenEVSE diagnostics (#171762) 2026-06-01 22:22:11 +02:00
Ermanno Baschiera 477756da5b Add Helty Flow integration (#172736) 2026-06-01 22:20:22 +02:00
Tom ec995a3472 Fix ProxmoxVE missing unused token data (#172782) 2026-06-01 22:10:43 +02:00
Maciej Bieniek a19f3045e7 Remove battery_level property from Tractive device tracker (#172756) 2026-06-01 21:57:42 +02:00
Mick Vleeshouwer 6a836bd1d9 Add tests for Overkiz select platform (#171899) 2026-06-01 21:53:45 +02:00
bkobus-bbx 01d390293b Refactor blebox integration to use DataUpdateCoordinator (#172533) 2026-06-01 21:44:21 +02:00
J. Nick Koston b069bc2f03 Bump habluetooth to 6.8.1 (#172768) 2026-06-01 14:41:55 -05:00
Ingo Fischer 7e36d265ed Filter stale replayed BLE advertisements in Matter BLE proxy (#172773)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:23:41 -05:00
Thijs W. 155cb38090 Fix get_play_status function call in frontier silicon (#172705) 2026-06-01 21:21:26 +02:00
J. Nick Koston 0f01148207 Explain why a Yale Access Bluetooth device could not be found (#172761) 2026-06-01 20:14:52 +01:00
J. Nick Koston a65503f203 Explain why an Airthings BLE device could not be found (#172758) 2026-06-01 20:13:12 +01:00
J. Nick Koston 063fa8df7e Explain why an INKBIRD device could not be found (#172762) 2026-06-01 20:12:49 +01:00
J. Nick Koston 1865c16041 Explain why a LED BLE device could not be found (#172764) 2026-06-01 20:11:09 +01:00
epenet cad177cdff Rename constant in reload helper test (#172711) 2026-06-01 20:46:42 +02:00
Yardian Support be2aaf926b Support Yardian YC models (#172347)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 20:40:05 +02:00
A. Gideonse d7219aa025 Fix binary sensor defaults for Indevolt (#172714) 2026-06-01 20:39:30 +02:00
Marcello 7fb475aad1 Add cover platform to Fluss (#169908)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 20:30:18 +02:00
jameson_uk 25f18c6082 media_player platform fixes for Alexa Devices (#172611) 2026-06-01 20:03:02 +02:00
fdebrus c901160fb3 Bump aioaquarite to 0.5.1 (#172754)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-01 19:44:53 +02:00
mellowism 018e42e670 Add custom themes to Cloud support package (#172708)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:40:44 +02:00
Michael 04442bb73e Use proper user-agent to fetch feeds (#172655) 2026-06-01 19:39:28 +02:00
Simon Lamon eb3fd52619 Add actions permission to delete stalebot state (#172704) 2026-06-01 19:39:06 +02:00
markvp 8e19fd280e Add Thread and Wi-Fi RSSI diagnostic sensors to Matter integration (#167853)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 19:31:15 +02:00
Paul Bottein e45f64b880 Add media browser to Yoto (#172325) 2026-06-01 19:27:20 +02:00
Perry Naseck 480a8d536f upb: Move to SerialPortSelector (#170053) 2026-06-01 19:01:48 +02:00
Paulus Schoutsen 0abc9b787b Ignore Beacons security policy flag in Thread dataset comparison (#172749)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-01 18:38:47 +02:00
WardZhou ce46be110d Add support for Thread Integration to Display Icons for Yeelight TBRs and Fix for Amazon Echo (#169384)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 18:38:27 +02:00
Mick Vleeshouwer c22f10bf87 Run Overkiz unique ID migration only once via config entry version (#172670)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 17:43:37 +02:00
Paul Bottein 97d9c23855 Bump yoto-api to 3.1.5 (#172753) 2026-06-01 17:38:57 +02:00
epenet 477d8bde6b Use Platform enum in reload helper (#172729) 2026-06-01 16:52:26 +02:00
Franck Nijhof da12c94f27 Skip Overkiz events for unknown device URLs (#172712) 2026-06-01 16:49:56 +02:00
Franck Nijhof 52afa0627e Return 404 instead of 500 when media player artwork is unavailable (#172700) 2026-06-01 16:47:36 +02:00
Markus Adrario 9b8b8c2d82 Homee: Exclude covers, that don't provide a closed state. (#172476) 2026-06-01 16:46:34 +02:00
renovate[bot] db0fc36a54 Update SQLAlchemy to 2.0.50 (#172685) 2026-06-01 16:11:43 +02:00
Abílio Costa cb00ee1503 Skip reauth flow on ConfigEntryAuthFailed when integration has none (#172483) 2026-06-01 15:09:35 +01:00
AlCalzone ae23c5db4d Use DataUpdateCoordinator in openSenseMap (#172713)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:04:17 +02:00
Pete Sage d0b4274c2b Replace usages of datetime.now(UTC) with dt_util for Sonos (#172737) 2026-06-01 16:02:56 +02:00
Maciej Bieniek ecd132b60c Translate the name of the Tractive tracker (#172747) 2026-06-01 16:02:22 +02:00
Maciej Bieniek 58d5db7494 Add missing _attr_name = None for Tractive device tracker (#172746) 2026-06-01 16:01:32 +02:00
Zach Wolf 3c0a34cc66 Bump python-roborock to 5.14.1 and revert defensive aiohttp catch (#172745)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:01:24 +02:00
jameson_uk de70e86eae Bump aioamazondevices to 13.8.2 (#172748) 2026-06-01 15:43:46 +02:00
Franck Nijhof 9d5bd5daff Sanitize surrogate characters in MeteoAlarm alert attributes (#172545) 2026-06-01 14:58:46 +02:00
Mick Vleeshouwer c314ee77e1 Retry transient Overkiz server unavailable errors (#172672) 2026-06-01 14:56:38 +02:00
dependabot[bot] 2d262d940b Bump github/gh-aw-actions from 0.74.9 to 0.75.0 (#172530)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 14:56:32 +02:00
Joost Lekkerkerker 425ce17d9c Enable RUF002 and RUF003 (#172739) 2026-06-01 14:52:42 +02:00
epenet 38a266ea6c Cleanup incorrect use of Platform enum in miele (#172699) 2026-06-01 14:48:54 +02:00
Joost Lekkerkerker 55af1c3b3c Enable RUF009 (#172738) 2026-06-01 14:17:00 +02:00
Joost Lekkerkerker 7f1dce45c5 Enable RUF061 (#172735) 2026-06-01 14:03:28 +02:00
tlpeter 62bfaa9d92 Bump renault-api to 0.5.11 (#172333)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-06-01 13:33:33 +02:00
Jan Bouwhuis 088fd398e2 Fix MQTT device_tracker logging attributes order (#172732) 2026-06-01 13:29:30 +02:00
Joost Lekkerkerker 519104166d Invert RUF Ruff rules (#172731) 2026-06-01 13:24:51 +02:00
Joost Lekkerkerker 25f64eb78c Invert B Ruff rules (#172730) 2026-06-01 13:07:36 +02:00
Franck Nijhof 3c0f6b7f2a Fix Jellyfin media source crash when entry is not loaded (#172437) 2026-06-01 13:06:10 +02:00
Franck Nijhof 94a976d974 Convert set_id to int in LG TV RS-232 config flow (#172701) 2026-06-01 12:58:26 +02:00
Franck Nijhof e377e9889a Handle malformed response errors in Denon AVR error wrapper (#172502) 2026-06-01 12:58:03 +02:00
Franck Nijhof cc897b926d Fix Overkiz UnoIO cover reporting wrong movement direction (#172506) 2026-06-01 12:38:02 +02:00
Franck Nijhof 6c04ca3685 Add missing Flexit BACnet transient operation modes to preset map (#172493) 2026-06-01 12:32:39 +02:00
Michael e03bc6faa5 Add extra device info to FRITZ!Box Tools diagnostics (#172647) 2026-06-01 12:19:09 +02:00
Martin Hjelmare fed946760d Add pylint plugin to enforce util.dt.utcnow (#172354) 2026-06-01 12:17:29 +02:00
jameson_uk 7a32cdc250 Improve http2 task handling for Alexa Devices (#172649) 2026-06-01 12:11:29 +02:00
Denis Shulyaka 6bb027f008 Fix ai_task camera snapshot mime type (#172682) 2026-06-01 12:06:04 +02:00
Franck Nijhof 460c67e9c5 Handle missing notAfter field in cert_expiry certificate data (#172503)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 11:42:46 +02:00
J. Nick Koston 33a51acd7b Explain why a Switchbot device could not be found (#172581) 2026-06-01 11:42:00 +02:00
Franck Nijhof 3df68f2088 Fix ephember crash when zone mode is None (#172504) 2026-06-01 11:32:01 +02:00
renovate[bot] 35e647de20 Update rf-protocols to 4.0.1 (#172597) 2026-06-01 11:06:35 +02:00
Franck Nijhof f5aed4b61e Raise errors instead of swallowing exceptions in Toon action handlers (#172511) 2026-06-01 11:05:07 +02:00
A. Gideonse c5a3a50d7b Bump indevolt-api to 1.8.3 (#172683) 2026-06-01 11:01:07 +02:00
epenet d256226e46 Adjust Renault configuration keys (#172694) 2026-06-01 10:58:55 +02:00
Simone Chemelli b537978260 Fix Shelly sensor restore when not initialized (#172441) 2026-06-01 10:57:09 +02:00
epenet e3cd4cdd37 Cleanup incorrect use of Platform enum in myuplink (#172697) 2026-06-01 10:54:19 +02:00
Yardian Support b5997c2e9e Fix Yardian water hammer diagnostic sensor name (#172698) 2026-06-01 10:50:52 +02:00
Mike Degatano defe00dd92 During onboarding, ensure Supervisor is up to date during hassio setup (#171129)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 10:34:16 +02:00
David Knowles 2fdad3dc41 Schlage: use lock connected status as availability signal (#172638)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 10:27:28 +02:00
A. Gideonse c86cb9281d Bugfix: Gen-1 Inverter sensor for Indevolt to display "N/A" when turned off (#172559) 2026-06-01 10:23:59 +02:00
epenet ebd8d0b9c9 Cleanup incorrect use of Platform enum in zimi (#172696) 2026-06-01 09:35:16 +02:00
TomFilsell 2a38d165d6 Add tests to cert_expiry (#171051)
Co-authored-by: FIls0010 <a1867444@adelaide.edu.au>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-01 08:56:23 +02:00
Franck Nijhof 8c569b4aa3 Fix Growatt setup failure on API rate limit (#172472) 2026-06-01 08:54:13 +02:00
MoonDevLT bea942fbaa Improve the zeroconf discovery card title of lunatone (#172356) 2026-06-01 08:53:01 +02:00
epenet 421d3b0835 Rename izone constant (#172689) 2026-06-01 08:40:55 +02:00
Josef Zweck adfb33489a Add connectivity binary sensor to tedee (#172688)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 08:18:13 +02:00
Mick Vleeshouwer 6b2c7423e4 Add tests for Overkiz lock platform (#172678) 2026-06-01 08:09:57 +02:00
Josef Zweck 67059e64e0 Fix tedee entity availability (#172667)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 08:05:26 +02:00
Manu 6ab4d8933d Bump pyrate-limiter to 4.2.0 (#172686) 2026-06-01 07:52:48 +02:00
Simone Chemelli 3d8369db81 Remove redundant definitions in Alexa Devices (#172488) 2026-06-01 06:59:54 +02:00
epenet f459946e86 Rename domain variable in tests (#172575) 2026-06-01 06:37:40 +02:00
Jordan Harvey 0c7c3e9fc7 Bump pynintendoparental to 2.4.0 (#172666) 2026-06-01 03:04:17 +02:00
Dawid Wróbel 93615edc60 Increase Proxmox API connection timeout to 30s (#172664) 2026-05-31 21:38:43 +02:00
Michael fdb581ea7f Add missing exception translation keys in Ecovacs (#172658) 2026-05-31 17:35:03 +02:00
Mick Vleeshouwer 30f03dc01e Fix sentence-casing of Overkiz energy demand status binary sensor (#172653) 2026-05-31 13:48:09 +02:00
renovate[bot] 4f92c1686b Update pytest-socket to 0.8.0 (#172516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-31 13:40:59 +02:00
Jan Bouwhuis a676072e0d Fix MQTT device_tracker not saving state on location accuracy changes (#172629) 2026-05-31 12:03:12 +02:00
epenet 0ebcbf33ba Bump tuya-device-handlers to 0.0.22 (#172648) 2026-05-31 11:57:11 +02:00
Kamil Breguła cb544f2f67 Refresh WLED firmware releases on manual entity update (#172517)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-31 11:55:30 +02:00
TheJulianJES 26c5c37f53 Bump ZHA to 1.4.1 (#172640) 2026-05-31 11:22:47 +02:00
epenet b9ed8e91df Cleanup incorrect import path in Tuya coordinator (#172330) 2026-05-31 11:10:22 +02:00
alexborro 33a721245c Catch network errors during Loqed config entry unload (#172617) 2026-05-31 11:04:40 +02:00
Sören 840243db9c Improve Avea Bluetooth discovery flow (#172623) 2026-05-30 09:49:36 -05:00
Avi Miller 740778f00b fix: bump aiolifx and aiolifx-themes (#172619)
Signed-off-by: Avi Miller <me@dje.li>
2026-05-30 16:56:20 +03:00
J. Nick Koston 1ec5e25b6b Fix ESPHome update entity stuck on for project versions with build suffix (#172571) 2026-05-30 08:50:26 -05:00
J. Nick Koston 83c35b8b4d Bump pyroute2 to 0.9.6 (#172521) 2026-05-30 08:50:16 -05:00
J. Nick Koston 02b760f142 Expose bluetooth address reachability diagnostics API (#172578) 2026-05-30 08:49:56 -05:00
Erwin Douna 0c10c2c16b Proxmox refactor config flow to support no nodes (#172615) 2026-05-30 15:45:45 +02:00
Michael 144257a377 Show error about missing api permissions while browsing Immich media (#172609) 2026-05-30 11:57:37 +02:00
epenet c5341b2ff6 Fix incorrect use of Platform enum in component tests (#172574) 2026-05-30 11:31:42 +02:00
J. Nick Koston 6aebf78961 Bump yalexs to 9.2.7 (#172582) 2026-05-30 11:53:00 +03:00
On Freund 759039728b Bump pyrisco to 0.8.0 (#172591) 2026-05-30 10:35:43 +02:00
J. Nick Koston 1d2f0793d7 Bump habluetooth to 6.8.0 (#172577) 2026-05-29 18:11:51 +02:00
epenet 14fcb6c2d6 Import notify domain in notify tests (#172572) 2026-05-29 18:10:59 +02:00
Jan Bouwhuis 5763829b4b Silent migrate MQTT protocol version to version 5 if the broker supports it or raise an issue (#172500)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-29 17:33:11 +02:00
dependabot[bot] 7dfec6ef3d Bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#172526) 2026-05-29 16:22:05 +02:00
dependabot[bot] efe55f247a Bump docker/metadata-action from 6.0.0 to 6.1.0 (#172528) 2026-05-29 16:20:53 +02:00
epenet 85f3141776 Fix CI failure due to missing ssdp patching in braviatv (#172561) 2026-05-29 14:18:08 +02:00
Michael a175c7c4be Handle FileNotFoundError in Immich upload_file action (#172490) 2026-05-29 13:22:26 +02:00
Zach Wolf 03c83091ab Catch network errors during Roborock config entry setup (#172492)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:21:01 +02:00
mhuiskes accebd7f38 Remove diagnostic category and dead translation key from pac sensor (#172548) 2026-05-29 12:51:17 +02:00
epenet 9d3bb346e9 Refactor Renault to use StrEnum (#172546) 2026-05-29 11:42:04 +02:00
mhuiskes d13721980e Fix zeversolar coordinator to raise UpdateFailed on errors (#170507) 2026-05-29 11:26:27 +02:00
Franck Nijhof ac6b5a5850 Add missing ssdp dependency to BraviaTV manifest (#172536) 2026-05-29 11:17:36 +02:00
Franck Nijhof 16dfa99673 Use state-based icon for Hue grouped light (#172535) 2026-05-29 11:17:00 +02:00
Franck Nijhof f51a02bbda Fix Volvo lock crash when API field is missing from coordinator data (#172465) 2026-05-29 10:50:55 +02:00
Paul Bottein 6a51b21242 Fix Yoto OAuth flow with cloud credentials (#172544)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-29 10:30:52 +02:00
dependabot[bot] 5eb502851c Bump docker/login-action from 4.1.0 to 4.2.0 (#172531) 2026-05-29 08:54:25 +02:00
dependabot[bot] ef20418c76 Bump github/codeql-action from 4.35.5 to 4.36.0 (#172529) 2026-05-29 08:53:42 +02:00
Erwin Douna 94ca34fd0c Portainer refactor services test (#172525) 2026-05-29 08:21:09 +02:00
Franck Nijhof 8634c22a53 Guard Shelly repairs checks for uninitialized RPC devices (#172509) 2026-05-29 09:12:25 +03:00
Brett Adams 5681ba40f1 Move Teslemetry destination name from device tracker to a sensor (#172514)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 07:56:32 +02:00
Brett Adams 8a9a1c5fed Move Tesla Fleet route destination from device tracker to a sensor (#172513)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 07:55:44 +02:00
Franck Nijhof c587e101af Reduce Wyoming satellite disconnect log to debug level (#172499) 2026-05-28 19:18:14 -05:00
Franck Nijhof 6eeeac46f3 Convert Roomba hw_version to string for device registry (#172497) 2026-05-28 23:13:08 +02:00
Franck Nijhof 86542b8ad0 Increase ConfigEntryNotReady retry backoff cap from 80s to 10 minutes (#172487) 2026-05-28 22:41:54 +02:00
Franck Nijhof 7e07e7062c Add prog operating mode to Overkiz Atlantic heater HVAC mapping (#172491) 2026-05-28 22:21:53 +02:00
Franck Nijhof d7c13fee27 Fix Tado config flow crash on device activation polling (#172486) 2026-05-28 22:06:24 +02:00
Ronald van der Meer a0a44f7a25 Refactor Duco tests to use shared fixtures (#172351) 2026-05-28 22:04:25 +02:00
Mike Degatano 2bba907013 Migrate analytics integration to config entry setup (#171801)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 20:42:25 +02:00
Crocmagnon 0dcb8fc507 ovhcloud_ai_endpoints: update quality scale to silver (#172440) 2026-05-28 20:40:41 +02:00
Jan Bouwhuis 18e6f67650 Move MQTT protocol setting to main options (#172482) 2026-05-28 20:36:39 +02:00
Joost Lekkerkerker e5fad17e17 Add pylint rule for checking async_migrate_entry calls in tests (#171877) 2026-05-28 20:22:41 +02:00
Boris Obmoroshev 219b9cbcaa Add regression test for ONVIF setup against a real ONVIFDevice (#172194)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 19:18:24 +01:00
Franck Nijhof 309b26f809 Handle DAVError in CalDAV get_supported_components (#172479) 2026-05-28 19:53:20 +02:00
Bram Kragten e78cb0114d Bump frontend to 20260527.1 (#172462)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-05-28 19:52:47 +02:00
Crocmagnon 06a4247078 ovhcloud_ai_endpoints: increase test coverage (#172439) 2026-05-28 19:48:08 +02:00
Daniel Feinberg 181e21dd2c Fix apple_tv HomePod streaming failures when device is idle (#170033)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:47:32 +02:00
Crocmagnon 31354d4129 ovhcloud_ai_endpoints: add diagnostics (#172444) 2026-05-28 19:42:49 +02:00
Simone Chemelli 57308d7760 Discard old events for Alexa Devices (#172446) 2026-05-28 19:42:19 +02:00
Joost Lekkerkerker c07fed05df Add pylint rule for checking async_setup_entry calls in tests (#171864) 2026-05-28 19:28:29 +02:00
jtjart 13ef737873 Add projector as media player device class (#169274) 2026-05-28 19:27:21 +02:00
TheJulianJES 0a1510135c Fix Matter BLE proxy blocking startup (#172456) 2026-05-28 19:25:36 +02:00
Simone Chemelli 6f6b7888cd Bump samsungtvws to 3.0.5 (#172471) 2026-05-28 19:02:30 +02:00
Paul Bottein b9173e36fb Name the Broadlink RF transmitter entity (#172468) 2026-05-28 19:02:14 +02:00
Ronald van der Meer a65ca9c86b Fix Duco regression where entities become unavailable when LAN info fetch fails (#172448) 2026-05-28 19:00:43 +02:00
Paulus Schoutsen fc12d6fbb6 Add lg_tv_rs232 to LG brand (#172458)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-05-28 18:52:55 +02:00
Keilin Bickar 2a6b686254 Add Sense API exception handling (#169957)
Co-authored-by: Inca <inca@popre.net>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 17:42:43 +01:00
G Johansson 4d841e4d84 Update async_update_entity_platform to not allow loaded entities (#171773) 2026-05-28 18:17:23 +02:00
Lukas df08e9f311 Add button platform for Samsung Infrared integration (#171791)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 17:14:47 +01:00
Abílio Costa d53e40eea8 Add skill instruction on not duplicating entity base class behavior (#172362) 2026-05-28 16:03:43 +01:00
Franck Nijhof 0b261b7198 Fix SmartThings light checking wrong component for capabilities (#172430)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-05-28 16:27:57 +02:00
dependabot[bot] 3a9f32de25 Bump github/gh-aw-actions from 0.74.4 to 0.74.9 (#172398)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 13:52:56 +02:00
dependabot[bot] b5e54583c7 Bump docker/build-push-action from 7.1.0 to 7.2.0 (#172397)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 13:51:38 +02:00
Franck Nijhof 85ea7c1176 Fix Hue light ZeroDivisionError when mirek value is zero (#172442) 2026-05-28 13:50:45 +02:00
Franck Nijhof 713f520bc8 Fix iZone integration broken by python-izone 1.2.10 API change (#172427) 2026-05-28 13:48:19 +02:00
Michael Davie e4bb5a9395 Use ECMap for Environment Canada radar with layer support (#161602)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-28 13:47:58 +02:00
LG-ThinQ-Integration 936b2fe933 Remove unused translation in lg_thinq (#172394)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-05-28 13:44:56 +02:00
dependabot[bot] c6c6f08885 Bump dessant/lock-threads from 6.0.0 to 6.0.1 (#172399)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 13:40:03 +02:00
Ariel Ebersberger c621721851 Remove advanced options from config/test_config_entires (#172423) 2026-05-28 13:37:31 +02:00
Erik Montnemery 5bb6b20641 Add zone entered left triggers (#172412) 2026-05-28 13:22:38 +02:00
Manu 37f41d8e09 Fix index error in DuckDNS integration (#172392) 2026-05-28 12:58:51 +02:00
Crocmagnon b02f312bed ovhcloud_ai_endpoints: reauthentication flow (#172405) 2026-05-28 12:58:39 +02:00
Nikhil Deepak 3520c821c5 Reset MQTT valve opening/closing state at intermediate positions (#165176)
Co-authored-by: jbouwh <jan@jbsoft.nl>
2026-05-28 12:07:30 +02:00
Jan Bouwhuis cbf737a03e Improve MQTT protocol deprecation repair message (#172404)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 12:05:35 +02:00
Franck Nijhof 5bd6d52e6a Convert yamaha_musiccast sw_version to string (#172411) 2026-05-28 12:05:19 +02:00
Linkplay2020 d9a89beb3d Bump wiim to 1.0.4 (#172334)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
2026-05-28 11:38:22 +02:00
Ludovic BOUÉ 41f783f14d Add Matter soil moisture sensor (#172372) 2026-05-28 11:03:58 +02:00
Erik Montnemery 35397b818d Deprecate device tracker battery_level property (#171819)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 10:54:08 +02:00
Erik Montnemery d42d02f20a Revert "Add zone triggers entered/left zone" (#172409) 2026-05-28 10:32:28 +02:00
Franck Nijhof 99c445f261 Bump version to 2026.7.0dev0 (#172367) 2026-05-28 10:20:00 +02:00
Stefan Agner 567fe85828 Reject backup uploads with unsafe inner name (#172368)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:19:06 +02:00
Erik Montnemery fd1a5d0c5a Add zone triggers entered/left zone (#171751) 2026-05-28 10:05:41 +02:00
Erik Montnemery 632ec39d53 Deprecate device tracker TrackerEntity location_name property (#171820) 2026-05-28 10:02:28 +02:00
Abílio Costa 67b9d28953 Fix OMIE sensors not updating on setup (#172383) 2026-05-28 08:29:53 +02:00
J. Nick Koston e3880eedb0 Bump yalexs to 9.2.1 (#172389) 2026-05-27 22:01:07 -05:00
J. Nick Koston ce64f5f902 Bump onvif-zeep-async to 4.1.1 (#172391) 2026-05-27 22:00:56 -05:00
J. Nick Koston 0da99a50fc Bump dbus-fast to 5.0.16 (#172378) 2026-05-27 17:16:36 -05:00
Arcadiy Ivanov 43f636be65 Include device identity in Matter light transition blocklist warning (#172324) 2026-05-27 23:58:37 +02:00
Simone Chemelli 262cdbfab5 Bump aioamazondevices to 13.8.1 (#172382) 2026-05-27 23:16:23 +02:00
puddly 8cbd358435 Bump ZHA to 1.4.0 (#172357) 2026-05-27 22:55:07 +02:00
torben-iometer df04b19a0a bump iometer version to 1.0.1 (#172338) 2026-05-27 22:19:20 +02:00
markvp adeb352079 Add GeneralDiagnostics sensors and fault binary sensors to Matter integration (#169830) 2026-05-27 21:07:08 +02:00
Stefan Agner 1e457600f1 Harden backup tar extraction with Python tar_filter (#172252) 2026-05-27 18:10:04 +02:00
1394 changed files with 59540 additions and 9841 deletions
+2 -33
View File
@@ -8,39 +8,8 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
## Follow these steps:
1. Use 'gh pr view' to get the PR details and description.
2. Use 'gh pr diff' to see all the changes in the PR.
3. Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
3. Review the changes following the `review` skill. It is VERY IMPORTANT to follow the `review` skill instructions.
4. Check if all existing review comments have been addressed.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
- Suggest improvements where appropriate
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -24,6 +24,7 @@ The following platforms have extra guidelines:
## Entity platforms
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
## Integration Quality Scale
+38
View File
@@ -0,0 +1,38 @@
---
name: review
description: Reviews code changes and provides constructive feedback. Should be used when a review is requested to provide a consistent review behavior and output format. This skill can be used for code reviews in general, not just for GitHub pull requests.
---
# Review Code Changes
## Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
## Verification:
- After the review, run parallel subagents for each finding to double-check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes.
- Be constructive and specific in your comments.
- Suggest improvements where appropriate.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -27,6 +27,7 @@ The following platforms have extra guidelines:
## Entity platforms
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
## Integration Quality Scale
+13 -13
View File
@@ -38,7 +38,7 @@ jobs:
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -102,7 +102,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -245,7 +245,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -292,7 +292,7 @@ jobs:
contents: read
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -344,13 +344,13 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -380,7 +380,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
@@ -394,7 +394,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -471,7 +471,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -518,19 +518,19 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -543,7 +543,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -40,7 +40,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
+11 -11
View File
@@ -31,12 +31,12 @@
# - GITHUB_TOKEN
#
# Custom actions used:
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
# - github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
#
# Container images used:
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
@@ -90,7 +90,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -133,7 +133,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- name: Checkout .github and .agents folders
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
sparse-checkout: |
@@ -352,7 +352,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -372,7 +372,7 @@ jobs:
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
} >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Create gh-aw temp directory
@@ -961,7 +961,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1100,7 +1100,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1127,7 +1127,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
- name: Checkout repository for patch context
if: needs.agent.outputs.has_patch == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# --- Threat Detection ---
@@ -1325,7 +1325,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1383,7 +1383,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
+25 -25
View File
@@ -39,7 +39,7 @@ on:
env:
CACHE_VERSION: 3
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.6"
HA_SHORT_VERSION: "2026.7"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
@@ -98,7 +98,7 @@ jobs:
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Generate partial Python venv restore key
@@ -264,7 +264,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Register problem matchers
@@ -291,7 +291,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Run zizmor
@@ -318,7 +318,7 @@ jobs:
- script/hassfest/docker/Dockerfile
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Register hadolint problem matcher
@@ -341,7 +341,7 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
@@ -404,7 +404,7 @@ jobs:
echo "version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)" >> "$GITHUB_OUTPUT"
- name: Set up uv
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ steps.read-uv-version.outputs.version }}
- name: Create Python virtual environment
@@ -469,7 +469,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -512,7 +512,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -548,7 +548,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -576,7 +576,7 @@ jobs:
&& github.event_name == 'pull_request'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Dependency review
@@ -603,7 +603,7 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
@@ -654,7 +654,7 @@ jobs:
|| github.event.inputs.pylint-only == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -707,7 +707,7 @@ jobs:
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -758,7 +758,7 @@ jobs:
|| github.event.inputs.mypy-only == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -825,7 +825,7 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -889,7 +889,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1030,7 +1030,7 @@ jobs:
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1179,7 +1179,7 @@ jobs:
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1317,7 +1317,7 @@ jobs:
if: needs.info.outputs.skip_coverage != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Download all coverage artifacts
@@ -1326,7 +1326,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
fail_ci_if_error: true
flags: full-suite
@@ -1355,7 +1355,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1476,7 +1476,7 @@ jobs:
- pytest-partial
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Download all coverage artifacts
@@ -1485,7 +1485,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
@@ -1513,7 +1513,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
report_type: test_results
fail_ci_if_error: true
+3 -3
View File
@@ -23,16 +23,16 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
with:
category: "/language:python"
@@ -236,7 +236,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
with:
model: openai/gpt-4o
system-prompt: |
@@ -62,7 +62,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
with:
model: openai/gpt-4o-mini
system-prompt: |
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
issues: write # To lock issues
pull-requests: write # To lock pull requests
steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
with:
github-token: ${{ github.token }}
issue-inactive-days: "30"
+25 -67
View File
@@ -20,22 +20,36 @@ jobs:
permissions:
issues: write # To label and close stale issues
pull-requests: write # To label and close stale PRs
actions: write # To delete stalebot state
steps:
# Generate a token for the GitHub App, we use this method to avoid
# hitting API limits for our GitHub actions + have a higher rate limit.
- name: Generate app token
id: token
# Pinned to a specific version of the action for security reasons
# v3.2.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private-key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
# The 60 day stale policy for PRs
# Used for:
# - PRs
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
# The 90 day stale policy for issues
# Used for:
# - Issues
# - No issues marked as no-stale or help-wanted
- name: 60 days stale PRs policy and 90 days stale issue policy
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
days-before-issue-stale: -1
days-before-issue-close: -1
operations-per-run: 150
repo-token: ${{ steps.token.outputs.token }}
remove-stale-when-updated: true
operations-per-run: 350
# pr policy
days-before-pr-stale: 60
days-before-pr-close: 7
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
stale-pr-message: >
@@ -48,65 +62,9 @@ jobs:
branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
# Generate a token for the GitHub App, we use this method to avoid
# hitting API limits for our GitHub actions + have a higher rate limit.
# This is only used for issues.
- name: Generate app token
id: token
# Pinned to a specific version of the action for security reasons
# v3.2.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
app-id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private-key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
# The 90 day stale policy for issues
# Used for:
# - Issues
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
days-before-close: 7
days-before-pr-stale: -1
days-before-pr-close: -1
operations-per-run: 250
remove-stale-when-updated: true
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,help-wanted,needs-more-information"
stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the
high number of incoming GitHub notifications, we have to clean some
of the old issues, as many of them have already been resolved with
the latest updates.
Please make sure to update to the latest Home Assistant version and
check if that solves the issue. Let us know if that works for you by
adding a comment 👍
This issue has now been marked as stale and will be closed if no
further activity occurs. Thank you for your contributions.
# The 30 day stale policy for issues
# Used for:
# - Issues that are pending more information (incomplete issues)
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"
days-before-stale: 14
days-before-close: 7
days-before-pr-stale: -1
days-before-pr-close: -1
operations-per-run: 250
remove-stale-when-updated: true
# issue policy
days-before-issue-stale: 90
days-before-issue-close: 7
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,help-wanted"
stale-issue-message: >
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+5 -5
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -116,7 +116,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -137,7 +137,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -167,7 +167,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -195,7 +195,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.14
rev: v0.15.15
hooks:
- id: ruff-check
args:
+2
View File
@@ -96,6 +96,7 @@ homeassistant.components.aprs.*
homeassistant.components.apsystems.*
homeassistant.components.aqualogic.*
homeassistant.components.aquostv.*
homeassistant.components.aqvify.*
homeassistant.components.aranet.*
homeassistant.components.arcam_fmj.*
homeassistant.components.arris_tg2492lg.*
@@ -286,6 +287,7 @@ homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.*
homeassistant.components.husqvarna_automower.*
homeassistant.components.huum.*
homeassistant.components.hvv_departures.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.hypontech.*
Generated
+14 -4
View File
@@ -162,6 +162,8 @@ CLAUDE.md @home-assistant/core
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
/homeassistant/components/aquacell/ @Jordi1990
/tests/components/aquacell/ @Jordi1990
/homeassistant/components/aqvify/ @astrandb
/tests/components/aqvify/ @astrandb
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
/tests/components/aranet/ @aschmitz @thecode @anrijs
/homeassistant/components/arcam_fmj/ @elupus
@@ -453,6 +455,8 @@ CLAUDE.md @home-assistant/core
/tests/components/ecovacs/ @mib1185 @edenhaus @Augar
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/edifier_infrared/ @abmantis
/tests/components/edifier_infrared/ @abmantis
/homeassistant/components/efergy/ @tkdrob
/tests/components/efergy/ @tkdrob
/homeassistant/components/egardia/ @jeroenterheerdt
@@ -501,6 +505,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
/homeassistant/components/entur_public_transport/ @hfurubotten @SanderBlom
/homeassistant/components/envertech_evt800/ @daniel-bergmann-00
/tests/components/envertech_evt800/ @daniel-bergmann-00
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 @roberty99
@@ -570,8 +576,8 @@ CLAUDE.md @home-assistant/core
/tests/components/flo/ @dmulcahey
/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor
/tests/components/flume/ @ChrisMandich @bdraco @jeeftor
/homeassistant/components/fluss/ @fluss
/tests/components/fluss/ @fluss
/homeassistant/components/fluss/ @fluss @Marcello17
/tests/components/fluss/ @fluss @Marcello17
/homeassistant/components/flux_led/ @icemanch
/tests/components/flux_led/ @icemanch
/homeassistant/components/forecast_solar/ @klaasnicolaas @frenck
@@ -623,8 +629,8 @@ CLAUDE.md @home-assistant/core
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/gentex_homelink/ @Gentex-Corporation/Homelink @rjones-gentex
/tests/components/gentex_homelink/ @Gentex-Corporation/Homelink @rjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -718,6 +724,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/heatmiser/ @andylockran
/homeassistant/components/hegel/ @boazca
/tests/components/hegel/ @boazca
/homeassistant/components/helty/ @ebaschiera
/tests/components/helty/ @ebaschiera
/homeassistant/components/heos/ @andrewsayre
/tests/components/heos/ @andrewsayre
/homeassistant/components/here_travel_time/ @eifinger
@@ -836,6 +844,8 @@ CLAUDE.md @home-assistant/core
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/imou/ @Imou-OpenPlatform
/tests/components/imou/ @Imou-OpenPlatform
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
+2 -4
View File
@@ -92,8 +92,7 @@ def _extract_backup(
):
ostf.tar.extractall(
path=Path(tempdir, "extracted"),
members=securetar.secure_path(ostf.tar),
filter="fully_trusted",
filter="tar",
)
backup_meta_file = Path(tempdir, "extracted", "backup.json")
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
@@ -119,8 +118,7 @@ def _extract_backup(
) as istf:
istf.extractall(
path=Path(tempdir, "homeassistant"),
members=securetar.secure_path(istf),
filter="fully_trusted",
filter="tar",
)
if restore_content.restore_homeassistant:
keep = list(KEEP_BACKUPS)
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_CUBIC_METER,
PERCENTAGE,
UV_INDEX,
UnitOfIrradiance,
@@ -47,6 +46,8 @@ from .coordinator import (
PARALLEL_UPDATES = 1
PARTS_PER_CUBIC_METER = "p/m³"
@dataclass(frozen=True, kw_only=True)
class AccuWeatherSensorDescription(SensorEntityDescription):
@@ -81,7 +82,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription(
key="Grass",
entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
native_unit_of_measurement=PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
@@ -107,7 +108,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription(
key="Mold",
entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
native_unit_of_measurement=PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
@@ -116,7 +117,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
),
AccuWeatherSensorDescription(
key="Ragweed",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
native_unit_of_measurement=PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {
@@ -184,7 +185,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
),
AccuWeatherSensorDescription(
key="Tree",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
native_unit_of_measurement=PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {
@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.5.6"]
"requirements": ["actron-neo-api==0.5.12"]
}
@@ -116,10 +116,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Migrate old config entry."""
# This means the user has downgraded from a future version
if entry.version > 2:
return False
# 1.1 Migrate config_entry to add advanced ssl settings
if entry.version == 1 and entry.minor_version == 1:
new_minor_version = 2
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["airos==0.6.5"]
"requirements": ["airos==0.6.8"]
}
@@ -1,6 +1,10 @@
"""The AirVisual Pro integration."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -25,6 +29,12 @@ class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["serial_number"])},
connections={
(
CONNECTION_NETWORK_MAC,
format_mac(self.coordinator.data["status"]["mac_address"]),
)
},
manufacturer="AirVisual",
model=self.coordinator.data["status"]["model"],
name=self.coordinator.data["settings"]["node_name"],
@@ -65,10 +65,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version > 1:
# This means the user has downgraded from a future version
return False
if entry.version == 1 and entry.minor_version < 3:
if CONF_SITE in entry.data:
# Site in data (wrong place), just move to login data
@@ -6,7 +6,7 @@ from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import slugify
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
from .entity import AmazonServiceEntity
# Coordinator is used to centralize the data updates
@@ -49,4 +49,5 @@ class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
async def async_press(self) -> None:
"""Handle button press action."""
await self.coordinator.api.call_routine(self._routine)
async with alexa_api_call(self.coordinator):
await self.coordinator.api.call_routine(self._routine)
@@ -1,5 +1,7 @@
"""Support for Alexa Devices."""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import timedelta
from aioamazondevices.api import AmazonEchoApi
@@ -19,7 +21,11 @@ from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -29,6 +35,65 @@ from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
SCAN_INTERVAL = 300
@asynccontextmanager
async def alexa_api_call(
coordinator: DataUpdateCoordinator | None = None,
) -> AsyncGenerator[None]:
"""Handle common Alexa API exceptions as HomeAssistantError."""
try:
yield
except CannotAuthenticate as err:
if coordinator:
coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except CannotConnect as err:
if coordinator:
coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except (CannotRetrieveData, ValueError) as err:
if coordinator:
coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
@asynccontextmanager
async def alexa_config_entry_errors() -> AsyncGenerator[None]:
"""Handle common Alexa API exceptions as ConfigEntry errors."""
try:
yield
except CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except (CannotConnect, TimeoutError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except (CannotRetrieveData, ValueError, KeyError, StopIteration) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
@@ -113,6 +178,12 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except ValueError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
else:
current_devices = set(data.keys())
if stale_devices := self.previous_devices - current_devices:
@@ -169,26 +240,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
async def sync_history_state(self) -> None:
"""Sync history state."""
try:
async with alexa_config_entry_errors():
self._vocal_records = await self.api.sync_history_state()
except CannotAuthenticate as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(e)},
) from e
except CannotConnect as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(e)},
) from e
except BaseException as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(e)},
) from e
async def history_state_event_handler(
self, vocal_records: dict[str, AmazonVocalRecord]
@@ -204,26 +257,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
async def sync_media_state(self) -> None:
"""Sync media state."""
try:
async with alexa_config_entry_errors():
await self.api.sync_media_state()
except CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
except (CannotConnect, TimeoutError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except (CannotRetrieveData, ValueError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
async def media_state_event_handler(
self, media_state: dict[str, AmazonMediaState]
@@ -22,9 +22,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import _LOGGER
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
from .entity import AmazonEntity
from .utils import alexa_api_call
PARALLEL_UPDATES = 1
@@ -216,16 +215,15 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
provider = media_type.value if isinstance(media_type, MediaType) else media_type
await self.async_call_alexa_music(media_id, provider)
@alexa_api_call
async def async_call_alexa_music(
self, search_phrase: str, provider_id: str
) -> None:
"""Call alexa music."""
await self.coordinator.api.call_alexa_music(
self.device, search_phrase, provider_id
)
async with alexa_api_call(self.coordinator):
await self.coordinator.api.call_alexa_music(
self.device, search_phrase, provider_id
)
@alexa_api_call
async def async_set_device_volume(self, volume: int) -> None:
"""Set the device volume."""
_LOGGER.debug(
@@ -233,7 +231,8 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
self.device.serial_number,
volume,
)
await self.coordinator.api.set_device_volume(self.device, volume)
async with alexa_api_call(self.coordinator):
await self.coordinator.api.set_device_volume(self.device, volume)
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level (0.0 to 1.0)."""
@@ -263,12 +262,12 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
await self.async_set_volume_level(target_volume / 100)
self._prev_volume = None
@alexa_api_call
async def _send_media_command(self, command: AmazonMediaControls) -> None:
_LOGGER.debug(
"Sending media command '%s' to %s", command, self.device.serial_number
)
await self.coordinator.api.send_media_command(self.device, command)
async with alexa_api_call(self.coordinator):
await self.coordinator.api.send_media_command(self.device, command)
async def async_media_stop(self) -> None:
"""Send stop command."""
@@ -12,9 +12,8 @@ from homeassistant.components.notify import NotifyEntity, NotifyEntityDescriptio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .coordinator import AmazonConfigEntry, alexa_api_call
from .entity import AmazonEntity
from .utils import alexa_api_call
PARALLEL_UPDATES = 1
@@ -80,10 +79,11 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
entity_description: AmazonNotifyEntityDescription
@alexa_api_call
async def async_send_message(
self, message: str, title: str | None = None, **kwargs: Any
) -> None:
"""Send a message."""
await self.entity_description.method(self.coordinator.api, self.device, message)
async with alexa_api_call(self.coordinator):
await self.entity_description.method(
self.coordinator.api, self.device, message
)
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from .const import DOMAIN, INFO_SKILLS_MAPPING
from .coordinator import AmazonConfigEntry
from .coordinator import AmazonConfigEntry, alexa_api_call
ATTR_TEXT_COMMAND = "text_command"
ATTR_SOUND = "sound"
@@ -85,13 +85,15 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
translation_key="invalid_sound_value",
translation_placeholders={"sound": value},
)
await coordinator.api.call_alexa_sound(
coordinator.data[device.serial_number], value
)
async with alexa_api_call():
await coordinator.api.call_alexa_sound(
coordinator.data[device.serial_number], value
)
elif attribute == ATTR_TEXT_COMMAND:
await coordinator.api.call_alexa_text_command(
coordinator.data[device.serial_number], value
)
async with alexa_api_call():
await coordinator.api.call_alexa_text_command(
coordinator.data[device.serial_number], value
)
elif attribute == ATTR_INFO_SKILL:
info_skill = INFO_SKILLS_MAPPING.get(value)
if info_skill not in ALEXA_INFO_SKILLS:
@@ -100,9 +102,10 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
translation_key="invalid_info_skill_value",
translation_placeholders={"info_skill": value},
)
await coordinator.api.call_alexa_info_skill(
coordinator.data[device.serial_number], info_skill
)
async with alexa_api_call():
await coordinator.api.call_alexa_info_skill(
coordinator.data[device.serial_number], info_skill
)
async def async_send_sound_notification(call: ServiceCall) -> None:
@@ -14,13 +14,9 @@ from homeassistant.components.switch import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .coordinator import AmazonConfigEntry, alexa_api_call
from .entity import AmazonEntity
from .utils import (
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
from .utils import async_remove_dnd_from_virtual_group, async_update_unique_id
PARALLEL_UPDATES = 1
@@ -90,7 +86,6 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
entity_description: AmazonSwitchEntityDescription
@alexa_api_call
async def _switch_set_state(self, state: bool) -> None:
"""Set desired switch state."""
method = getattr(self.coordinator.api, self.entity_description.method)
@@ -98,7 +93,8 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
if TYPE_CHECKING:
assert method is not None
await method(self.device, state)
async with alexa_api_call(self.coordinator):
await method(self.device, state)
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
@@ -1,54 +1,19 @@
"""Utils for Alexa Devices."""
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.const.schedules import (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
)
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonDevicesCoordinator
from .entity import AmazonEntity
def alexa_api_call[_T: AmazonEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
"""Catch Alexa API call exceptions."""
@wraps(func)
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap all command methods."""
try:
await func(self, *args, **kwargs)
except CannotConnect as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(err)},
) from err
except CannotRetrieveData as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
return cmd_wrapper
async def async_update_unique_id(
@@ -75,10 +75,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> b
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
if entry.version > 1:
# This means the user has downgraded from a future version
return False
if entry.version == 1 and entry.minor_version == 1:
new_data = {**entry.data}
if CONF_DEVICES in new_data:
@@ -178,10 +178,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
"""Migrate entry."""
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
if entry.version > 2:
# This means the user has downgraded from a future version
return False
if entry.version == 2 and entry.minor_version == 1:
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
device_registry = dr.async_get(hass)
+2 -3
View File
@@ -4,7 +4,6 @@ import base64
from collections import deque
from collections.abc import AsyncIterator, Callable, Iterable
from dataclasses import dataclass, field
from datetime import UTC, datetime
import json
from mimetypes import guess_file_type
from pathlib import Path
@@ -114,7 +113,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from homeassistant.util.json import JsonArrayType, JsonObjectType
from .const import (
@@ -372,7 +371,7 @@ def _convert_content( # noqa: C901
)
if (
content.native.container is not None
and content.native.container.expires_at > datetime.now(UTC)
and content.native.container.expires_at > dt_util.utcnow()
):
container_id = content.native.container.id
+5 -4
View File
@@ -59,7 +59,6 @@ ATTR_EXTERNAL_URL = "external_url"
ATTR_INTERNAL_URL = "internal_url"
ATTR_LOCATION_NAME = "location_name"
ATTR_INSTALLATION_TYPE = "installation_type"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_UUID = "uuid"
ATTR_VERSION = "version"
@@ -222,7 +221,7 @@ class APIStatesView(HomeAssistantView):
states = (
state.as_dict_json
for state in hass.states.async_all()
if entity_perm(state.entity_id, "read")
if entity_perm(state.entity_id, POLICY_READ)
)
response = web.Response(
body=b"".join((b"[", b",".join(states), b"]")),
@@ -294,8 +293,10 @@ class APIEntityStateView(HomeAssistantView):
# Read the state back for our response
status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK
state = hass.states.get(entity_id)
assert state
if (state := hass.states.get(entity_id)) is None:
return self.json_message(
"Error storing state.", HTTPStatus.INTERNAL_SERVER_ERROR
)
resp = self.json(state.as_dict(), status_code)
resp.headers.add("Location", f"/api/states/{entity_id}")
@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["apprise"],
"quality_scale": "legacy",
"requirements": ["apprise==1.9.1"]
"requirements": ["apprise==1.11.0"]
}
@@ -0,0 +1,28 @@
"""The Aqvify integration."""
import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import AqvifyConfigEntry, AqvifyCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AqvifyConfigEntry) -> bool:
"""Set up Aqvify from a config entry."""
coordinator = AqvifyCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AqvifyConfigEntry) -> bool:
"""Unload Aqvify config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,98 @@
"""Config flow for the Aqvify integration."""
from collections.abc import Mapping
import logging
from typing import Any
from aiohttp import ClientResponseError
from pyaqvify import AqvifyAPI, AqvifyAuthException
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
}
)
class AqvifyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aqvify."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
hub = AqvifyAPI(
user_input[CONF_API_KEY],
websession=async_get_clientsession(self.hass),
)
try:
account_data = await hub.async_get_account_id()
except AqvifyAuthException:
errors["base"] = "invalid_auth"
except ClientResponseError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(account_data.account_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Aqvify", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={
"aqvify_url": "https://app.aqvify.com/User",
},
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-authentication confirmation."""
errors = {}
if user_input is not None:
api_client = AqvifyAPI(
user_input[CONF_API_KEY],
websession=async_get_clientsession(self.hass),
)
try:
account_data = await api_client.async_get_account_id()
except AqvifyAuthException:
errors["base"] = "invalid_auth"
except ClientResponseError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(account_data.account_id)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
+3
View File
@@ -0,0 +1,3 @@
"""Constants for the Aqvify integration."""
DOMAIN = "aqvify"
@@ -0,0 +1,137 @@
"""Coordinator for Aqvify integration."""
from dataclasses import dataclass
from datetime import timedelta
import logging
from aiohttp import ClientResponseError
from pyaqvify import AqvifyAPI, AqvifyAuthException, AqvifyDeviceData, AqvifyDevices
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=60)
type AqvifyConfigEntry = ConfigEntry[AqvifyCoordinator]
@dataclass
class AqvifyCoordinatorData:
"""Data class for storing coordinator data."""
devices: AqvifyDevices
device_data: dict[str, AqvifyDeviceData]
class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
"""Data update coordinator for Aqvify devices."""
config_entry: AqvifyConfigEntry
def __init__(self, hass: HomeAssistant, entry: AqvifyConfigEntry) -> None:
"""Initialize the Aqvify data update coordinator."""
super().__init__(
hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
config_entry=entry,
)
self.api_client = AqvifyAPI(
entry.data[CONF_API_KEY], websession=async_get_clientsession(hass)
)
async def _async_setup(self) -> None:
"""Set up the coordinator."""
try:
await self.api_client.async_get_account_id()
except AqvifyAuthException:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_api_key",
) from None
except ClientResponseError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"entry": self.config_entry.title,
},
) from err
except TimeoutError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="api_timeout",
translation_placeholders={
"entry": self.config_entry.title,
},
) from err
async def _async_update_data(self) -> AqvifyCoordinatorData:
"""Fetch device state."""
try:
devices = await self.api_client.async_get_devices()
except AqvifyAuthException:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_api_key",
) from None
except ClientResponseError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"entry": self.config_entry.title,
},
) from err
except TimeoutError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_timeout",
translation_placeholders={
"entry": self.config_entry.title,
},
) from err
device_data = {}
for device in devices.devices.values():
try:
device_key = str(device.device_key)
device_data[
device_key
] = await self.api_client.async_get_device_latest_data(device_key)
except AqvifyAuthException:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_api_key",
) from None
except ClientResponseError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"entry": self.config_entry.title,
},
) from err
except TimeoutError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_timeout",
translation_placeholders={
"entry": self.config_entry.title,
},
) from err
return AqvifyCoordinatorData(
devices=devices,
device_data=device_data,
)
@@ -0,0 +1,30 @@
"""Diagnostics platform for Aqvify integration."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from .coordinator import AqvifyConfigEntry
TO_REDACT = [CONF_API_KEY]
TO_REDACT_AQVIFY = ["name"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AqvifyConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
device_list_raw_data = entry.runtime_data.data.devices.raw
device_data_raw_data = {
key: device.raw_data
for key, device in entry.runtime_data.data.device_data.items()
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"devices": async_redact_data(device_list_raw_data, TO_REDACT_AQVIFY),
"device_data": device_data_raw_data,
}
+35
View File
@@ -0,0 +1,35 @@
"""Defines a base Aqvify entity."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AqvifyCoordinator
class AqvifyBaseEntity(CoordinatorEntity[AqvifyCoordinator]):
"""Defines a base Aqvify entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AqvifyCoordinator,
description: EntityDescription,
device_key: str,
) -> None:
"""Initialize the Aqvify entity."""
super().__init__(coordinator)
account_id = self.coordinator.config_entry.unique_id
self.device_key = device_key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{account_id}_{device_key}")},
name=coordinator.data.devices.devices[device_key].name,
manufacturer="Aqvify",
configuration_url="https://app.aqvify.com",
serial_number=device_key,
)
self._attr_unique_id = f"{account_id}_{device_key}_{description.key}"
self.entity_description = description
@@ -0,0 +1,12 @@
{
"entity": {
"sensor": {
"meter_value": {
"default": "mdi:waves-arrow-up"
},
"water_level": {
"default": "mdi:waves"
}
}
}
}
@@ -0,0 +1,12 @@
{
"domain": "aqvify",
"name": "Aqvify",
"codeowners": ["@astrandb"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aqvify",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyaqvify"],
"quality_scale": "bronze",
"requirements": ["pyaqvify==0.0.9"]
}
@@ -0,0 +1,69 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No actions in this integration.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
The integration does not provide any actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration do not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: todo
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: todo
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
+79
View File
@@ -0,0 +1,79 @@
"""Sensor platform for Aqvify integration."""
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from pyaqvify import AqvifyDeviceData
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
StateType,
)
from homeassistant.const import UnitOfLength
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AqvifyConfigEntry
from .entity import AqvifyBaseEntity
# Coordinator is used to centralize the data updates.
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AqvifySensorEntityDescription(SensorEntityDescription):
"""Description of an Aqvify sensor entity."""
value_fn: Callable[[AqvifyDeviceData], float | int | None]
ENTITIES: tuple[AqvifySensorEntityDescription, ...] = (
AqvifySensorEntityDescription(
key="meter_value",
translation_key="meter_value",
native_unit_of_measurement=UnitOfLength.METERS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DISTANCE,
suggested_display_precision=2,
value_fn=lambda value: value.meter_value,
),
AqvifySensorEntityDescription(
key="water_level",
translation_key="water_level",
native_unit_of_measurement=UnitOfLength.METERS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DISTANCE,
suggested_display_precision=2,
value_fn=lambda value: value.water_level,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AqvifyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aqvify sensor entities from a config entry."""
async_add_entities(
AqvifySensor(entry.runtime_data, description, device_key)
for description in ENTITIES
for device_key in entry.runtime_data.data.devices.devices
)
class AqvifySensor(AqvifyBaseEntity, SensorEntity):
"""Representation of an Aqvify sensor entity."""
entity_description: AqvifySensorEntityDescription
@property
def native_value(self) -> StateType | datetime | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(
self.coordinator.data.device_data[self.device_key]
)
@@ -0,0 +1,55 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "The entered API key corresponds to a different account."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::aqvify::config::step::user::data_description::api_key%]"
},
"description": "Reauthentication required. Please enter your updated API key."
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "Your Aqvify API key"
},
"description": "Navigate to your [Aqvify account]({aqvify_url}), copy your API key, and paste it below."
}
}
},
"entity": {
"sensor": {
"meter_value": {
"name": "Meter value"
},
"water_level": {
"name": "Water level"
}
}
},
"exceptions": {
"api_error": {
"message": "An error occurred while communicating with the Aqvify API for {entry}"
},
"api_timeout": {
"message": "Timeout occurred while communicating with the Aqvify API for {entry}"
},
"invalid_api_key": {
"message": "Invalid API key. Please verify your API key and try to reauthenticate."
}
}
}
+3 -2
View File
@@ -4,6 +4,7 @@
"codeowners": [],
"dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/arwn",
"iot_class": "local_polling",
"quality_scale": "legacy"
"iot_class": "local_push",
"quality_scale": "legacy",
"requirements": ["arwn-client==0.2.1"]
}
+80 -121
View File
@@ -3,113 +3,26 @@
import logging
from typing import Any
from arwn_client import parse_message
from homeassistant.components import mqtt
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util.json import json_loads_object
_LOGGER = logging.getLogger(__name__)
DOMAIN = "arwn"
DATA_ARWN = "arwn"
TOPIC = "arwn/#"
def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] | None:
"""Given a topic, dynamically create the right sensor type.
Async friendly.
"""
parts = topic.split("/")
unit = payload.get("units", "")
domain = parts[1]
if domain == "temperature":
name = parts[2]
if unit == "F":
unit = UnitOfTemperature.FAHRENHEIT
else:
unit = UnitOfTemperature.CELSIUS
return [
ArwnSensor(
topic, name, "temp", unit, device_class=SensorDeviceClass.TEMPERATURE
)
]
if domain == "moisture":
name = f"{parts[2]} Moisture"
return [ArwnSensor(topic, name, "moisture", unit, "mdi:water-percent")]
if domain == "rain":
if len(parts) >= 3 and parts[2] == "today":
return [
ArwnSensor(
topic,
"Rain Since Midnight",
"since_midnight",
UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
)
]
return [
ArwnSensor(
topic + "/total",
"Total Rainfall",
"total",
unit,
device_class=SensorDeviceClass.PRECIPITATION,
),
ArwnSensor(
topic + "/rate",
"Rainfall Rate",
"rate",
unit,
device_class=SensorDeviceClass.PRECIPITATION,
),
]
if domain == "barometer":
return [
ArwnSensor(topic, "Barometer", "pressure", unit, "mdi:thermometer-lines")
]
if domain == "wind":
return [
ArwnSensor(
topic + "/speed",
"Wind Speed",
"speed",
unit,
device_class=SensorDeviceClass.WIND_SPEED,
),
ArwnSensor(
topic + "/gust",
"Wind Gust",
"gust",
unit,
device_class=SensorDeviceClass.WIND_SPEED,
),
ArwnSensor(
topic + "/dir",
"Wind Direction",
"direction",
DEGREE,
"mdi:compass",
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
]
return None
def _slug(name: str) -> str:
return f"sensor.arwn_{slugify(name)}"
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -118,28 +31,25 @@ async def async_setup_platform(
) -> None:
"""Set up the ARWN platform."""
# Make sure MQTT integration is enabled and the client is available
if not await mqtt.async_wait_for_mqtt_client(hass):
_LOGGER.error("MQTT integration is not available")
return
@callback
def async_sensor_event_received(msg: mqtt.ReceiveMessage) -> None:
"""Process events as sensors.
"""Process MQTT events as sensors."""
try:
event = json_loads_object(msg.payload)
device = parse_message(msg.topic, event)
except Exception: # noqa: BLE001
_LOGGER.debug(
"Failed to parse ARWN message on topic %s",
msg.topic,
exc_info=True,
)
return
When a new event on our topic (arwn/#) is received we map it
into a known kind of sensor based on topic name. If we've
never seen this before, we keep this sensor around in a global
cache. If we have seen it before, we update the values of the
existing sensor. Either way, we push an ha state update at the
end for the new event we've seen.
This lets us dynamically incorporate sensors without any
configuration on our side.
"""
event = json_loads_object(msg.payload)
sensors = discover_sensors(msg.topic, event)
if not sensors:
if device is None:
return
if (store := hass.data.get(DATA_ARWN)) is None:
@@ -148,22 +58,71 @@ async def async_setup_platform(
if "timestamp" in event:
del event["timestamp"]
for sensor in sensors:
if sensor.name not in store:
sensor.hass = hass
sensor.set_event(event)
store[sensor.name] = sensor
new_sensors: list[ArwnSensor] = []
for reading in device.readings:
if not reading.expose:
continue
unique_id = (
f"{msg.topic}/{reading.sensor_key}"
if len(device.readings) > 1
else msg.topic
)
try:
device_class = (
SensorDeviceClass(reading.device_class)
if reading.device_class
else None
)
except ValueError:
_LOGGER.debug(
"Unknown device_class=%s for sensor %s",
reading.device_class,
reading.sensor_name,
)
device_class = None
try:
state_class = (
SensorStateClass(reading.state_class)
if reading.state_class
else None
)
except ValueError:
_LOGGER.debug(
"Unknown state_class=%s for sensor %s",
reading.state_class,
reading.sensor_name,
)
state_class = None
if unique_id not in store:
sensor = ArwnSensor(
unique_id=unique_id,
name=reading.sensor_name,
state_key=reading.sensor_key,
units=reading.unit,
icon=reading.icon,
device_class=device_class,
state_class=state_class,
event=event,
)
store[unique_id] = sensor
_LOGGER.debug(
"Registering sensor %(name)s => %(event)s",
{"name": sensor.name, "event": event},
{"name": reading.sensor_name, "event": event},
)
async_add_entities((sensor,), True)
new_sensors.append(sensor)
else:
_LOGGER.debug(
"Recording sensor %(name)s => %(event)s",
{"name": sensor.name, "event": event},
{"name": reading.sensor_name, "event": event},
)
store[sensor.name].set_event(event)
store[unique_id].set_event(event)
if new_sensors:
async_add_entities(new_sensors, True)
await mqtt.async_subscribe(hass, TOPIC, async_sensor_event_received, 0)
@@ -175,29 +134,29 @@ class ArwnSensor(SensorEntity):
def __init__(
self,
topic: str,
unique_id: str,
name: str,
state_key: str,
units: str,
icon: str | None = None,
device_class: SensorDeviceClass | None = None,
state_class: SensorStateClass | None = None,
event: dict[str, Any] | None = None,
) -> None:
"""Initialize the sensor."""
self.entity_id = _slug(name)
self._attr_name = name
# This mqtt topic for the sensor which is its uid
self._attr_unique_id = topic
self._attr_unique_id = unique_id
self._state_key = state_key
self._attr_native_unit_of_measurement = units
self._attr_icon = icon
self._attr_device_class = device_class
self._attr_state_class = state_class
if event is not None:
self._attr_extra_state_attributes = dict(event)
self._attr_native_value = event.get(state_key)
def set_event(self, event: dict[str, Any]) -> None:
"""Update the sensor with the most recent event."""
ev: dict[str, Any] = {}
ev.update(event)
self._attr_extra_state_attributes = ev
self._attr_native_value = ev.get(self._state_key)
self._attr_extra_state_attributes = dict(event)
self._attr_native_value = event.get(self._state_key)
self.async_write_ha_state()
@@ -1816,6 +1816,11 @@ class PipelineInput:
await self.run.text_to_speech(tts_input)
except PipelineError as err:
if self.run.tts_stream:
# Clean up TTS stream
self.run.tts_stream.delete()
self.run.tts_stream = None
self.run.process_event(
PipelineEvent(
PipelineEventType.ERROR,
@@ -1885,15 +1890,17 @@ class PipelineInput:
):
prepare_tasks.append(self.run.prepare_recognize_intent(self.session))
if prepare_tasks:
await asyncio.gather(*prepare_tasks)
# Do TTS prepare separately so we don't create a ResultStream if the
# pipeline is invalid.
if (
start_stage_index
<= PIPELINE_STAGE_ORDER.index(PipelineStage.TTS)
<= end_stage_index
):
prepare_tasks.append(self.run.prepare_text_to_speech())
if prepare_tasks:
await asyncio.gather(*prepare_tasks)
await self.run.prepare_text_to_speech()
class PipelinePreferred(CollectionError):
@@ -3,8 +3,11 @@
from dataclasses import asdict
import logging
from pathlib import Path
import re
from typing import Any
from hassil.parse_expression import parse_sentence
from hassil.parser import ParseError
from hassil.util import (
PUNCTUATION_END,
PUNCTUATION_END_WORD,
@@ -164,6 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
is_valid_sentence,
),
}
],
@@ -201,6 +205,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
for sentence in value:
# Exclude {list_references} which may contain punctuation characters.
sentence = _remove_list_references(sentence)
if (
PUNCTUATION_START.search(sentence)
or PUNCTUATION_END.search(sentence)
@@ -212,6 +218,21 @@ def has_no_punctuation(value: list[str]) -> list[str]:
return value
def _remove_list_references(sentence: str) -> str:
"""Remove {list_references} from a sentence for linting."""
return re.sub(r"(?<!\\)\{[^{}]*\}", "", sentence)
def is_valid_sentence(value: list[str]) -> list[str]:
"""Validate result can be parsed by hassil."""
for sentence in value:
try:
parse_sentence(sentence)
except ParseError as err:
raise vol.Invalid(f"invalid sentence: {err}") from err
return value
def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0"]
"requirements": ["hassil==3.6.0"]
}
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
}
@@ -0,0 +1 @@
"""Virtual integration: Avosdim."""
@@ -0,0 +1,6 @@
{
"domain": "avosdim",
"name": "Avosdim",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}
+5 -6
View File
@@ -6,7 +6,6 @@ from blebox_uniapi.box import Box
from blebox_uniapi.error import Error
from blebox_uniapi.session import ApiHost
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -18,10 +17,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DEFAULT_SETUP_TIMEOUT
from .coordinator import BleBoxConfigEntry, BleBoxCoordinator
from .helpers import get_maybe_authenticated_session
type BleBoxConfigEntry = ConfigEntry[Box]
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
@@ -35,8 +33,6 @@ PLATFORMS = [
Platform.UPDATE,
]
PARALLEL_UPDATES = 0
async def async_setup_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bool:
"""Set up BleBox devices from a config entry."""
@@ -58,7 +54,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bo
_LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex)
raise ConfigEntryNotReady from ex
entry.runtime_data = product
coordinator = BleBoxCoordinator(hass, entry, product)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -11,13 +11,20 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
PARALLEL_UPDATES = 0
BINARY_SENSOR_TYPES = (
BinarySensorEntityDescription(
key="moisture",
device_class=BinarySensorDeviceClass.MOISTURE,
),
BinarySensorEntityDescription(
key="open",
device_class=BinarySensorDeviceClass.WINDOW,
),
)
@@ -27,23 +34,27 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
coordinator = config_entry.runtime_data
entities = [
BleBoxBinarySensorEntity(feature, description)
for feature in config_entry.runtime_data.features.get("binary_sensors", [])
BleBoxBinarySensorEntity(coordinator, feature, description)
for feature in coordinator.box.features.get("binary_sensors", [])
for description in BINARY_SENSOR_TYPES
if description.key == feature.device_class
]
async_add_entities(entities, True)
async_add_entities(entities)
class BleBoxBinarySensorEntity(BleBoxEntity[BinarySensorFeature], BinarySensorEntity):
"""Representation of a BleBox binary sensor feature."""
def __init__(
self, feature: BinarySensorFeature, description: BinarySensorEntityDescription
self,
coordinator: BleBoxCoordinator,
feature: BinarySensorFeature,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize a BleBox binary sensor feature."""
super().__init__(feature)
super().__init__(coordinator, feature)
self.entity_description = description
@property
+32 -20
View File
@@ -2,12 +2,26 @@
import blebox_uniapi.button
from homeassistant.components.button import ButtonEntity
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
from .util import blebox_command
PARALLEL_UPDATES = 1
BUTTON_TYPES: dict[str, ButtonEntityDescription] = {
"up": ButtonEntityDescription(key="up", translation_key="up"),
"down": ButtonEntityDescription(key="down", translation_key="down"),
"fav": ButtonEntityDescription(key="fav", translation_key="fav"),
"open": ButtonEntityDescription(key="open", translation_key="open"),
"close": ButtonEntityDescription(key="close", translation_key="close"),
}
_DEFAULT_BUTTON = ButtonEntityDescription(key="button")
async def async_setup_entry(
@@ -16,35 +30,33 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox button entry."""
coordinator = config_entry.runtime_data
entities = [
BleBoxButtonEntity(feature)
for feature in config_entry.runtime_data.features.get("buttons", [])
BleBoxButtonEntity(coordinator, feature)
for feature in coordinator.box.features.get("buttons", [])
]
async_add_entities(entities, True)
async_add_entities(entities)
class BleBoxButtonEntity(BleBoxEntity[blebox_uniapi.button.Button], ButtonEntity):
"""Representation of BleBox buttons."""
def __init__(self, feature: blebox_uniapi.button.Button) -> None:
def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.button.Button
) -> None:
"""Initialize a BleBox button feature."""
super().__init__(feature)
self._attr_icon = self.get_icon()
def get_icon(self) -> str | None:
"""Return icon for endpoint."""
if "up" in self._feature.query_string:
return "mdi:arrow-up-circle"
if "down" in self._feature.query_string:
return "mdi:arrow-down-circle"
if "fav" in self._feature.query_string:
return "mdi:heart-circle"
if "open" in self._feature.query_string:
return "mdi:arrow-up-circle"
if "close" in self._feature.query_string:
return "mdi:arrow-down-circle"
return None
super().__init__(coordinator, feature)
self.entity_description = self._get_description()
def _get_description(self) -> ButtonEntityDescription:
"""Return the description matching this button's query string."""
for key, description in BUTTON_TYPES.items():
if key in self._feature.query_string:
return description
return _DEFAULT_BUTTON
@blebox_command
async def async_press(self) -> None:
"""Handle the button press."""
await self._feature.set()
+8 -5
View File
@@ -1,6 +1,5 @@
"""BleBox climate entity."""
from datetime import timedelta
from typing import Any
import blebox_uniapi.climate
@@ -17,8 +16,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .entity import BleBoxEntity
from .util import blebox_command
SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 1
BLEBOX_TO_HVACMODE = {
0: HVACMode.OFF,
@@ -40,11 +40,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox climate entity."""
coordinator = config_entry.runtime_data
entities = [
BleBoxClimateEntity(feature)
for feature in config_entry.runtime_data.features.get("climates", [])
BleBoxClimateEntity(coordinator, feature)
for feature in coordinator.box.features.get("climates", [])
]
async_add_entities(entities, True)
async_add_entities(entities)
class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity):
@@ -108,6 +109,7 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn
"""Return the desired thermostat temperature."""
return self._feature.desired
@blebox_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the climate entity mode."""
if hvac_mode in [HVACMode.HEAT, HVACMode.COOL]:
@@ -116,6 +118,7 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn
await self._feature.async_off()
@blebox_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the thermostat temperature."""
value = kwargs[ATTR_TEMPERATURE]
+88 -48
View File
@@ -33,23 +33,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
def create_schema(previous_input=None):
"""Create a schema with given values as default."""
if previous_input is not None:
host = previous_input[CONF_HOST]
port = previous_input[CONF_PORT]
else:
host = DEFAULT_HOST
port = DEFAULT_PORT
return vol.Schema(
{
vol.Required(CONF_HOST, default=host): str,
vol.Required(CONF_PORT, default=port): int,
vol.Inclusive(CONF_USERNAME, "auth"): str,
vol.Inclusive(CONF_PASSWORD, "auth"): str,
}
)
STEP_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
vol.Inclusive(CONF_USERNAME, "auth"): str,
vol.Inclusive(CONF_PASSWORD, "auth"): str,
}
)
LOG_MSG = {
@@ -69,18 +60,44 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
self.device_config: dict[str, Any] = {}
def handle_step_exception(
self, step, exception, schema, host, port, message_id, log_fn
self, exception, schema, host, port, message_id, log_fn, step_id
):
"""Handle step exceptions."""
log_fn("%s at %s:%d (%s)", LOG_MSG[message_id], host, port, exception)
return self.async_show_form(
step_id="user",
step_id=step_id,
data_schema=schema,
errors={"base": message_id},
description_placeholders={"address": f"{host}:{port}"},
)
async def _async_from_host_or_form(
self, api_host: ApiHost, user_input: dict[str, Any], step_id: str
) -> tuple[Box, None] | tuple[None, ConfigFlowResult]:
"""Try to connect to the device; return product or an error form."""
schema = self.add_suggested_values_to_schema(STEP_SCHEMA, user_input)
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
try:
return await Box.async_from_host(api_host), None
except UnsupportedBoxVersion as ex:
return None, self.handle_step_exception(
ex, schema, host, port, UNSUPPORTED_VERSION, _LOGGER.debug, step_id
)
except UnauthorizedRequest as ex:
return None, self.handle_step_exception(
ex, schema, host, port, CANNOT_CONNECT, _LOGGER.error, step_id
)
except Error as ex:
return None, self.handle_step_exception(
ex, schema, host, port, CANNOT_CONNECT, _LOGGER.warning, step_id
)
except RuntimeError as ex:
return None, self.handle_step_exception(
ex, schema, host, port, UNKNOWN, _LOGGER.error, step_id
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
@@ -145,12 +162,11 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle initial user-triggered config step."""
hass = self.hass
schema = create_schema(user_input)
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=schema,
data_schema=STEP_SCHEMA,
errors={},
description_placeholders={},
)
@@ -173,36 +189,60 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
api_host = ApiHost(
host, port, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER
)
try:
product = await Box.async_from_host(api_host)
except UnsupportedBoxVersion as ex:
return self.handle_step_exception(
"user",
ex,
schema,
host,
port,
UNSUPPORTED_VERSION,
_LOGGER.debug,
)
except UnauthorizedRequest as ex:
return self.handle_step_exception(
"user", ex, schema, host, port, CANNOT_CONNECT, _LOGGER.error
)
except Error as ex:
return self.handle_step_exception(
"user", ex, schema, host, port, CANNOT_CONNECT, _LOGGER.warning
)
except RuntimeError as ex:
return self.handle_step_exception(
"user", ex, schema, host, port, UNKNOWN, _LOGGER.error
)
product, error = await self._async_from_host_or_form(
api_host, user_input, step_id="user"
)
if error is not None:
return error
assert product is not None
# Check if configured but IP changed since
await self.async_set_unique_id(product.unique_id, raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=product.name, data=user_input)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of a BleBox device."""
reconfigure_entry = self._get_reconfigure_entry()
if user_input is None:
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
STEP_SCHEMA, reconfigure_entry.data
),
)
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
username = user_input.get(CONF_USERNAME)
password = user_input.get(CONF_PASSWORD)
websession = get_maybe_authenticated_session(self.hass, password, username)
api_host = ApiHost(
host, port, DEFAULT_SETUP_TIMEOUT, websession, self.hass.loop, _LOGGER
)
product, error = await self._async_from_host_or_form(
api_host, user_input, step_id="reconfigure"
)
if error is not None:
return error
assert product is not None
await self.async_set_unique_id(product.unique_id, raise_on_progress=False)
self._abort_if_unique_id_mismatch()
data_updates: dict[str, Any] = {CONF_HOST: host, CONF_PORT: port}
if username is not None:
data_updates[CONF_USERNAME] = username
if password is not None:
data_updates[CONF_PASSWORD] = password
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates=data_updates,
)
+7
View File
@@ -14,6 +14,13 @@ UNKNOWN = "unknown"
DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80
OPEN_STATUS: dict[int, str] = {
0: "open",
1: "unclosed_or_unlocked",
2: "ajar",
3: "closed_but_unlocked",
4: "closed",
}
LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
@@ -0,0 +1,48 @@
"""DataUpdateCoordinator for BleBox devices."""
from datetime import timedelta
import logging
from blebox_uniapi.box import Box
from blebox_uniapi.error import Error
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type BleBoxConfigEntry = ConfigEntry[BleBoxCoordinator]
class BleBoxCoordinator(DataUpdateCoordinator[None]):
"""Coordinator for a single BleBox device."""
config_entry: BleBoxConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: BleBoxConfigEntry, box: Box
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=timedelta(seconds=5),
)
self.box = box
async def _async_update_data(self) -> None:
"""Fetch data from the BleBox device."""
try:
await self.box.async_update_data()
except Error as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="data_update_failed",
translation_placeholders={"error": str(err)},
) from err
+19 -5
View File
@@ -17,7 +17,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
from .util import blebox_command
PARALLEL_UPDATES = 1
BLEBOX_TO_COVER_DEVICE_CLASSES = {
"gate": CoverDeviceClass.GATE,
@@ -59,19 +63,22 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
coordinator = config_entry.runtime_data
entities = [
BleBoxCoverEntity(feature)
for feature in config_entry.runtime_data.features.get("covers", [])
BleBoxCoverEntity(coordinator, feature)
for feature in coordinator.box.features.get("covers", [])
]
async_add_entities(entities, True)
async_add_entities(entities)
class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
"""Representation of a BleBox cover feature."""
def __init__(self, feature: blebox_uniapi.cover.Cover) -> None:
def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.cover.Cover
) -> None:
"""Initialize a BleBox cover feature."""
super().__init__(feature)
super().__init__(coordinator, feature)
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
@@ -135,33 +142,40 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
"""Return whether cover is closed."""
return self._is_state(CoverState.CLOSED)
@blebox_command
async def async_open_cover(self, **kwargs: Any) -> None:
"""Fully open the cover position."""
await self._feature.async_open()
@blebox_command
async def async_close_cover(self, **kwargs: Any) -> None:
"""Fully close the cover position."""
await self._feature.async_close()
@blebox_command
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Fully open the cover tilt."""
position = 50 if self._feature.is_tilt_180 else 0
await self._feature.async_set_tilt_position(position)
@blebox_command
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Fully close the cover tilt."""
# note: values are reversed
await self._feature.async_set_tilt_position(100)
@blebox_command
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Set the cover position."""
position = kwargs[ATTR_POSITION]
await self._feature.async_set_position(100 - position)
@blebox_command
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self._feature.async_stop()
@blebox_command
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Set the tilt position."""
position = kwargs[ATTR_TILT_POSITION]
@@ -0,0 +1,33 @@
"""Diagnostics support for BleBox devices."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import BleBoxConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: BleBoxConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
product = entry.runtime_data.box
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device": {
"name": product.name,
"type": product.type,
"model": product.model,
"unique_id": product.unique_id,
"firmware_version": product.firmware_version,
"hardware_version": product.hardware_version,
"available_firmware_version": product.available_firmware_version,
"api_version": product.api_version,
"last_data": product.last_data,
},
}
+5 -15
View File
@@ -1,23 +1,20 @@
"""Base entity for the BleBox devices integration."""
import logging
from blebox_uniapi.error import Error
from blebox_uniapi.feature import Feature
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
from .coordinator import BleBoxCoordinator
class BleBoxEntity[_FeatureT: Feature](Entity):
class BleBoxEntity[_FeatureT: Feature](CoordinatorEntity[BleBoxCoordinator]):
"""Implements a common class for entities representing a BleBox feature."""
def __init__(self, feature: _FeatureT) -> None:
def __init__(self, coordinator: BleBoxCoordinator, feature: _FeatureT) -> None:
"""Initialize a BleBox entity."""
super().__init__(coordinator)
self._feature = feature
self._attr_name = feature.full_name
self._attr_unique_id = feature.unique_id
@@ -30,10 +27,3 @@ class BleBoxEntity[_FeatureT: Feature](Entity):
sw_version=product.firmware_version,
configuration_url=f"http://{product.address}",
)
async def async_update(self) -> None:
"""Update the entity state."""
try:
await self._feature.async_update()
except Error as ex:
_LOGGER.error("Updating '%s' failed: %s", self.name, ex)
@@ -0,0 +1,29 @@
{
"entity": {
"button": {
"close": {
"default": "mdi:arrow-down-circle"
},
"down": {
"default": "mdi:arrow-down-circle"
},
"fav": {
"default": "mdi:heart-circle"
},
"open": {
"default": "mdi:arrow-up-circle"
},
"up": {
"default": "mdi:arrow-up-circle"
}
},
"sensor": {
"open_status": {
"default": "mdi:window-open"
},
"power_consumption": {
"default": "mdi:lightning-bolt"
}
}
}
}
+23 -13
View File
@@ -1,6 +1,5 @@
"""BleBox light entities implementation."""
from datetime import timedelta
import logging
import math
from typing import Any
@@ -20,15 +19,18 @@ from homeassistant.components.light import (
LightEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .const import DOMAIN, LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
from .util import blebox_command
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 1
async def async_setup_entry(
@@ -37,11 +39,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
coordinator = config_entry.runtime_data
entities = [
BleBoxLightEntity(feature)
for feature in config_entry.runtime_data.features.get("lights", [])
BleBoxLightEntity(coordinator, feature)
for feature in coordinator.box.features.get("lights", [])
]
async_add_entities(entities, True)
async_add_entities(entities)
COLOR_MODE_MAP = {
@@ -61,9 +64,11 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
_attr_min_color_temp_kelvin = LIGHT_MIN_KELVINS
_attr_max_color_temp_kelvin = LIGHT_MAX_KELVINS
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.light.Light
) -> None:
"""Initialize a BleBox light."""
super().__init__(feature)
super().__init__(coordinator, feature)
if feature.effect_list:
self._attr_supported_features = LightEntityFeature.EFFECT
@@ -165,6 +170,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
return None
return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbww_hex))
@blebox_command
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
@@ -210,8 +216,10 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
try:
await self._feature.async_on(value)
except ValueError as exc:
raise ValueError(
f"Turning on '{self.name}' failed: Bad value {value}"
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="bad_value",
translation_placeholders={"error": str(exc)},
) from exc
if effect is not None:
@@ -219,11 +227,13 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
effect_value = self.effect_list.index(effect)
await self._feature.async_api_command("effect", effect_value)
except ValueError as exc:
raise ValueError(
f"Turning on with effect '{self.name}' failed: {effect} not in"
" effect list."
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="effect_not_found",
translation_placeholders={"error": str(exc)},
) from exc
@blebox_command
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self._feature.async_off()
+65 -27
View File
@@ -1,6 +1,8 @@
"""BleBox sensor entities."""
from datetime import datetime, timedelta
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
import blebox_uniapi.sensor
@@ -26,94 +28,126 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import BleBoxConfigEntry
from .const import OPEN_STATUS
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 0
SENSOR_TYPES = (
SensorEntityDescription(
@dataclass(kw_only=True, frozen=True)
class BleBoxSensorEntityDescription(SensorEntityDescription):
"""Describes a BleBox sensor entity."""
value_fn: Callable[[StateType], StateType] = lambda v: v
SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
BleBoxSensorEntityDescription(
key="pm1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="pm2_5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="pm10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="powerConsumption",
translation_key="power_consumption",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
icon="mdi:lightning-bolt",
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="wind",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="forwardActiveEnergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="reverseActiveEnergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="reactivePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="activePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="apparentPower",
device_class=SensorDeviceClass.APPARENT_POWER,
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="frequency",
device_class=SensorDeviceClass.FREQUENCY,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
),
BleBoxSensorEntityDescription(
key="openStatus",
translation_key="open_status",
device_class=SensorDeviceClass.ENUM,
options=list(OPEN_STATUS.values()),
value_fn=lambda v: OPEN_STATUS.get(int(v)) if v is not None else None,
),
)
@@ -124,31 +158,35 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
coordinator = config_entry.runtime_data
entities = [
BleBoxSensorEntity(feature, description)
for feature in config_entry.runtime_data.features.get("sensors", [])
BleBoxSensorEntity(coordinator, feature, description)
for feature in coordinator.box.features.get("sensors", [])
for description in SENSOR_TYPES
if description.key == feature.device_class
]
async_add_entities(entities, True)
async_add_entities(entities)
class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEntity):
"""Representation of a BleBox sensor feature."""
entity_description: BleBoxSensorEntityDescription
def __init__(
self,
coordinator: BleBoxCoordinator,
feature: blebox_uniapi.sensor.BaseSensor,
description: SensorEntityDescription,
description: BleBoxSensorEntityDescription,
) -> None:
"""Initialize a BleBox sensor feature."""
super().__init__(feature)
super().__init__(coordinator, feature)
self.entity_description = description
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state."""
return self._feature.native_value
return self.entity_description.value_fn(self._feature.native_value)
@property
def last_reset(self) -> datetime | None:
+46 -1
View File
@@ -2,7 +2,9 @@
"config": {
"abort": {
"address_already_configured": "A BleBox device is already configured at {address}.",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The device identifier does not match the previously configured device."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -11,6 +13,16 @@
},
"flow_title": "{name} ({host})",
"step": {
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "Update the connection settings for your BleBox device.",
"title": "Reconfigure BleBox device"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::ip%]",
@@ -22,5 +34,38 @@
"title": "Set up your BleBox device"
}
}
},
"entity": {
"sensor": {
"open_status": {
"state": {
"ajar": "Ajar",
"closed": "[%key:common::state::closed%]",
"closed_but_unlocked": "Closed but unlocked",
"open": "[%key:common::state::open%]",
"unclosed_or_unlocked": "Unclosed or unlocked"
}
}
}
},
"exceptions": {
"bad_value": {
"message": "Turning on the light failed: {error}"
},
"command_failed": {
"message": "Failed to execute command on the BleBox device: {error}"
},
"data_update_failed": {
"message": "An error occurred while communicating with the BleBox device: {error}"
},
"effect_not_found": {
"message": "The specified light effect is not available on this device: {error}"
},
"install_failed": {
"message": "Failed to install firmware update on the BleBox device: {error}"
},
"update_failed": {
"message": "Failed to fetch firmware update information from the BleBox device: {error}"
}
}
}
+8 -5
View File
@@ -1,6 +1,5 @@
"""BleBox switch implementation."""
from datetime import timedelta
from typing import Any
import blebox_uniapi.switch
@@ -11,8 +10,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .entity import BleBoxEntity
from .util import blebox_command
SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 1
async def async_setup_entry(
@@ -21,11 +21,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox switch entity."""
coordinator = config_entry.runtime_data
entities = [
BleBoxSwitchEntity(feature)
for feature in config_entry.runtime_data.features.get("switches", [])
BleBoxSwitchEntity(coordinator, feature)
for feature in coordinator.box.features.get("switches", [])
]
async_add_entities(entities, True)
async_add_entities(entities)
class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity):
@@ -38,10 +39,12 @@ class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity
"""Return whether switch is on."""
return self._feature.is_on
@blebox_command
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
await self._feature.async_turn_on()
@blebox_command
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
await self._feature.async_turn_off()
+26 -7
View File
@@ -18,8 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from . import BleBoxConfigEntry
from .const import DOMAIN
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(hours=1)
@@ -33,11 +36,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox update entry."""
coordinator = config_entry.runtime_data
entities = [
BleBoxUpdateEntity(feature)
for feature in config_entry.runtime_data.features.get("updates", [])
BleBoxUpdateEntity(coordinator, feature)
for feature in coordinator.box.features.get("updates", [])
]
async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)
class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity):
@@ -48,9 +52,16 @@ class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
def __init__(self, feature: blebox_uniapi.update.Update) -> None:
@property
def should_poll(self) -> bool:
"""Return True because firmware versions cannot be fetched via coordinator."""
return True
def __init__(
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.update.Update
) -> None:
"""Initialize the update entity."""
super().__init__(feature)
super().__init__(coordinator, feature)
self._in_progress_old_version: str | None = None
self._poll_cancel: CALLBACK_TYPE | None = None
self._poll_attempts: int = 0
@@ -76,7 +87,11 @@ class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity
try:
await self._feature.async_update()
except Error as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(ex)},
) from ex
self._sync_sw_version()
@property
@@ -111,7 +126,11 @@ class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity
await self._feature.async_install()
except Error as ex:
self._reset_progress()
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="install_failed",
translation_placeholders={"error": str(ex)},
) from ex
self._poll_cancel = async_call_later(
self.hass, _POLL_INTERVAL_SECONDS, self._poll_until_updated
)
+34
View File
@@ -0,0 +1,34 @@
"""Utilities for BleBox."""
from collections.abc import Awaitable, Callable, Coroutine
from typing import Any, Concatenate
from blebox_uniapi.error import Error
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .entity import BleBoxEntity
def blebox_command[_BleBoxEntityT: BleBoxEntity, **_P, _R](
func: Callable[Concatenate[_BleBoxEntityT, _P], Awaitable[_R]],
) -> Callable[Concatenate[_BleBoxEntityT, _P], Coroutine[Any, Any, _R]]:
"""Decorate BleBox calls that send commands to the device.
Catches BleBox errors and refreshes the coordinator after the command.
"""
async def handler(self: _BleBoxEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
try:
return await func(self, *args, **kwargs)
except Error as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"error": str(err)},
) from err
finally:
await self.coordinator.async_refresh()
return handler
-4
View File
@@ -169,9 +169,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
try:
await self._camera.save_recent_clips(output_dir=file_path)
except OSError as err:
# pylint: disable-next=home-assistant-exception-message-with-translation
raise ServiceValidationError(
str(err),
translation_domain=DOMAIN,
translation_key="cant_write",
) from err
@@ -191,9 +189,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
try:
await self._camera.video_to_file(filename)
except OSError as err:
# pylint: disable-next=home-assistant-exception-message-with-translation
raise ServiceValidationError(
str(err),
translation_domain=DOMAIN,
translation_key="cant_write",
) from err
+1 -1
View File
@@ -54,7 +54,7 @@
},
"exceptions": {
"cant_write": {
"message": "Can't write to file."
"message": "Can't write to file, check logs for details."
},
"failed_arm": {
"message": "Blink failed to arm camera."
+12 -6
View File
@@ -6,6 +6,7 @@ These APIs are the only documented way to interact with the bluetooth integratio
import asyncio
from asyncio import Future
from collections.abc import Callable, Iterable
from contextlib import ExitStack
from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
@@ -178,15 +179,20 @@ async def async_process_advertisements(
if not done.done() and callback(service_info):
done.set_result(service_info)
unload = _get_manager(hass).async_register_callback(
_async_discovered_device, match_dict, mode, scan_duration=timeout
)
manager = _get_manager(hass)
with ExitStack() as stack:
unload = manager.async_register_callback(
_async_discovered_device, match_dict, mode
)
stack.callback(unload)
if mode == BluetoothScanningMode.ACTIVE:
task = hass.async_create_task(manager.async_request_active_scan(timeout))
stack.callback(task.cancel)
try:
async with asyncio.timeout(timeout):
return await done
finally:
unload()
@hass_callback
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.16",
"habluetooth==6.8.1"
"habluetooth==6.8.3"
]
}
+10 -18
View File
@@ -1,18 +1,19 @@
"""The Brands integration."""
from collections import deque
from collections.abc import Container, Mapping
from http import HTTPStatus
import logging
from pathlib import Path
from random import SystemRandom
import time
from typing import Any, Final
from typing import Any, Final, override
from aiohttp import ClientError, hdrs, web
from aiohttp import ClientError, web
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback, valid_domain
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -108,23 +109,18 @@ def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
class _BrandsBaseView(HomeAssistantView):
"""Base view for serving brand images."""
requires_auth = False
use_query_token_for_auth = True
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the view."""
self._hass = hass
self._cache_dir = Path(hass.config.cache_path(DOMAIN))
def _authenticate(self, request: web.Request) -> None:
"""Authenticate the request using Bearer token or query token."""
access_tokens: deque[str] = self._hass.data[DOMAIN]
authenticated = (
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
)
if not authenticated:
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
raise web.HTTPForbidden
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
return self._hass.data[DOMAIN]
async def _serve_from_custom_integration(
self,
@@ -240,8 +236,6 @@ class BrandsIntegrationView(_BrandsBaseView):
image: str,
) -> web.Response:
"""Handle GET request for an integration brand image."""
self._authenticate(request)
if not valid_domain(domain) or image not in ALLOWED_IMAGES:
return web.Response(status=HTTPStatus.NOT_FOUND)
@@ -274,8 +268,6 @@ class BrandsHardwareView(_BrandsBaseView):
image: str,
) -> web.Response:
"""Handle GET request for a hardware brand image."""
self._authenticate(request)
if not CATEGORY_RE.match(category):
return web.Response(status=HTTPStatus.NOT_FOUND)
# Hardware images have dynamic names like "manufacturer_model.png"
@@ -230,10 +230,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
entry.minor_version,
)
if entry.version > 1:
# Downgraded from a future version; cannot migrate.
return False
# 1.1 -> 1.2: Add CONF_HEATING_CIRCUITS. Attempt to discover available
# heating circuits from the device; fall back to [1] (pre-multi-circuit
# default) if the device is unreachable or the endpoint is unsupported.
@@ -183,7 +183,6 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
try:
await self.coordinator.client.thermostat(**data, circuit=self._circuit)
except BSBLANError as err:
# pylint: disable-next=home-assistant-exception-message-with-translation
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_data_error",
@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"quality_scale": "platinum",
"requirements": ["aiostreammagic==2.13.1"],
"requirements": ["aiostreammagic==2.13.2"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
+14 -18
View File
@@ -2,7 +2,7 @@
import asyncio
import collections
from collections.abc import Awaitable, Callable, Coroutine
from collections.abc import Awaitable, Callable, Container, Coroutine, Mapping
from contextlib import suppress
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
@@ -12,16 +12,16 @@ import logging
import os
from random import SystemRandom
import time
from typing import Any, Final, final
from typing import Any, Final, final, override
from aiohttp import hdrs, web
from aiohttp import web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
@@ -776,30 +776,26 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
class CameraView(HomeAssistantView):
"""Base CameraView."""
requires_auth = False
use_query_token_for_auth = True
def __init__(self, component: EntityComponent[Camera]) -> None:
"""Initialize a basic camera view."""
self.component = component
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (camera := self.component.get_entity(match_info["entity_id"])) is None:
return ()
return camera.access_tokens
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
if (camera := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in camera.access_tokens
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or camera access token
raise web.HTTPForbidden
if not camera.is_on:
_LOGGER.debug("Camera is off")
raise web.HTTPServiceUnavailable
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
"iot_class": "cloud_push",
"loggers": ["dio_chacon_api"],
"requirements": ["dio-chacon-wifi-api==1.2.2"]
"requirements": ["dio-chacon-wifi-api==1.3.0"]
}
@@ -24,6 +24,7 @@ from homeassistant.components.alexa import (
entities as alexa_entities,
errors as alexa_errors,
)
from homeassistant.components.frontend import DATA_THEMES
from homeassistant.components.google_assistant import helpers as google_helpers
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
@@ -508,6 +509,15 @@ class DownloadSupportPackageView(HomeAssistantView):
"custom_integrations": custom_integrations,
}
@callback
def _get_themes_info(self, hass: HomeAssistant) -> dict[str, Any]:
"""Collect information about user-installed custom themes."""
themes: dict[str, Any] = hass.data.get(DATA_THEMES, {})
return {
"count": len(themes),
"themes": sorted(themes),
}
async def _generate_markdown(
self,
hass: HomeAssistant,
@@ -569,6 +579,25 @@ class DownloadSupportPackageView(HomeAssistantView):
)
markdown += "\n</details>\n\n"
# Add custom themes information
try:
themes_info = self._get_themes_info(hass)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
markdown += "## Custom Themes\n\n"
markdown += "Unable to collect themes information\n\n"
else:
markdown += "## Custom Themes\n\n"
markdown += f"Custom themes: {themes_info['count']}\n\n"
if themes_info["themes"]:
markdown += "<details><summary>Custom themes</summary>\n\n"
markdown += "Name\n"
markdown += "---\n"
for theme in themes_info["themes"]:
markdown += f"{theme}\n"
markdown += "\n</details>\n\n"
for domain, domain_info in domains_info.items():
domain_info_md = get_domain_table_markdown(domain_info)
markdown += (
@@ -87,10 +87,6 @@ async def async_migrate_entry(
) -> bool:
"""Migrate old entry."""
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1 and config_entry.minor_version == 1:
device_registry = dr.async_get(hass)
@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import area_registry as ar
from homeassistant.helpers import area_registry as ar, label_registry as lr
@callback
@@ -69,8 +69,9 @@ def websocket_create_area(
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
if "labels" in data:
# Convert labels to a set
data["labels"] = set(data["labels"])
# Strip labels which are not in the label registry
labels = set(data["labels"])
data["labels"] = labels - lr.async_get_missing_label_ids(hass, labels)
try:
entry = registry.async_create(**data)
@@ -139,8 +140,11 @@ def websocket_update_area(
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
if "labels" in data:
# Convert labels to a set
data["labels"] = set(data["labels"])
# Strip labels which are not in the label registry. This also cleans up
# any stale labels already stored on the area (e.g. left behind by a
# deleted label) the next time it is saved.
labels = set(data["labels"])
data["labels"] = labels - lr.async_get_missing_label_ids(hass, labels)
try:
entry = registry.async_update(**data)
@@ -9,7 +9,7 @@ from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import require_admin
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, label_registry as lr
from homeassistant.helpers.device_registry import DeviceEntry, DeviceEntryDisabler
@@ -84,8 +84,11 @@ def websocket_update_device(
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
if "labels" in msg:
# Convert labels to a set
msg["labels"] = set(msg["labels"])
# Strip labels which are not in the label registry. This also cleans up
# any stale labels already stored on the device (e.g. left behind by a
# deleted label) the next time it is saved.
labels = set(msg["labels"])
msg["labels"] = labels - lr.async_get_missing_label_ids(hass, labels)
entry = cast(DeviceEntry, registry.async_update_device(**msg))
@@ -13,6 +13,7 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
label_registry as lr,
)
from homeassistant.helpers.json import json_dumps
@@ -234,8 +235,11 @@ def websocket_update_entity(
aliases.append(alias)
if "labels" in msg:
# Convert labels to a set
changes["labels"] = set(msg["labels"])
# Strip labels which are not in the label registry. This also cleans up
# any stale labels already stored on the entity (e.g. left behind by a
# deleted label) the next time it is saved.
labels = set(msg["labels"])
changes["labels"] = labels - lr.async_get_missing_label_ids(hass, labels)
if "disabled_by" in msg and msg["disabled_by"] is None:
# Don't allow enabling an entity of a disabled device
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.6.1"]
"requirements": ["hassil==3.6.0", "home-assistant-intents==2026.6.1"]
}
@@ -1,8 +1,11 @@
"""Offer sentence based automation rules."""
from collections.abc import Awaitable, Callable
import re
from typing import Any
from hassil.parse_expression import parse_sentence
from hassil.parser import ParseError
from hassil.recognize import RecognizeResult
from hassil.util import (
PUNCTUATION_END,
@@ -31,6 +34,8 @@ TRIGGER_CALLBACK_TYPE = Callable[
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
for sentence in value:
# Exclude {list_references} which may contain punctuation characters.
sentence = _remove_list_references(sentence)
if (
PUNCTUATION_START.search(sentence)
or PUNCTUATION_END.search(sentence)
@@ -42,6 +47,21 @@ def has_no_punctuation(value: list[str]) -> list[str]:
return value
def _remove_list_references(sentence: str) -> str:
"""Remove {list_references} from a sentence for linting."""
return re.sub(r"(?<!\\)\{[^{}]*\}", "", sentence)
def is_valid_sentence(value: list[str]) -> list[str]:
"""Validate result can be parsed by hassil."""
for sentence in value:
try:
parse_sentence(sentence)
except ParseError as err:
raise vol.Invalid(f"invalid sentence: {err}") from err
return value
def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
@@ -58,7 +78,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Required(CONF_COMMAND): vol.All(
cv.ensure_list, [cv.string], has_one_non_empty_item, has_no_punctuation
cv.ensure_list,
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
is_valid_sentence,
),
}
)
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.17.2"],
"requirements": ["pydaikin==2.18.1"],
"zeroconf": ["_dkapi._tcp.local."]
}
@@ -5,7 +5,7 @@ import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError
from data_grand_lyon_ha import DataGrandLyonClient
from data_grand_lyon_ha import DataGrandLyonClient, TclStop, find_tcl_stop_by_id
import voluptuous as vol
from homeassistant.config_entries import (
@@ -18,6 +18,12 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import (
CONF_LINE,
@@ -43,13 +49,6 @@ STEP_RECONFIGURE_SCHEMA = vol.Schema(
}
)
STEP_STOP_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_LINE): str,
vol.Required(CONF_STOP_ID): vol.Coerce(int),
}
)
STEP_VELOV_STATION_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATION_ID): vol.Coerce(int),
@@ -179,33 +178,126 @@ class DataGrandLyonConfigFlow(ConfigFlow, domain=DOMAIN):
class StopSubentryFlowHandler(ConfigSubentryFlow):
"""Handle a subentry flow for adding a Data Grand Lyon stop."""
def __init__(self) -> None:
"""Initialize the flow."""
self._stops: list[TclStop] = []
self._selected_stop: TclStop | None = None
self._selected_stop_id: int | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Handle the user step to add a new stop."""
entry = self._get_entry()
"""Pick a stop from the list fetched from the API, or enter one manually."""
if not self._stops:
if error := await self._async_load_stops():
return self.async_abort(reason=error)
errors: dict[str, str] = {}
if user_input is not None:
line = user_input[CONF_LINE]
stop_id = user_input[CONF_STOP_ID]
unique_id = f"{line}_{stop_id}"
try:
stop_id = int(user_input[CONF_STOP_ID])
except ValueError:
errors[CONF_STOP_ID] = "invalid_stop_id"
else:
self._selected_stop_id = stop_id
self._selected_stop = find_tcl_stop_by_id(self._stops, stop_id)
return await self.async_step_pick_line()
for subentry in entry.subentries.values():
if subentry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
name = f"{line} - Stop {stop_id}"
return self.async_create_entry(
title=name,
data={CONF_LINE: line, CONF_STOP_ID: stop_id},
unique_id=unique_id,
options = [
SelectOptionDict(value=str(stop.id), label=_stop_label(stop))
for stop in sorted(
self._stops, key=lambda s: (s.nom, s.commune or "", s.id or 0)
)
]
schema = vol.Schema(
{
vol.Required(CONF_STOP_ID): SelectSelector(
SelectSelectorConfig(
options=options,
mode=SelectSelectorMode.DROPDOWN,
sort=False,
custom_value=True,
)
)
}
)
return self.async_show_form(
step_id="user",
data_schema=STEP_STOP_DATA_SCHEMA,
data_schema=schema,
errors=errors,
)
async def async_step_pick_line(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Pick a line from the selected stop's desserte, or enter one manually."""
assert self._selected_stop_id is not None
if user_input is not None:
return self._create_stop(
line=user_input[CONF_LINE], stop_id=self._selected_stop_id
)
options = self._selected_stop.desserte if self._selected_stop else []
schema = vol.Schema(
{
vol.Required(CONF_LINE): SelectSelector(
SelectSelectorConfig(
options=options,
mode=SelectSelectorMode.DROPDOWN,
custom_value=True,
)
)
}
)
return self.async_show_form(step_id="pick_line", data_schema=schema)
async def _async_load_stops(self) -> str | None:
"""Fetch TCL stops from the API, returning an error key on failure."""
entry = self._get_entry()
session = async_get_clientsession(self.hass)
client = DataGrandLyonClient(
session=session,
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
try:
self._stops = await client.get_tcl_stops()
except ClientResponseError as err:
if err.status in (401, 403):
return "invalid_auth"
return "cannot_connect"
except ClientError, TimeoutError:
return "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected error fetching Data Grand Lyon TCL stops")
return "unknown"
return None
def _create_stop(self, line: str, stop_id: int) -> SubentryFlowResult:
"""Create the stop subentry, aborting on duplicate."""
entry = self._get_entry()
unique_id = f"{line}_{stop_id}"
for subentry in entry.subentries.values():
if subentry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
return self.async_create_entry(
title=f"{line} - Stop {stop_id}",
data={CONF_LINE: line, CONF_STOP_ID: stop_id},
unique_id=unique_id,
)
def _stop_label(stop: TclStop) -> str:
label = stop.nom
# variable extracted to please codespell.
address = stop.adresse # codespell:ignore adresse
if address or stop.commune:
label += " (" + ", ".join(filter(None, [address, stop.commune])) + ")"
label += f" - {stop.id}"
return label
class VelovStationSubentryFlowHandler(ConfigSubentryFlow):
"""Handle a subentry flow for adding a Vélo'v station."""
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["data-grand-lyon-ha==0.7.0"]
"requirements": ["data-grand-lyon-ha==0.8.0"]
}
@@ -46,17 +46,30 @@
"config_subentries": {
"stop": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"entry_type": "Transit stop",
"error": {
"invalid_stop_id": "Stop ID must be a number."
},
"initiate_flow": {
"user": "Add transit stop"
},
"step": {
"pick_line": {
"data": {
"line": "Line"
}
},
"user": {
"data": {
"line": "Line",
"stop_id": "Stop ID"
"stop_id": "Stop"
},
"data_description": {
"stop_id": "Search by stop name, address or city, or enter a stop ID directly."
}
}
}
+20 -2
View File
@@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -27,7 +28,19 @@ async def async_setup_entry(
BinarySensorDeviceClass.MOISTURE,
),
DemoBinarySensor(
"binary_2", "Movement Backyard", True, BinarySensorDeviceClass.MOTION
"binary_2",
"Movement Backyard",
True,
BinarySensorDeviceClass.MOTION,
),
DemoBinarySensor(
"binary_3",
"Outside Temperature",
False,
BinarySensorDeviceClass.BATTERY_CHARGING,
device_id="sensor_1",
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery Charging",
),
]
)
@@ -46,6 +59,9 @@ class DemoBinarySensor(BinarySensorEntity):
device_name: str,
state: bool,
device_class: BinarySensorDeviceClass,
device_id: str | None = None,
entity_category: EntityCategory | None = None,
entity_name: str | None = None,
) -> None:
"""Initialize the demo sensor."""
self._unique_id = unique_id
@@ -54,10 +70,12 @@ class DemoBinarySensor(BinarySensorEntity):
self._attr_device_info = DeviceInfo(
identifiers={
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, self.unique_id)
(DOMAIN, device_id or unique_id)
},
name=device_name,
)
self._attr_entity_category = entity_category
self._attr_name = entity_name
@property
def unique_id(self) -> str:
@@ -54,10 +54,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry.minor_version,
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
if config_entry.minor_version < 2:
new_options = {**config_entry.options}
@@ -22,6 +22,7 @@ from .const import ( # noqa: F401
ATTR_LOCATION_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
ATTR_TRACKING_TYPE,
CONF_ASSOCIATED_ZONE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
@@ -36,6 +37,7 @@ from .const import ( # noqa: F401
PLATFORM_TYPE_LEGACY,
SCAN_INTERVAL,
SourceType,
TrackingType,
)
from .entity import ( # noqa: F401
BaseScannerEntity,
@@ -25,6 +25,18 @@ class SourceType(StrEnum):
BLUETOOTH_LE = "bluetooth_le"
class TrackingType(StrEnum):
"""Tracking type for device trackers.
Describes how the tracker determines presence: by the device's geographic
position (e.g. GPS) or by its connection to a known endpoint (e.g. a router
or beacon associated with a zone).
"""
CONNECTION = "connection"
POSITION = "position"
CONF_SCAN_INTERVAL: Final = "interval_seconds"
SCAN_INTERVAL: Final = timedelta(seconds=12)
@@ -47,6 +59,7 @@ ATTR_IN_ZONES: Final = "in_zones"
ATTR_LOCATION_NAME: Final = "location_name"
ATTR_MAC: Final = "mac"
ATTR_SOURCE_TYPE: Final = "source_type"
ATTR_TRACKING_TYPE: Final = "tracking_type"
ATTR_CONSIDER_HOME: Final = "consider_home"
ATTR_IP: Final = "ip"
@@ -1,6 +1,7 @@
"""Provide functionality to keep track of devices."""
import asyncio
import logging
from typing import TYPE_CHECKING, Any, final
from propcache.api import cached_property
@@ -22,6 +23,7 @@ from homeassistant.core import (
EventStateChangedData,
HomeAssistant,
State,
async_get_hass_or_none,
callback,
)
from homeassistant.helpers import (
@@ -37,6 +39,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey
from .const import (
@@ -45,13 +48,17 @@ from .const import (
ATTR_IP,
ATTR_MAC,
ATTR_SOURCE_TYPE,
ATTR_TRACKING_TYPE,
CONF_ASSOCIATED_ZONE,
CONNECTED_DEVICE_REGISTERED,
DOMAIN,
LOGGER,
SourceType,
TrackingType,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY: HassKey[dict[str, tuple[str, str]]] = HassKey(f"{DOMAIN}_mac")
@@ -164,11 +171,35 @@ class BaseTrackerEntity(Entity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_source_type: SourceType
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if "battery_level" in cls.__dict__:
if cls.__module__.startswith("homeassistant.components."):
# Don't ask users to report issue for built in integrations,
# they already have issues opened on them.
return
report_issue = async_suggest_report_issue(
async_get_hass_or_none(), module=cls.__module__
)
_LOGGER.warning(
(
"%s::%s is overriding the deprecated battery_level property on "
"a subclass of BaseTrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
cls.__module__,
cls.__name__,
report_issue,
)
@cached_property
def battery_level(self) -> int | None:
"""Return the battery level of the device.
Percentage from 0-100.
The property is deprecated and will be removed in Home Assistant 2027.7.
"""
return None
@@ -209,16 +240,44 @@ class TrackerEntity(
"""Base class for a tracked device."""
entity_description: TrackerEntityDescription
_attr_capability_attributes: dict[str, Any] = {
ATTR_TRACKING_TYPE: TrackingType.POSITION
}
_attr_in_zones: list[str] | None = None
_attr_latitude: float | None = None
_attr_location_accuracy: float = 0
# _attr_location_name is deprecated and will be removed in Home Assistant 2027.7
_attr_location_name: str | None = None
_attr_longitude: float | None = None
_attr_source_type: SourceType = SourceType.GPS
__active_zone: State | None = None
# If we reported setting deprecated _attr_location_name
__deprecated_attr_location_name_reported = False
__in_zones: list[str] | None = None
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if "location_name" in cls.__dict__:
if cls.__module__.startswith("homeassistant.components."):
# Don't ask users to report issue for built in integrations,
# they already have issues opened on them.
return
report_issue = async_suggest_report_issue(
async_get_hass_or_none(), module=cls.__module__
)
_LOGGER.warning(
(
"%s::%s is overriding the deprecated location_name property on "
"an instance of TrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
cls.__module__,
cls.__name__,
report_issue,
)
@cached_property
def should_poll(self) -> bool:
"""No polling for entities that have location pushed."""
@@ -249,7 +308,32 @@ class TrackerEntity(
@cached_property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device."""
"""Return a location name for the current location of the device.
The property is deprecated and will be removed in Home Assistant 2027.7.
"""
if (location_name := self._attr_location_name) is not None:
if (
not self.__deprecated_attr_location_name_reported
and not self.__class__.__module__.startswith(
"homeassistant.components."
)
):
report_issue = async_suggest_report_issue(
self.hass, module=self.__class__.__module__
)
_LOGGER.warning(
(
"%s::%s is setting the deprecated _attr_location_name attribute "
"on an instance of TrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
self.__class__.__module__,
self.__class__.__name__,
report_issue,
)
self.__deprecated_attr_location_name_reported = True
return location_name
return self._attr_location_name
@cached_property
@@ -332,6 +416,9 @@ class BaseScannerEntity(BaseTrackerEntity):
addresses being used to identify the device.
"""
_attr_capability_attributes: dict[str, Any] = {
ATTR_TRACKING_TYPE: TrackingType.CONNECTION
}
_scanner_option_associated_zone: str = zone.ENTITY_ID_HOME
_scanner_option_associated_zone_unsub: CALLBACK_TYPE | None = None
@@ -40,6 +40,13 @@
"gps": "GPS",
"router": "Router"
}
},
"tracking_type": {
"name": "Tracking type",
"state": {
"connection": "Connection",
"position": "Position"
}
}
}
}

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