Compare commits

..

112 Commits

Author SHA1 Message Date
Franck Nijhof 3e6473d130 2025.5.3 (#145516) 2025-05-23 17:09:32 +02:00
Franck Nijhof 9a183bc16a Bump version to 2025.5.3 2025-05-23 14:03:47 +00:00
Robert Resch e540247c14 Bump deebot-client to 13.2.1 (#145492) 2025-05-23 14:03:02 +00:00
rappenze 0aef8b58d8 Bump pyfibaro to 0.8.3 (#145488) 2025-05-23 14:03:01 +00:00
tronikos f0501f917b Fix strings related to Google search tool in Google AI (#145480) 2025-05-23 14:02:59 +00:00
tronikos 97004e13cb Make Gemma models work in Google AI (#145479)
* Make Gemma models work in Google AI

* move one line to be improve readability
2025-05-23 14:02:58 +00:00
tronikos f867a0af24 Bump opower to 0.12.1 (#145464) 2025-05-23 14:02:57 +00:00
Joost Lekkerkerker d3b3839ffa Bump pysmartthings to 3.2.3 (#145444) 2025-05-23 14:02:56 +00:00
starkillerOG 1a227d6a10 Reolink fix device migration (#145443) 2025-05-23 14:02:54 +00:00
Joost Lekkerkerker fc8c403a3a Bump yt-dlp to 2025.05.22 (#145441) 2025-05-23 14:02:53 +00:00
Josef Zweck c1bf596eba Mark backflush binary sensor not supported for GS3 MP in lamarzocco (#145406) 2025-05-23 14:02:52 +00:00
Michael 63f69a9e3d Bump py-synologydsm-api to 2.7.2 (#145403)
bump py-synologydsm-api to 2.7.2
2025-05-23 14:02:51 +00:00
Josef Zweck e13b014b6f Bump pylamarzocco to 2.0.4 (#145402) 2025-05-23 14:02:49 +00:00
c0ffeeca7 be0d4d926c OTBR: remove links to obsolete multiprotocol docs (#145394) 2025-05-23 14:02:48 +00:00
Raj Laud 2403fff81f Bump pysqueezebox to v0.12.1 (#145384) 2025-05-23 14:02:47 +00:00
Andy 8c475787cc Fix: Revert Ecovacs mower total_stats_area unit to square meters (#145380) 2025-05-23 14:02:45 +00:00
peteS-UK d9fe1edd82 Add initial coordinator refresh for players in Squeezebox (#145347)
* initial

* add test for new player
2025-05-23 14:02:44 +00:00
Michael f5cf64700a Fix limit of shown backups on Synology DSM location (#145342) 2025-05-23 14:02:43 +00:00
Josef Zweck 777b04d7a5 Handle more exceptions in azure_storage (#145320) 2025-05-23 14:02:41 +00:00
Josef Zweck 9fc78ed4e2 Add cloud as after_dependency to onedrive (#145301) 2025-05-23 14:02:40 +00:00
Matthew FitzGerald-Chamberlain d03af549d4 Bump pyaprilaire to 0.9.0 (#145260) 2025-05-23 14:02:39 +00:00
G Johansson d91f01243c Bump holidays to 0.73 (#145238) 2025-05-23 14:02:38 +00:00
Martin Hjelmare 5094208db6 Fix Z-Wave config entry unique id after NVM restore (#145221)
* Fix Z-Wave config entry unique id after NVM restore

* Remove stale comment
2025-05-23 14:01:37 +00:00
Simone Chemelli 006f66a841 Bump aiocomelit to 0.12.3 (#145209) 2025-05-23 14:01:36 +00:00
Maikel Punie 64b7d77840 Bump velbusaio to 2025.5.0 (#145198) 2025-05-23 14:01:35 +00:00
Martin Hjelmare abf6a809b8 Fix Z-Wave unique id update during controller migration (#145185) 2025-05-23 14:01:33 +00:00
Martin Hjelmare 1b7dd205c7 Improve Z-Wave config flow tests (#144871)
* Improve Z-Wave config flow tests

* Fix test

* Use identify check for result type
2025-05-23 14:01:32 +00:00
Keilin Bickar 3e00366a61 Bump sense-energy to 0.13.8 (#145156) 2025-05-23 13:50:34 +00:00
karwosts a17275b559 Fix history_stats with sliding window that ends before now (#145117) 2025-05-23 13:50:33 +00:00
Jan-Philipp Benecke 9534a919ce Add missing device condition translations to lock component (#145104) 2025-05-23 13:45:44 +00:00
Joost Lekkerkerker 422dbfef88 Map auto to heat_cool for thermostat in SmartThings (#145098) 2025-05-23 13:45:43 +00:00
Robert Resch 8e44684a61 Fix proberly Ecovacs mower area sensors (#145078) 2025-05-23 13:45:41 +00:00
Manu 642e7fd487 Bump aiontfy to 0.5.2 (#145044) 2025-05-23 13:45:40 +00:00
peteS-UK 9bb9132e7b Fix album and artist returning "None" rather than None for Squeezebox media player. (#144971)
* fix

* snapshot update

* cast type
2025-05-23 13:45:39 +00:00
J. Nick Koston 41be82f167 Bump ESPHome stable BLE version to 2025.5.0 (#144857) 2025-05-23 13:45:37 +00:00
Marc Hörsken 47140e14d9 Postpone update in WMSPro after service call (#144836)
* Reduce stress on WMS WebControl pro with higher scan interval

Avoid delays and connection issues due to overloaded hub.
Fixes #133832 and #134413

* Schedule an entity state update after performing an action

Avoid delaying immediate status updates, e.g. on/off changes.

* Replace scheduled state updates with delayed action completion

Suggested-by: joostlek
2025-05-23 13:45:36 +00:00
TheOneValen 926502b0f1 Allow image send with read-only access (matrix notify) (#144819) 2025-05-23 13:45:35 +00:00
disforw 78351ff7a7 Fix QNAP fail to load (#144675)
* Update coordinator.py

* Update coordinator.py

@peternash

* Update coordinator.py

* Update coordinator.py

* Update coordinator.py

* Update coordinator.py
2025-05-23 13:45:33 +00:00
wuede c333726867 Netatmo: do not fail on schedule updates (#142933)
* do not fail on schedule updates

* add test to check that the store data remains unchanged
2025-05-23 13:45:32 +00:00
Franck Nijhof f66feabaaf 2025.5.2 (#145072)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: TimL <tl@smlight.tech>
Co-authored-by: Seweryn Zeman <seweryn.zeman@jazzy.pro>
Co-authored-by: hahn-th <15319212+hahn-th@users.noreply.github.com>
Co-authored-by: Luke Lashley <conway220@gmail.com>
Co-authored-by: starkillerOG <starkiller.og@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
Co-authored-by: Thomas55555 <59625598+Thomas55555@users.noreply.github.com>
Co-authored-by: Øyvind Matheson Wergeland <oyvind@wergeland.org>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: rjblake <richard.blake@gmail.com>
Co-authored-by: Daniel Hjelseth Høyer <github@dahoiv.net>
Co-authored-by: Matthias Alphart <farmio@alphart.net>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Odd Stråbø <oddstr13@openshell.no>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
fix privacy mode availability for NVR IPC cams (#144569)
fix enphase_envoy diagnostics home endpoint name (#144634)
Close Octoprint aiohttp session on unload (#144670)
Fix strings typo for Comelit (#144672)
Fix wrong state in Husqvarna Automower (#144684)
Fix Netgear handeling of missing MAC in device registry (#144722)
Fix blocking call in azure storage (#144803)
Fix Z-Wave unique id after controller reset (#144813)
Fix blocking call in azure_storage config flow (#144818)
Fix wall connector states in Teslemetry (#144855)
Fix Reolink setup when ONVIF push is unsupported (#144869)
Fix some Home Connect translation strings (#144905)
Fix unknown Pure AQI in Sensibo (#144924)
Fix Home Assistant Yellow config entry data (#144948)
Fix ESPHome entities unavailable if deep sleep enabled after entry setup (#144970)
fix from ZHA event `unique_id` (#145006)
Fix climate idle state for Comelit (#145059)
Fix fan AC mode in SmartThings AC (#145064)
Fix Ecovacs mower area sensors (#145071)
2025-05-16 23:08:52 +02:00
Franck Nijhof 0ef098a9f3 Pin rpds-py to 0.24.0 (#145074) 2025-05-16 20:40:02 +00:00
Franck Nijhof 02b028add3 Bump version to 2025.5.2 2025-05-16 19:31:36 +00:00
Robert Resch 34455f9743 Fix Ecovacs mower area sensors (#145071) 2025-05-16 19:31:15 +00:00
Joost Lekkerkerker 8c4eec231f Don't create entities for Smartthings smarttags (#145066) 2025-05-16 19:31:14 +00:00
Joost Lekkerkerker 621a14d7cc Fix fan AC mode in SmartThings AC (#145064) 2025-05-16 19:31:12 +00:00
Joost Lekkerkerker 4906e78a5c Only set suggested area for new SmartThings devices (#145063) 2025-05-16 19:31:11 +00:00
Bram Kragten 146e440d59 Update frontend to 20250516.0 (#145062) 2025-05-16 19:31:10 +00:00
Joost Lekkerkerker e2ede3ed19 Map SmartThings auto mode correctly (#145061) 2025-05-16 19:31:09 +00:00
Simone Chemelli b76ac68fb1 Fix climate idle state for Comelit (#145059) 2025-05-16 19:31:07 +00:00
Joost Lekkerkerker 0691ad9362 Set SmartThings oven setpoint to unknown if its 1 Fahrenheit (#145038) 2025-05-16 19:31:06 +00:00
Joost Lekkerkerker 715f116954 Bump pySmartThings to 3.2.2 (#145033) 2025-05-16 19:31:05 +00:00
puddly 9f0db98745 Strip _CLIENT suffix from ZHA event unique_id (#145006) 2025-05-16 19:31:03 +00:00
Odd Stråbø 0ba55c31e8 Fix ESPHome entities unavailable if deep sleep enabled after entry setup (#144970) 2025-05-16 19:31:02 +00:00
Robert Resch 19b7cfbd4a Bump deebot-client to 13.2.0 (#144957) 2025-05-16 19:31:01 +00:00
Erik Montnemery a9520888cf Fix Home Assistant Yellow config entry data (#144948) 2025-05-16 19:31:00 +00:00
Matthias Alphart f086f4a955 Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue for new setups (#144940) 2025-05-16 19:30:59 +00:00
G Johansson a657964c25 Fix unknown Pure AQI in Sensibo (#144924)
* Fix unknown Pure AQI in Sensibo

* Fix mypy
2025-05-16 19:30:57 +00:00
Daniel Hjelseth Høyer 543104b36c Update mill library 0.12.5 (#144911)
* Update mill library 0.12.5

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Update mill library 0.12.5

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

---------

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-05-16 19:30:56 +00:00
Daniel Hjelseth Høyer bf1d2069e4 Update Tibber lib 0.31.2 (#144908)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-05-16 19:30:55 +00:00
rjblake e5e1c9fb05 Fix some Home Connect translation strings (#144905)
* Update strings.json

Corrected program names:
changed "Pre_rinse" to "Pre-Rinse"
changed "Kurz 60°C" to "Speed 60°C"

Both match the Home Connect app; although the UK documentation refers to "Speed 60°C" as "Quick 60°C"

* Adjust casing

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-05-16 19:30:53 +00:00
starkillerOG 4c4be88323 Fix Reolink setup when ONVIF push is unsupported (#144869)
* Fix setup when ONVIF push is not supported

* fix styling
2025-05-16 19:30:52 +00:00
Brett Adams 5a83627dc5 Fix wall connector states in Teslemetry (#144855)
* Fix wall connector

* Update snapshot
2025-05-16 19:30:51 +00:00
Allen Porter 3123a7b168 Bump ical to 9.2.4 (#144852) 2025-05-16 19:30:50 +00:00
Luke Lashley 8161ce6ea8 Bump python-snoo to 0.6.6 (#144849) 2025-05-16 19:30:49 +00:00
Josef Zweck d9cbd1b65f Bump pylamarzocco to 2.0.3 (#144825) 2025-05-16 19:30:47 +00:00
Josef Zweck b7c07209b8 Fix blocking call in azure_storage config flow (#144818)
* Fix blocking call in azure_storage config flow

* Fix blocking call in azure_storage config_flow as well

* move session getting to event flow
2025-05-16 19:30:46 +00:00
Martin Hjelmare 6c3a4f17f0 Fix Z-Wave unique id after controller reset (#144813) 2025-05-16 19:30:45 +00:00
Josef Zweck d82feb807f Fix blocking call in azure storage (#144803) 2025-05-16 19:30:43 +00:00
Jan Bouwhuis c373fa9296 Do not show an empty component name on MQTT device subentries not as None if it is not set (#144792) 2025-05-16 19:30:42 +00:00
starkillerOG 139b48440f Cleanup wrongly combined Reolink devices (#144771) 2025-05-16 19:30:41 +00:00
Joost Lekkerkerker 9de1d3b143 Fill in Plaato URL via placeholders (#144754) 2025-05-16 19:29:35 +00:00
Martin Hjelmare b69ebdaecb Repair Z-Wave unknown controller (#144738)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-05-16 19:23:25 +00:00
starkillerOG f25e50b017 Fix Netgear handeling of missing MAC in device registry (#144722) 2025-05-16 19:23:24 +00:00
Simone Chemelli a4a7601f9f Bump aiocomelit to 0.12.1 (#144720) 2025-05-16 19:23:22 +00:00
Øyvind Matheson Wergeland 41a503f76f Bump gcal-sync to 7.0.1 (#144718)
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-05-16 19:23:21 +00:00
Allen Porter f1a3d62db2 Bump ical to 9.2.2 (#144713) 2025-05-16 19:23:20 +00:00
Allen Porter e465276464 Bump voluptuous-openapi to 0.1.0 (#144703) 2025-05-16 19:23:18 +00:00
Thomas55555 47b45444eb Fix wrong state in Husqvarna Automower (#144684) 2025-05-16 19:22:02 +00:00
Simone Chemelli cf0911cc56 Avoid closing shared session for Comelit (#144682) 2025-05-16 19:22:00 +00:00
Simone Chemelli da79d5b2e3 Fix strings typo for Comelit (#144672) 2025-05-16 19:21:59 +00:00
G Johansson 358b0c1c17 Bump holidays to 0.72 (#144671) 2025-05-16 19:21:58 +00:00
Ruben van Dijk 543348fe58 Close Octoprint aiohttp session on unload (#144670) 2025-05-16 19:21:57 +00:00
Simon Lamon 0635856761 Bump python-linkplay to v0.2.5 (#144666)
Bump linkplay to 0.2.5
2025-05-16 19:21:55 +00:00
Allen Porter 081afe6034 Bump ical to 9.2.1 (#144642) 2025-05-16 19:21:54 +00:00
Arie Catsman ca14322227 bump pyenphase to 1.26.1 (#144641) 2025-05-16 19:21:53 +00:00
Josef Zweck a54816a6e5 Bump pylamarzocco to 2.0.2 (#144635)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-05-16 19:21:51 +00:00
Arie Catsman 27db4e90b5 fix enphase_envoy diagnostics home endpoint name (#144634) 2025-05-16 19:21:50 +00:00
J. Nick Koston e9cc624d93 Mark inkbird coordinator as not needing connectable (#144584) 2025-05-16 19:19:59 +00:00
starkillerOG 5a95f43992 Bump reolink_aio to 0.13.3 (#144583) 2025-05-16 19:19:58 +00:00
J. Nick Koston 36a35132c0 Bump aiodiscover to 2.7.0 (#144571) 2025-05-16 19:19:57 +00:00
starkillerOG 2fbc75f89b Reolink fix privacy mode availability for NVR IPC cams (#144569)
* Correct "available" for IPC cams

* Check privacy mode when updating
2025-05-16 19:19:56 +00:00
Luke Lashley 48aa6be889 Don't scale Roborock mop Path (#144421)
don't scale mop path
2025-05-16 19:19:55 +00:00
hahn-th bde04bc47b Doorbell Event is fired just once in homematicip_cloud (#144357)
* fire event if event type if correct

* Fix requested changes
2025-05-16 19:19:53 +00:00
Seweryn Zeman 7d163aa659 Removed unused file_id param from open_ai_conversation request (#143878) 2025-05-16 19:19:52 +00:00
TimL 010b044379 Allow dns hostnames to be retained for SMLIGHT user flow. (#142514)
* Dont overwrite host with local IP

* adjust test for user flow change
2025-05-16 19:19:50 +00:00
Franck Nijhof 00627b82e0 2025.5.1 (#144564) 2025-05-09 17:03:40 +02:00
Franck Nijhof 13aba6201e Bump version to 2025.5.1 2025-05-09 13:29:29 +00:00
starkillerOG f392e0c1c7 Prevent errors during cleaning of connections/identifiers in device registry (#144558) 2025-05-09 13:28:33 +00:00
starkillerOG 181eca6c82 Reolink clean device registry mac (#144554) 2025-05-09 13:28:32 +00:00
Bram Kragten 196d923ac6 Update frontend to 20250509.0 (#144549) 2025-05-09 13:28:30 +00:00
Josef Zweck 4ad387c967 Fix statistics coordinator subscription for lamarzocco (#144541) 2025-05-09 13:28:29 +00:00
J. Nick Koston cb475bf153 Bump aiodns to 3.4.0 (#144511) 2025-05-09 13:28:28 +00:00
Michael 47acceea08 Fix removing of smarthome templates on startup of AVM Fritz!SmartHome integration (#144506) 2025-05-09 13:28:26 +00:00
J. Nick Koston fd6fb7e3bc Bump forecast-solar to 4.2.0 (#144502) 2025-05-09 13:28:25 +00:00
Erik Montnemery 30f7e9b441 Don't encrypt or decrypt unknown files in backup archives (#144495) 2025-05-09 13:28:24 +00:00
Matthias Alphart a8beec2691 Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue (#144463) 2025-05-09 13:28:23 +00:00
Fredrik Erlandsson 23244fb79f Fix point import error (#144462)
* fix import error

* fix failing tests

* Apply suggestions from code review

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-09 13:28:22 +00:00
Martin Hjelmare e5c56629e2 Fix Z-Wave reset accumulated values button entity category (#144459) 2025-05-09 13:28:20 +00:00
Josef Zweck a793503c8a Bump pylamarzocco to 2.0.1 (#144454) 2025-05-09 13:28:19 +00:00
DukeChocula 054c7a0adc Add LAP-V102S-AUSR to VeSync (#144437)
Update const.py

Added LAP-V102S-AUSR to Vital 100S
2025-05-09 13:28:18 +00:00
Tamer Wahba 6eb2d1aa7c fix homekit air purifier temperature sensor to convert unit (#144435) 2025-05-09 13:28:16 +00:00
Martin Hjelmare 619fdea5df Fix Z-Wave restore nvm command to wait for driver ready (#144413) 2025-05-09 13:28:15 +00:00
1339 changed files with 10743 additions and 39769 deletions
+28 -32
View File
@@ -37,10 +37,10 @@ on:
type: boolean
env:
CACHE_VERSION: 2
CACHE_VERSION: 12
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.6"
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.5"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -259,7 +259,7 @@ jobs:
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -276,7 +276,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true'
@@ -306,7 +306,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -315,7 +315,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Run ruff-format
run: |
@@ -346,7 +346,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -355,7 +355,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Run ruff
run: |
@@ -386,7 +386,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -395,7 +395,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Register yamllint problem matcher
@@ -501,7 +501,7 @@ jobs:
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -509,10 +509,10 @@ jobs:
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-uv-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Install additional OS dependencies
@@ -598,7 +598,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run hassfest
run: |
@@ -631,7 +631,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run gen_requirements_all.py
run: |
@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Dependency review
uses: actions/dependency-review-action@v4.7.0
uses: actions/dependency-review-action@v4.6.0
with:
license-check: false # We use our own license audit checks
@@ -688,7 +688,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Extract license data
run: |
@@ -731,7 +731,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher
run: |
@@ -778,7 +778,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher
run: |
@@ -830,17 +830,17 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.2.3
with:
path: .mypy_cache
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-mypy-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-mypy-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-mypy-${{
env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Register mypy problem matcher
@@ -900,7 +900,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run split_tests.py
run: |
@@ -959,8 +959,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
@@ -1085,8 +1084,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
@@ -1220,8 +1218,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
@@ -1372,8 +1369,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.17
uses: github/codeql-action/init@v3.28.16
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.17
uses: github/codeql-action/analyze@v3.28.16
with:
category: "/language:python"
+1 -1
View File
@@ -332,7 +332,6 @@ homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.met_eireann.*
homeassistant.components.metoffice.*
homeassistant.components.miele.*
homeassistant.components.mikrotik.*
homeassistant.components.min_max.*
homeassistant.components.minecraft_server.*
@@ -434,6 +433,7 @@ homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.rpi_power.*
homeassistant.components.rss_feed_template.*
homeassistant.components.rtsp_to_webrtc.*
homeassistant.components.russound_rio.*
homeassistant.components.ruuvi_gateway.*
homeassistant.components.ruuvitag_ble.*
Generated
+8 -8
View File
@@ -46,8 +46,8 @@ build.json @home-assistant/supervisor
/tests/components/accuweather/ @bieniu
/homeassistant/components/acmeda/ @atmurray
/tests/components/acmeda/ @atmurray
/homeassistant/components/adax/ @danielhiversen @lazytarget
/tests/components/adax/ @danielhiversen @lazytarget
/homeassistant/components/adax/ @danielhiversen
/tests/components/adax/ @danielhiversen
/homeassistant/components/adguard/ @frenck
/tests/components/adguard/ @frenck
/homeassistant/components/ads/ @mrpasztoradam
@@ -455,8 +455,8 @@ build.json @home-assistant/supervisor
/tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb
/tests/components/evohome/ @zxdavb
/homeassistant/components/ezviz/ @RenierM26
/tests/components/ezviz/ @RenierM26
/homeassistant/components/ezviz/ @RenierM26 @baqs
/tests/components/ezviz/ @RenierM26 @baqs
/homeassistant/components/faa_delays/ @ntilley905
/tests/components/faa_delays/ @ntilley905
/homeassistant/components/fan/ @home-assistant/core
@@ -1111,8 +1111,8 @@ build.json @home-assistant/supervisor
/tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya
/tests/components/openuv/ @bachya
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi
/tests/components/openweathermap/ @fabaff @freekode @nzapponi
/homeassistant/components/opnsense/ @mtreinish
/tests/components/opnsense/ @mtreinish
/homeassistant/components/opower/ @tronikos
@@ -1307,6 +1307,8 @@ build.json @home-assistant/supervisor
/tests/components/rpi_power/ @shenxn @swetoast
/homeassistant/components/rss_feed_template/ @home-assistant/core
/tests/components/rss_feed_template/ @home-assistant/core
/homeassistant/components/rtsp_to_webrtc/ @allenporter
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/homeassistant/components/russound_rio/ @noahhusby
@@ -1794,8 +1796,6 @@ build.json @home-assistant/supervisor
/tests/components/zeversolar/ @kvanzuijlen
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/homeassistant/components/zimi/ @markhannon
/tests/components/zimi/ @markhannon
/homeassistant/components/zodiac/ @JulienTant
/tests/components/zodiac/ @JulienTant
/homeassistant/components/zone/ @home-assistant/core
+1 -1
View File
@@ -1,7 +1,7 @@
{
"domain": "adax",
"name": "Adax",
"codeowners": ["@danielhiversen", "@lazytarget"],
"codeowners": ["@danielhiversen"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
@@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
from .models import AdvantageAirData
@@ -52,8 +52,8 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
self._id: str = light["id"]
self._attr_unique_id += f"-{self._id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
via_device=(DOMAIN, self.coordinator.data["system"]["rid"]),
identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)},
via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]),
manufacturer="Advantage Air",
model=light.get("moduleType"),
name=light["name"],
@@ -6,7 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import DOMAIN
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirEntity
from .models import AdvantageAirData
@@ -32,7 +32,9 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
"""Initialize the Advantage Air App."""
super().__init__(instance)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])},
identifiers={
(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"])
},
manufacturer="Advantage Air",
model=self.coordinator.data["system"]["sysType"],
name=self.coordinator.data["system"]["name"],
@@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, SERVER_URL
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL
ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
@@ -46,7 +46,7 @@ async def async_setup_entry(
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, agent_client.unique)},
identifiers={(AGENT_DOMAIN, agent_client.unique)},
manufacturer="iSpyConnect",
name=f"Agent {agent_client.name}",
model="Agent DVR",
@@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AgentDVRConfigEntry
from .const import DOMAIN
from .const import DOMAIN as AGENT_DOMAIN
CONF_HOME_MODE_NAME = "home"
CONF_AWAY_MODE_NAME = "away"
@@ -47,7 +47,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
self._client = client
self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, client.unique)},
identifiers={(AGENT_DOMAIN, client.unique)},
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
manufacturer="Agent",
model=CONST_ALARM_CONTROL_PANEL_NAME,
@@ -3,19 +3,6 @@
"name": "Airthings",
"codeowners": ["@danielhiversen", "@LaStrada"],
"config_flow": true,
"dhcp": [
{
"hostname": "airthings-view"
},
{
"hostname": "airthings-hub",
"macaddress": "D0141190*"
},
{
"hostname": "airthings-hub",
"macaddress": "70B3D52A0*"
}
],
"documentation": "https://www.home-assistant.io/integrations/airthings",
"iot_class": "cloud_polling",
"loggers": ["airthings"],
@@ -14,7 +14,6 @@ from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
@@ -79,12 +78,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="light",
state_class=SensorStateClass.MEASUREMENT,
),
"lux": SensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
"virusRisk": SensorEntityDescription(
key="virusRisk",
translation_key="virus_risk",
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Choose AlarmDecoder protocol",
"title": "Choose AlarmDecoder Protocol",
"data": {
"protocol": "Protocol"
}
@@ -12,8 +12,8 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"device_baudrate": "Device baud rate",
"device_path": "Device path"
"device_baudrate": "Device Baud Rate",
"device_path": "Device Path"
},
"data_description": {
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
@@ -44,36 +44,36 @@
"arm_settings": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"data": {
"auto_bypass": "Auto-bypass on arm",
"code_arm_required": "Code required for arming",
"alt_night_mode": "Alternative night mode"
"auto_bypass": "Auto Bypass on Arm",
"code_arm_required": "Code Required for Arming",
"alt_night_mode": "Alternative Night Mode"
}
},
"zone_select": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter the zone number you'd like to to add, edit, or remove.",
"data": {
"zone_number": "Zone number"
"zone_number": "Zone Number"
}
},
"zone_details": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
"data": {
"zone_name": "Zone name",
"zone_type": "Zone type",
"zone_rfid": "RF serial",
"zone_loop": "RF loop",
"zone_relayaddr": "Relay address",
"zone_relaychan": "Relay channel"
"zone_name": "Zone Name",
"zone_type": "Zone Type",
"zone_rfid": "RF Serial",
"zone_loop": "RF Loop",
"zone_relayaddr": "Relay Address",
"zone_relaychan": "Relay Channel"
}
}
},
"error": {
"relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together.",
"relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.",
"int": "The field below must be an integer.",
"loop_rfid": "'RF loop' cannot be used without 'RF serial'.",
"loop_range": "'RF loop' must be an integer between 1 and 4."
"loop_rfid": "RF Loop cannot be used without RF Serial.",
"loop_range": "RF Loop must be an integer between 1 and 4."
}
},
"services": {
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.8.1"]
"requirements": ["pyaprilaire==0.9.0"]
}
+1 -1
View File
@@ -18,7 +18,7 @@
},
"step": {
"validation": {
"title": "Two-factor authentication",
"title": "Two factor authentication",
"data": {
"verification_code": "Verification code"
},
@@ -4,8 +4,8 @@
"user": {
"description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel",
"data": {
"port": "RS485 or USB-RS485 adaptor port",
"address": "Inverter address"
"port": "RS485 or USB-RS485 Adaptor Port",
"address": "Inverter Address"
}
}
},
@@ -16,7 +16,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_serial_ports": "No com ports found. The integration needs a valid RS485 device to communicate."
"no_serial_ports": "No com ports found. Need a valid RS485 device to communicate."
}
},
"entity": {
+2 -2
View File
@@ -5,7 +5,7 @@
"step": {
"init": {
"title": "Set up two-factor authentication using TOTP",
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
}
},
"error": {
@@ -13,7 +13,7 @@
}
},
"notify": {
"title": "Notify one-time password",
"title": "Notify One-Time Password",
"step": {
"init": {
"title": "Set up one-time password delivered by notify component",
+2 -2
View File
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, EntityDescription
from .const import DOMAIN
from .const import DOMAIN as AXIS_DOMAIN
if TYPE_CHECKING:
from .hub import AxisHub
@@ -61,7 +61,7 @@ class AxisEntity(Entity):
self.hub = hub
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, hub.unique_id)},
identifiers={(AXIS_DOMAIN, hub.unique_id)},
serial_number=hub.unique_id,
)
@@ -2,8 +2,8 @@
from aiohttp import ClientTimeout
from azure.core.exceptions import (
AzureError,
ClientAuthenticationError,
HttpResponseError,
ResourceNotFoundError,
)
from azure.core.pipeline.transport._aiohttp import (
@@ -39,11 +39,20 @@ async def async_setup_entry(
session = async_create_clientsession(
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
)
container_client = ContainerClient(
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
container_name=entry.data[CONF_CONTAINER_NAME],
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=session),
def create_container_client() -> ContainerClient:
"""Create a ContainerClient."""
return ContainerClient(
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
container_name=entry.data[CONF_CONTAINER_NAME],
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=session),
)
# has a blocking call to open in cpython
container_client: ContainerClient = await hass.async_add_executor_job(
create_container_client
)
try:
@@ -61,7 +70,7 @@ async def async_setup_entry(
translation_key="invalid_auth",
translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]},
) from err
except HttpResponseError as err:
except AzureError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
@@ -8,7 +8,7 @@ import json
import logging
from typing import Any, Concatenate
from azure.core.exceptions import HttpResponseError
from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError
from azure.storage.blob import BlobProperties
from homeassistant.components.backup import (
@@ -80,6 +80,20 @@ def handle_backup_errors[_R, **P](
f"Error during backup operation in {func.__name__}:"
f" Status {err.status_code}, message: {err.message}"
) from err
except ServiceRequestError as err:
raise BackupAgentError(
f"Timeout during backup operation in {func.__name__}"
) from err
except AzureError as err:
_LOGGER.debug(
"Error during backup in %s: %s",
func.__name__,
err,
exc_info=True,
)
raise BackupAgentError(
f"Error during backup operation in {func.__name__}: {err}"
) from err
return wrapper
@@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__)
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for azure storage."""
def get_account_url(self, account_name: str) -> str:
"""Get the account URL."""
return f"https://{account_name}.blob.core.windows.net/"
async def get_container_client(
self, account_name: str, container_name: str, storage_account_key: str
) -> ContainerClient:
"""Get the container client.
ContainerClient has a blocking call to open in cpython
"""
session = async_get_clientsession(self.hass)
def create_container_client() -> ContainerClient:
return ContainerClient(
account_url=f"https://{account_name}.blob.core.windows.net/",
container_name=container_name,
credential=storage_account_key,
transport=AioHttpTransport(session=session),
)
return await self.hass.async_add_executor_job(create_container_client)
async def validate_config(
self, container_client: ContainerClient
@@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match(
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
)
container_client = ContainerClient(
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
container_client = await self.get_container_client(
account_name=user_input[CONF_ACCOUNT_NAME],
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
@@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry = self._get_reauth_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
container_client = await self.get_container_client(
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
if not errors:
return self.async_update_reload_and_abort(
@@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(
reconfigure_entry.data[CONF_ACCOUNT_NAME]
),
container_client = await self.get_container_client(
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
if not errors:
@@ -30,7 +30,6 @@ class BackupCoordinatorData:
"""Class to hold backup data."""
backup_manager_state: BackupManagerState
last_attempted_automatic_backup: datetime | None
last_successful_automatic_backup: datetime | None
next_scheduled_automatic_backup: datetime | None
@@ -71,7 +70,6 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
"""Update backup manager data."""
return BackupCoordinatorData(
self.backup_manager.state,
self.backup_manager.config.data.last_attempted_automatic_backup,
self.backup_manager.config.data.last_completed_automatic_backup,
self.backup_manager.config.data.schedule.next_automatic_backup,
)
@@ -46,12 +46,6 @@ BACKUP_MANAGER_DESCRIPTIONS = (
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.last_successful_automatic_backup,
),
BackupSensorEntityDescription(
key="last_attempted_automatic_backup",
translation_key="last_attempted_automatic_backup",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.last_attempted_automatic_backup,
),
)
@@ -37,9 +37,6 @@
"next_scheduled_automatic_backup": {
"name": "Next scheduled automatic backup"
},
"last_attempted_automatic_backup": {
"name": "Last attempted automatic backup"
},
"last_successful_automatic_backup": {
"name": "Last successful automatic backup"
}
-6
View File
@@ -332,9 +332,6 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size
padding = max(minimum_size - output_stream.tell(), 0)
@@ -420,9 +417,6 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size
padding = max(minimum_size - output_stream.tell(), 0)
+3 -3
View File
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Sign in with Blink account",
"title": "Sign-in with Blink account",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
@@ -30,7 +30,7 @@
"step": {
"simple_options": {
"data": {
"scan_interval": "Scan interval (seconds)"
"scan_interval": "Scan Interval (seconds)"
},
"title": "Blink options",
"description": "Configure Blink integration"
@@ -93,7 +93,7 @@
},
"config_entry_id": {
"name": "Integration ID",
"description": "The Blink integration ID."
"description": "The Blink Integration ID."
}
}
}
@@ -21,7 +21,6 @@ from .coordinator import (
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BUTTON,
Platform.MEDIA_PLAYER,
]
@@ -1,128 +0,0 @@
"""Button entities for Bluesound."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from pyblu import Player
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import BluesoundCoordinator
from .media_player import DEFAULT_PORT
from .utils import format_unique_id
if TYPE_CHECKING:
from . import BluesoundConfigEntry
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BluesoundConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Bluesound entry."""
async_add_entities(
BluesoundButton(
config_entry.runtime_data.coordinator,
config_entry.runtime_data.player,
config_entry.data[CONF_PORT],
description,
)
for description in BUTTON_DESCRIPTIONS
)
@dataclass(kw_only=True, frozen=True)
class BluesoundButtonEntityDescription(ButtonEntityDescription):
"""Description for Bluesound button entities."""
press_fn: Callable[[Player], Awaitable[None]]
async def clear_sleep_timer(player: Player) -> None:
"""Clear the sleep timer."""
sleep = -1
while sleep != 0:
sleep = await player.sleep_timer()
async def set_sleep_timer(player: Player) -> None:
"""Set the sleep timer."""
await player.sleep_timer()
BUTTON_DESCRIPTIONS = [
BluesoundButtonEntityDescription(
key="set_sleep_timer",
translation_key="set_sleep_timer",
entity_registry_enabled_default=False,
press_fn=set_sleep_timer,
),
BluesoundButtonEntityDescription(
key="clear_sleep_timer",
translation_key="clear_sleep_timer",
entity_registry_enabled_default=False,
press_fn=clear_sleep_timer,
),
]
class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
"""Base class for Bluesound buttons."""
_attr_has_entity_name = True
entity_description: BluesoundButtonEntityDescription
def __init__(
self,
coordinator: BluesoundCoordinator,
player: Player,
port: int,
description: BluesoundButtonEntityDescription,
) -> None:
"""Initialize the Bluesound button."""
super().__init__(coordinator)
sync_status = coordinator.data.sync_status
self.entity_description = description
self._player = player
self._attr_unique_id = (
f"{description.key}-{format_unique_id(sync_status.mac, port)}"
)
if port == DEFAULT_PORT:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
name=sync_status.name,
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
)
else:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))},
name=sync_status.name,
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.press_fn(self._player)
@@ -22,11 +22,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
issue_registry as ir,
)
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -38,7 +34,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util, slugify
from homeassistant.util import dt as dt_util
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
from .coordinator import BluesoundCoordinator
@@ -492,36 +488,10 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
async def async_increase_timer(self) -> int:
"""Increase sleep time on player."""
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_service_{SERVICE_SET_TIMER}",
is_fixable=False,
breaks_in_ha_version="2025.12.0",
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_service_set_sleep_timer",
translation_placeholders={
"name": slugify(self.sync_status.name),
},
)
return await self._player.sleep_timer()
async def async_clear_timer(self) -> None:
"""Clear sleep timer on player."""
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_service_{SERVICE_CLEAR_TIMER}",
is_fixable=False,
breaks_in_ha_version="2025.12.0",
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_service_clear_sleep_timer",
translation_placeholders={
"name": slugify(self.sync_status.name),
},
)
sleep = 1
while sleep > 0:
sleep = await self._player.sleep_timer()
@@ -26,16 +26,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"issues": {
"deprecated_service_set_sleep_timer": {
"title": "Detected use of deprecated action bluesound.set_sleep_timer",
"description": "Use `button.{name}_set_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts."
},
"deprecated_service_clear_sleep_timer": {
"title": "Detected use of deprecated action bluesound.clear_sleep_timer",
"description": "Use `button.{name}_clear_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts."
}
},
"services": {
"join": {
"name": "Join",
@@ -81,15 +71,5 @@
}
}
}
},
"entity": {
"button": {
"set_sleep_timer": {
"name": "Set sleep timer"
},
"clear_sleep_timer": {
"name": "Clear sleep timer"
}
}
}
}
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .entity import BMWBaseEntity
if TYPE_CHECKING:
@@ -111,7 +111,7 @@ class BMWButton(BMWBaseEntity, ButtonEntity):
await self.entity_description.remote_function(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -71,7 +71,7 @@ class BMWLock(BMWBaseEntity, LockEntity):
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -95,7 +95,7 @@ class BMWLock(BMWBaseEntity, LockEntity):
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
PARALLEL_UPDATES = 1
@@ -92,7 +92,7 @@ class BMWNotificationService(BaseNotificationService):
except (vol.Invalid, TypeError, ValueError) as ex:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="invalid_poi",
translation_placeholders={
"poi_exception": str(ex),
@@ -107,7 +107,7 @@ class BMWNotificationService(BaseNotificationService):
await vehicle.remote_services.trigger_send_poi(poi)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -110,7 +110,7 @@ class BMWNumber(BMWBaseEntity, NumberEntity):
await self.entity_description.remote_service(self.vehicle, value)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -124,7 +124,7 @@ class BMWSelect(BMWBaseEntity, SelectEntity):
await self.entity_description.remote_service(self.vehicle, option)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -69,7 +69,7 @@
"name": "Door lock state"
},
"condition_based_services": {
"name": "Condition-based services"
"name": "Condition based services"
},
"check_control_messages": {
"name": "Check control messages"
@@ -81,7 +81,7 @@
"name": "Connection status"
},
"is_pre_entry_climatization_enabled": {
"name": "Pre-entry climatization"
"name": "Pre entry climatization"
}
},
"button": {
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -112,7 +112,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
await self.entity_description.remote_service_on(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -124,7 +124,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
await self.entity_description.remote_service_off(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
+1 -2
View File
@@ -5,7 +5,7 @@ import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError, ClientTimeout
from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup
from bond_async import Bond, BPUPSubscriptions, start_bpup
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -49,7 +49,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool
token=token,
timeout=ClientTimeout(total=_API_TIMEOUT),
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
hub = BondHub(bond, host)
try:
+3 -11
View File
@@ -8,7 +8,7 @@ import logging
from typing import Any
from aiohttp import ClientConnectionError, ClientResponseError
from bond_async import Bond, RequestorUUID
from bond_async import Bond
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
@@ -34,12 +34,7 @@ TOKEN_SCHEMA = vol.Schema({})
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
"""Try to fetch the token from the bond device."""
bond = Bond(
host,
"",
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
bond = Bond(host, "", session=async_get_clientsession(hass))
response: dict[str, str] = {}
with contextlib.suppress(ClientConnectionError):
response = await bond.token()
@@ -50,10 +45,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st
"""Validate the user input allows us to connect."""
bond = Bond(
data[CONF_HOST],
data[CONF_ACCESS_TOKEN],
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
)
try:
hub = BondHub(bond, data[CONF_HOST])
@@ -14,11 +14,7 @@ from homeassistant.helpers import device_registry as dr
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
PLATFORMS: list[Platform] = [
Platform.ALARM_CONTROL_PANEL,
Platform.SENSOR,
Platform.SWITCH,
]
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR]
type BoschAlarmConfigEntry = ConfigEntry[Panel]
@@ -86,57 +86,3 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
self._area.ready_observer.detach(self.schedule_update_ha_state)
if self._observe_status:
self._area.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmDoorEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, door_id: int, unique_id: str) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._door_id = door_id
self._door = panel.doors[door_id]
self._door_unique_id = f"{unique_id}_door_{door_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._door_unique_id)},
name=self._door.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._door.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._door.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmOutputEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
"""Set up a output related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._output_id = output_id
self._output = panel.outputs[output_id]
self._output_unique_id = f"{unique_id}_output_{output_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._output_unique_id)},
name=self._output.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._output.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._output.status_observer.detach(self.schedule_update_ha_state)
@@ -2,27 +2,7 @@
"entity": {
"sensor": {
"faulting_points": {
"default": "mdi:alert-circle"
}
},
"switch": {
"locked": {
"default": "mdi:lock",
"state": {
"off": "mdi:lock-open"
}
},
"secured": {
"default": "mdi:lock",
"state": {
"off": "mdi:lock-open"
}
},
"cycling": {
"default": "mdi:lock",
"state": {
"on": "mdi:lock-open"
}
"default": "mdi:alert-circle-outline"
}
}
}
@@ -54,23 +54,9 @@
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
},
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is being cycled."
}
},
"entity": {
"switch": {
"secured": {
"name": "Secured"
},
"cycling": {
"name": "Cycling"
},
"locked": {
"name": "Locked"
}
},
"sensor": {
"faulting_points": {
"name": "Faulting points",
@@ -1,150 +0,0 @@
"""Support for Bosch Alarm Panel outputs and doors as switches."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.panel import Door
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .const import DOMAIN
from .entity import BoschAlarmDoorEntity, BoschAlarmOutputEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSwitchEntityDescription(SwitchEntityDescription):
"""Describes Bosch Alarm door entity."""
value_fn: Callable[[Door], bool]
on_fn: Callable[[Panel, int], Coroutine[Any, Any, None]]
off_fn: Callable[[Panel, int], Coroutine[Any, Any, None]]
DOOR_SWITCH_TYPES: list[BoschAlarmSwitchEntityDescription] = [
BoschAlarmSwitchEntityDescription(
key="locked",
translation_key="locked",
value_fn=lambda door: door.is_locked(),
on_fn=lambda panel, door_id: panel.door_relock(door_id),
off_fn=lambda panel, door_id: panel.door_unlock(door_id),
),
BoschAlarmSwitchEntityDescription(
key="secured",
translation_key="secured",
value_fn=lambda door: door.is_secured(),
on_fn=lambda panel, door_id: panel.door_secure(door_id),
off_fn=lambda panel, door_id: panel.door_unsecure(door_id),
),
BoschAlarmSwitchEntityDescription(
key="cycling",
translation_key="cycling",
value_fn=lambda door: door.is_cycling(),
on_fn=lambda panel, door_id: panel.door_cycle(door_id),
off_fn=lambda panel, door_id: panel.door_relock(door_id),
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up switch entities for outputs."""
panel = config_entry.runtime_data
entities: list[SwitchEntity] = [
PanelOutputEntity(
panel, output_id, config_entry.unique_id or config_entry.entry_id
)
for output_id in panel.outputs
]
entities.extend(
PanelDoorEntity(
panel,
door_id,
config_entry.unique_id or config_entry.entry_id,
entity_description,
)
for door_id in panel.doors
for entity_description in DOOR_SWITCH_TYPES
)
async_add_entities(entities)
PARALLEL_UPDATES = 0
class PanelDoorEntity(BoschAlarmDoorEntity, SwitchEntity):
"""A switch entity for a door on a bosch alarm panel."""
entity_description: BoschAlarmSwitchEntityDescription
def __init__(
self,
panel: Panel,
door_id: int,
unique_id: str,
entity_description: BoschAlarmSwitchEntityDescription,
) -> None:
"""Set up a switch entity for a door on a bosch alarm panel."""
super().__init__(panel, door_id, unique_id)
self.entity_description = entity_description
self._attr_unique_id = f"{self._door_unique_id}_{entity_description.key}"
@property
def is_on(self) -> bool:
"""Return the value function."""
return self.entity_description.value_fn(self._door)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Run the on function."""
# If the door is currently cycling, we can't send it any other commands until it is done
if self._door.is_cycling():
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="incorrect_door_state"
)
await self.entity_description.on_fn(self.panel, self._door_id)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Run the off function."""
# If the door is currently cycling, we can't send it any other commands until it is done
if self._door.is_cycling():
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="incorrect_door_state"
)
await self.entity_description.off_fn(self.panel, self._door_id)
class PanelOutputEntity(BoschAlarmOutputEntity, SwitchEntity):
"""An output entity for a bosch alarm panel."""
_attr_name = None
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
"""Set up an output entity for a bosch alarm panel."""
super().__init__(panel, output_id, unique_id)
self._attr_unique_id = self._output_unique_id
@property
def is_on(self) -> bool:
"""Check if this entity is on."""
return self._output.is_active()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on this output."""
await self.panel.set_output_active(self._output_id)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off this output."""
await self.panel.set_output_inactive(self._output_id)
+2 -10
View File
@@ -10,12 +10,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import (
BringActivityCoordinator,
BringConfigEntry,
BringCoordinators,
BringDataUpdateCoordinator,
)
from .coordinator import BringConfigEntry, BringDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
@@ -31,10 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo
coordinator = BringDataUpdateCoordinator(hass, entry, bring)
await coordinator.async_config_entry_first_refresh()
activity_coordinator = BringActivityCoordinator(hass, entry, coordinator)
await activity_coordinator.async_config_entry_first_refresh()
entry.runtime_data = BringCoordinators(coordinator, activity_coordinator)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+8 -87
View File
@@ -30,15 +30,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type BringConfigEntry = ConfigEntry[BringCoordinators]
@dataclass
class BringCoordinators:
"""Data class holding coordinators."""
data: BringDataUpdateCoordinator
activity: BringActivityCoordinator
type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
@dataclass(frozen=True)
@@ -47,27 +39,16 @@ class BringData(DataClassORJSONMixin):
lst: BringList
content: BringItemsResponse
@dataclass(frozen=True)
class BringActivityData(DataClassORJSONMixin):
"""Coordinator data class."""
activity: BringActivityResponse
users: BringUsersResponse
class BringBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Bring base coordinator."""
config_entry: BringConfigEntry
lists: list[BringList]
class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
"""A Bring Data Update Coordinator."""
config_entry: BringConfigEntry
user_settings: BringUserSettingsResponse
lists: list[BringList]
def __init__(
self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring
@@ -109,19 +90,16 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
current_lists := {lst.listUuid for lst in self.lists}
):
self._purge_deleted_lists()
new_lists = current_lists - self.previous_lists
self.previous_lists = current_lists
list_dict: dict[str, BringData] = {}
for lst in self.lists:
if (
(ctx := set(self.async_contexts()))
and lst.listUuid not in ctx
and lst.listUuid not in new_lists
):
if (ctx := set(self.async_contexts())) and lst.listUuid not in ctx:
continue
try:
items = await self.bring.get_list(lst.listUuid)
activity = await self.bring.get_activity(lst.listUuid)
users = await self.bring.get_list_users(lst.listUuid)
except BringRequestException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -133,7 +111,7 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
translation_key="setup_parse_exception",
) from e
else:
list_dict[lst.listUuid] = BringData(lst, items)
list_dict[lst.listUuid] = BringData(lst, items, activity, users)
return list_dict
@@ -178,60 +156,3 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
device_reg.async_update_device(
device.id, remove_config_entry_id=self.config_entry.entry_id
)
class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]]):
"""A Bring Activity Data Update Coordinator."""
user_settings: BringUserSettingsResponse
def __init__(
self,
hass: HomeAssistant,
config_entry: BringConfigEntry,
coordinator: BringDataUpdateCoordinator,
) -> None:
"""Initialize the Bring Activity data coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=timedelta(minutes=10),
)
self.coordinator = coordinator
self.lists = coordinator.lists
async def _async_update_data(self) -> dict[str, BringActivityData]:
"""Fetch activity data from bring."""
list_dict: dict[str, BringActivityData] = {}
for lst in self.lists:
if (
ctx := set(self.coordinator.async_contexts())
) and lst.listUuid not in ctx:
continue
try:
activity = await self.coordinator.bring.get_activity(lst.listUuid)
users = await self.coordinator.bring.get_list_users(lst.listUuid)
except BringAuthException as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="setup_authentication_exception",
translation_placeholders={CONF_EMAIL: self.coordinator.bring.mail},
) from e
except BringRequestException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_request_exception",
) from e
except BringParseException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_parse_exception",
) from e
else:
list_dict[lst.listUuid] = BringActivityData(activity, users)
return list_dict
@@ -20,12 +20,9 @@ async def async_get_config_entry_diagnostics(
return {
"data": {
k: v.to_dict() for k, v in config_entry.runtime_data.data.data.items()
},
"activity": {
k: async_redact_data(v.to_dict(), TO_REDACT)
for k, v in config_entry.runtime_data.activity.data.items()
for k, v in config_entry.runtime_data.data.items()
},
"lists": [lst.to_dict() for lst in config_entry.runtime_data.data.lists],
"user_settings": config_entry.runtime_data.data.user_settings.to_dict(),
"lists": [lst.to_dict() for lst in config_entry.runtime_data.lists],
"user_settings": config_entry.runtime_data.user_settings.to_dict(),
}
+4 -6
View File
@@ -8,17 +8,17 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import BringBaseCoordinator
from .coordinator import BringDataUpdateCoordinator
class BringBaseEntity(CoordinatorEntity[BringBaseCoordinator]):
class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]):
"""Bring base entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: BringBaseCoordinator,
coordinator: BringDataUpdateCoordinator,
bring_list: BringList,
) -> None:
"""Initialize the entity."""
@@ -34,7 +34,5 @@ class BringBaseEntity(CoordinatorEntity[BringBaseCoordinator]):
},
manufacturer="Bring! Labs AG",
model="Bring! Grocery Shopping List",
configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_list)}"
if bring_list in self.coordinator.lists
else None,
configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_list)}",
)
+6 -7
View File
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BringConfigEntry
from .coordinator import BringActivityCoordinator
from .coordinator import BringDataUpdateCoordinator
from .entity import BringBaseEntity
PARALLEL_UPDATES = 0
@@ -32,18 +32,18 @@ async def async_setup_entry(
"""Add event entities."""
nonlocal lists_added
if new_lists := {lst.listUuid for lst in coordinator.data.lists} - lists_added:
if new_lists := {lst.listUuid for lst in coordinator.lists} - lists_added:
async_add_entities(
BringEventEntity(
coordinator.activity,
coordinator,
bring_list,
)
for bring_list in coordinator.data.lists
for bring_list in coordinator.lists
if bring_list.listUuid in new_lists
)
lists_added |= new_lists
coordinator.activity.async_add_listener(add_entities)
coordinator.async_add_listener(add_entities)
add_entities()
@@ -51,11 +51,10 @@ class BringEventEntity(BringBaseEntity, EventEntity):
"""An event entity."""
_attr_translation_key = "activities"
coordinator: BringActivityCoordinator
def __init__(
self,
coordinator: BringActivityCoordinator,
coordinator: BringDataUpdateCoordinator,
bring_list: BringList,
) -> None:
"""Initialize the entity."""
+1 -2
View File
@@ -88,7 +88,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
coordinator = config_entry.runtime_data.data
coordinator = config_entry.runtime_data
lists_added: set[str] = set()
@callback
@@ -117,7 +117,6 @@ class BringSensorEntity(BringBaseEntity, SensorEntity):
"""A sensor entity."""
entity_description: BringSensorEntityDescription
coordinator: BringDataUpdateCoordinator
def __init__(
self,
+2 -5
View File
@@ -44,7 +44,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor from a config entry created in the integrations UI."""
coordinator = config_entry.runtime_data.data
coordinator = config_entry.runtime_data
lists_added: set[str] = set()
@callback
@@ -88,7 +88,6 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity):
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
coordinator: BringDataUpdateCoordinator
def __init__(
self, coordinator: BringDataUpdateCoordinator, bring_list: BringList
@@ -108,9 +107,7 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity):
description=item.specification,
status=TodoItemStatus.NEEDS_ACTION,
)
for item in sorted(
self.bring_list.content.items.purchase, key=lambda i: i.itemId
)
for item in self.bring_list.content.items.purchase
),
*(
TodoItem(
@@ -11,13 +11,6 @@
},
"audio_output": {
"default": "mdi:audio-input-stereo-minijack"
},
"control_bus_mode": {
"default": "mdi:audio-video-off",
"state": {
"amplifier": "mdi:speaker",
"receiver": "mdi:audio-video"
}
}
},
"switch": {
@@ -11,7 +11,6 @@ from aiostreammagic import (
StreamMagicClient,
TransportControl,
)
from aiostreammagic.models import ControlBusMode
from homeassistant.components.media_player import (
BrowseMedia,
@@ -92,8 +91,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
features = BASE_FEATURES
if self.client.state.pre_amp_mode:
features |= PREAMP_FEATURES
if self.client.state.control_bus == ControlBusMode.AMPLIFIER:
features |= MediaPlayerEntityFeature.VOLUME_STEP
if TransportControl.PLAY_PAUSE in controls:
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
for control in controls:
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from aiostreammagic import StreamMagicClient
from aiostreammagic.models import ControlBusMode, DisplayBrightness
from aiostreammagic.models import DisplayBrightness
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
@@ -76,20 +76,6 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
value_fn=_audio_output_value_fn,
set_value_fn=_audio_output_set_value_fn,
),
CambridgeAudioSelectEntityDescription(
key="control_bus_mode",
translation_key="control_bus_mode",
options=[
ControlBusMode.AMPLIFIER.value,
ControlBusMode.RECEIVER.value,
ControlBusMode.OFF.value,
],
entity_category=EntityCategory.CONFIG,
value_fn=lambda client: client.state.control_bus,
set_value_fn=lambda client, value: client.set_control_bus_mode(
ControlBusMode(value)
),
),
)
@@ -46,14 +46,6 @@
},
"audio_output": {
"name": "Audio output"
},
"control_bus_mode": {
"name": "Control Bus mode",
"state": {
"amplifier": "Amplifier",
"receiver": "Receiver",
"off": "[%key:common::state::off%]"
}
}
},
"switch": {
+78 -4
View File
@@ -61,6 +61,7 @@ from homeassistant.helpers.deprecation import (
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.network import get_url
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, VolDictType
@@ -85,6 +86,7 @@ from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCLegacyProvider,
CameraWebRTCProvider,
WebRTCAnswer,
WebRTCCandidate, # noqa: F401
@@ -92,8 +94,10 @@ from .webrtc import (
WebRTCError,
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_legacy_provider,
async_get_supported_provider,
async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -432,6 +436,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
CACHED_PROPERTIES_WITH_ATTR_ = {
"brand",
"frame_interval",
"frontend_stream_type",
"is_on",
"is_recording",
"is_streaming",
@@ -451,6 +456,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Entity Properties
_attr_brand: str | None = None
_attr_frame_interval: float = MIN_STREAM_INTERVAL
# Deprecated in 2024.12. Remove in 2025.6
_attr_frontend_stream_type: StreamType | None
_attr_is_on: bool = True
_attr_is_recording: bool = False
_attr_is_streaming: bool = False
@@ -473,6 +480,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
@@ -480,6 +488,16 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer
)
self._deprecate_attr_frontend_stream_type_logged = False
if type(self).frontend_stream_type != Camera.frontend_stream_type:
report_usage(
(
f"is overwriting the 'frontend_stream_type' property in the {type(self).__name__} class,"
" which is deprecated and will be removed in Home Assistant 2025.6, "
),
core_integration_behavior=ReportBehavior.ERROR,
exclude_integrations={DOMAIN},
)
@cached_property
def entity_picture(self) -> str:
@@ -541,6 +559,40 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the interval between frames of the mjpeg stream."""
return self._attr_frame_interval
@property
def frontend_stream_type(self) -> StreamType | None:
"""Return the type of stream supported by this camera.
A camera may have a single stream type which is used to inform the
frontend which camera attributes and player to use. The default type
is to use HLS, and components can override to change the type.
"""
# Deprecated in 2024.12. Remove in 2025.6
# Use the camera_capabilities instead
if hasattr(self, "_attr_frontend_stream_type"):
if not self._deprecate_attr_frontend_stream_type_logged:
report_usage(
(
f"is setting the '_attr_frontend_stream_type' attribute in the {type(self).__name__} class,"
" which is deprecated and will be removed in Home Assistant 2025.6, "
),
core_integration_behavior=ReportBehavior.ERROR,
exclude_integrations={DOMAIN},
)
self._deprecate_attr_frontend_stream_type_logged = True
return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
if (
self._webrtc_provider
or self._legacy_webrtc_provider
or self._supports_native_sync_webrtc
or self._supports_native_async_webrtc
):
return StreamType.WEB_RTC
return StreamType.HLS
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -642,7 +694,14 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
)
return
raise HomeAssistantError("Camera does not support WebRTC")
if self._legacy_webrtc_provider and (
answer := await self._legacy_webrtc_provider.async_handle_web_rtc_offer(
self, offer_sdp
)
):
send_message(WebRTCAnswer(answer))
else:
raise HomeAssistantError("Camera does not support WebRTC")
def camera_image(
self, width: int | None = None, height: int | None = None
@@ -738,6 +797,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if motion_detection_enabled := self.motion_detection_enabled:
attrs["motion_detection"] = motion_detection_enabled
if frontend_stream_type := self.frontend_stream_type:
attrs["frontend_stream_type"] = frontend_stream_type
return attrs
@callback
@@ -761,7 +823,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
providers or inputs to the state attributes change.
"""
old_provider = self._webrtc_provider
old_legacy_provider = self._legacy_webrtc_provider
new_provider = None
new_legacy_provider = None
# Skip all providers if the camera has a native WebRTC implementation
if not (
@@ -772,8 +836,15 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async_get_supported_provider
)
if old_provider != new_provider:
if new_provider is None:
# Only add the legacy provider if the new provider is not available
new_legacy_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_legacy_provider
)
if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
self._webrtc_provider = new_provider
self._legacy_webrtc_provider = new_legacy_provider
self._invalidate_camera_capabilities_cache()
if write_state:
self.async_write_ha_state()
@@ -808,7 +879,10 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = self._supports_native_sync_webrtc
config.get_candidates_upfront = (
self._supports_native_sync_webrtc
or self._legacy_webrtc_provider is not None
)
return config
@@ -844,7 +918,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
else:
frontend_stream_types.add(StreamType.HLS)
if self._webrtc_provider:
if self._webrtc_provider or self._legacy_webrtc_provider:
frontend_stream_types.add(StreamType.WEB_RTC)
return CameraCapabilities(frontend_stream_types)
@@ -46,6 +46,10 @@
}
}
}
},
"legacy_webrtc_provider": {
"title": "Detected use of legacy WebRTC provider registered by {legacy_integration}",
"description": "The {legacy_integration} integration has registered a legacy WebRTC provider. Home Assistant prefers using the built-in modern WebRTC provider registered by the {builtin_integration} integration.\n\nBenefits of the built-in integration are:\n\n- The camera stream is started faster.\n- More camera devices are supported.\n\nTo fix this issue, you can either keep using the built-in modern WebRTC provider and remove the {legacy_integration} integration or remove the {builtin_integration} integration to use the legacy provider, and then restart Home Assistant."
}
},
"services": {
+126 -2
View File
@@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Protocol
from mashumaro import MissingField
import voluptuous as vol
@@ -22,7 +22,8 @@ from webrtc_models import (
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.deprecation import deprecated_function
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.ulid import ulid
@@ -38,6 +39,9 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey(
"camera_webrtc_legacy_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
@@ -159,6 +163,18 @@ class CameraWebRTCProvider(ABC):
return ## This is an optional method so we need a default here.
class CameraWebRTCLegacyProvider(Protocol):
"""WebRTC provider."""
async def async_is_supported(self, stream_source: str) -> bool:
"""Determine if the provider supports the stream source."""
async def async_handle_web_rtc_offer(
self, camera: Camera, offer_sdp: str
) -> str | None:
"""Handle the WebRTC offer and return an answer."""
@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
@@ -188,6 +204,8 @@ def async_register_webrtc_provider(
async def _async_refresh_providers(hass: HomeAssistant) -> None:
"""Check all cameras for any state changes for registered providers."""
_async_check_conflicting_legacy_provider(hass)
component = hass.data[DATA_COMPONENT]
await asyncio.gather(
*(camera.async_refresh_providers() for camera in component.entities)
@@ -362,6 +380,21 @@ async def async_get_supported_provider(
return None
async def async_get_supported_legacy_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCLegacyProvider | None:
"""Return the first supported provider for the camera."""
providers = hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
return None
for provider in providers.values():
if await provider.async_is_supported(stream_source):
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
@@ -378,3 +411,94 @@ def async_register_ice_servers(
servers.append(get_ice_server_fn)
return remove
# The following code is legacy code that was introduced with rtsp_to_webrtc and will be deprecated/removed in the future.
# Left it so custom integrations can still use it.
_RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"}
# An RtspToWebRtcProvider accepts these inputs:
# stream_source: The RTSP url
# offer_sdp: The WebRTC SDP offer
# stream_id: A unique id for the stream, used to update an existing source
# The output is the SDP answer, or None if the source or offer is not eligible.
# The Callable may throw HomeAssistantError on failure.
type RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]]
class _CameraRtspToWebRTCProvider(CameraWebRTCLegacyProvider):
def __init__(self, fn: RtspToWebRtcProviderType) -> None:
"""Initialize the RTSP to WebRTC provider."""
self._fn = fn
async def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source."""
return any(stream_source.startswith(prefix) for prefix in _RTSP_PREFIXES)
async def async_handle_web_rtc_offer(
self, camera: Camera, offer_sdp: str
) -> str | None:
"""Handle the WebRTC offer and return an answer."""
if not (stream_source := await camera.stream_source()):
return None
return await self._fn(stream_source, offer_sdp, camera.entity_id)
@deprecated_function("async_register_webrtc_provider", breaks_in_ha_version="2025.6")
def async_register_rtsp_to_web_rtc_provider(
hass: HomeAssistant,
domain: str,
provider: RtspToWebRtcProviderType,
) -> Callable[[], None]:
"""Register an RTSP to WebRTC provider.
The first provider to satisfy the offer will be used.
"""
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")
legacy_providers = hass.data.setdefault(DATA_WEBRTC_LEGACY_PROVIDERS, {})
if domain in legacy_providers:
raise ValueError("Provider already registered")
provider_instance = _CameraRtspToWebRTCProvider(provider)
@callback
def remove_provider() -> None:
legacy_providers.pop(domain)
hass.async_create_task(_async_refresh_providers(hass))
legacy_providers[domain] = provider_instance
hass.async_create_task(_async_refresh_providers(hass))
return remove_provider
@callback
def _async_check_conflicting_legacy_provider(hass: HomeAssistant) -> None:
"""Check if a legacy provider is registered together with the builtin provider."""
builtin_provider_domain = "go2rtc"
if (
(legacy_providers := hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS))
and (providers := hass.data.get(DATA_WEBRTC_PROVIDERS))
and any(provider.domain == builtin_provider_domain for provider in providers)
):
for domain in legacy_providers:
ir.async_create_issue(
hass,
DOMAIN,
f"legacy_webrtc_provider_{domain}",
is_fixable=False,
is_persistent=False,
issue_domain=domain,
learn_more_url="https://www.home-assistant.io/integrations/go2rtc/",
severity=ir.IssueSeverity.WARNING,
translation_key="legacy_webrtc_provider",
translation_placeholders={
"legacy_integration": domain,
"builtin_integration": builtin_provider_domain,
},
)
+2 -2
View File
@@ -10,12 +10,12 @@
"known_hosts": "Add known host"
},
"data_description": {
"known_hosts": "Hostnames or IP addresses of cast devices, use if mDNS discovery is not working"
"known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working"
}
}
},
"error": {
"invalid_known_hosts": "Known hosts must be a comma-separated list of hosts."
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
}
},
"options": {
@@ -61,6 +61,7 @@ from .const import (
CONF_RELAYER_SERVER,
CONF_REMOTESTATE_SERVER,
CONF_SERVICEHANDLERS_SERVER,
CONF_THINGTALK_SERVER,
CONF_USER_POOL_ID,
DATA_CLOUD,
DATA_CLOUD_LOG_HANDLER,
@@ -133,6 +134,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
vol.Optional(CONF_RELAYER_SERVER): str,
vol.Optional(CONF_REMOTESTATE_SERVER): str,
vol.Optional(CONF_THINGTALK_SERVER): str,
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
}
)
+1 -10
View File
@@ -26,11 +26,7 @@ from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util.aiohttp import MockRequest, serialize_response
from . import alexa_config, google_config
@@ -40,7 +36,6 @@ from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__)
VALID_REPAIR_TRANSLATION_KEYS = {
"no_subscription",
"warn_bad_custom_domain_configuration",
"reset_bad_custom_domain_configuration",
}
@@ -414,7 +409,3 @@ class CloudClient(Interface):
severity=IssueSeverity(severity),
is_fixable=False,
)
async def async_delete_repair_issue(self, identifier: str) -> None:
"""Delete a repair issue."""
async_delete_issue(hass=self._hass, domain=DOMAIN, issue_id=identifier)
+1
View File
@@ -81,6 +81,7 @@ CONF_ACME_SERVER = "acme_server"
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
CONF_RELAYER_SERVER = "relayer_server"
CONF_REMOTESTATE_SERVER = "remotestate_server"
CONF_THINGTALK_SERVER = "thingtalk_server"
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
MODE_DEV = "development"
+21 -1
View File
@@ -16,7 +16,7 @@ from typing import Any, Concatenate, cast
import aiohttp
from aiohttp import web
import attr
from hass_nabucasa import AlreadyConnectedError, Cloud, auth
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
from hass_nabucasa.const import STATE_DISCONNECTED
from hass_nabucasa.voice_data import TTS_VOICES
import voluptuous as vol
@@ -104,6 +104,7 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, alexa_list)
websocket_api.async_register_command(hass, alexa_sync)
websocket_api.async_register_command(hass, thingtalk_convert)
websocket_api.async_register_command(hass, tts_info)
hass.http.register_view(GoogleActionsSyncView)
@@ -997,6 +998,25 @@ async def alexa_sync(
)
@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str})
@websocket_api.async_response
async def thingtalk_convert(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Convert a query."""
cloud = hass.data[DATA_CLOUD]
async with asyncio.timeout(10):
try:
connection.send_result(
msg["id"], await thingtalk.async_convert(cloud, msg["query"])
)
except thingtalk.ThingTalkConversionError as err:
connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
@websocket_api.websocket_command({"type": "cloud/tts/info"})
def tts_info(
hass: HomeAssistant,
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.100.0"],
"requirements": ["hass-nabucasa==0.96.0"],
"single_config_entry": true
}
@@ -62,10 +62,6 @@
}
}
},
"no_subscription": {
"title": "No subscription detected",
"description": "You do not have a Home Assistant Cloud subscription. Subscribe at {account_url}."
},
"warn_bad_custom_domain_configuration": {
"title": "Detected wrong custom domain configuration",
"description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. Please check the DNS configuration of your domain and make sure it points to the correct CNAME."
+1 -3
View File
@@ -134,11 +134,9 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
self._attr_current_temperature = values[0] / 10
self._attr_hvac_action = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_action = HVACAction.OFF
if not _active:
self._attr_hvac_action = HVACAction.IDLE
if _mode in API_STATUS:
elif _mode in API_STATUS:
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
self._attr_hvac_mode = None
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "bronze",
"requirements": ["aiocomelit==0.12.0"]
"requirements": ["aiocomelit==0.12.3"]
}
@@ -65,8 +65,8 @@ rules:
status: todo
comment: missing implementation
entity-category:
status: exempt
comment: no config or diagnostic entities
status: todo
comment: PR in progress
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
@@ -165,7 +165,9 @@ class ConfigManagerFlowIndexView(
"""Not implemented."""
raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"])
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
@RequestDataValidator(
vol.Schema(
{
@@ -216,12 +218,16 @@ class ConfigManagerFlowResourceView(
url = "/api/config/config_entries/flow/{flow_id}"
name = "api:config:config_entries:flow:resource"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@@ -256,7 +262,9 @@ class OptionManagerFlowIndexView(
url = "/api/config/config_entries/options/flow"
name = "api:config:config_entries:option:flow"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request) -> web.Response:
"""Handle a POST request.
@@ -273,12 +281,16 @@ class OptionManagerFlowResourceView(
url = "/api/config/config_entries/options/flow/{flow_id}"
name = "api:config:config_entries:options:flow:resource"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@@ -292,7 +304,9 @@ class SubentryManagerFlowIndexView(
url = "/api/config/config_entries/subentries/flow"
name = "api:config:config_entries:subentries:flow"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
@RequestDataValidator(
vol.Schema(
{
@@ -327,12 +341,16 @@ class SubentryManagerFlowResourceView(
url = "/api/config/config_entries/subentries/flow/{flow_id}"
name = "api:config:config_entries:subentries:flow:resource"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@@ -9,13 +9,12 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
from homeassistant.helpers.json import json_dumps
@@ -23,7 +22,6 @@ from homeassistant.helpers.json import json_dumps
def async_setup(hass: HomeAssistant) -> bool:
"""Enable the Entity Registry views."""
websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids)
websocket_api.async_register_command(hass, websocket_get_entities)
websocket_api.async_register_command(hass, websocket_get_entity)
websocket_api.async_register_command(hass, websocket_list_entities_for_display)
@@ -318,43 +316,3 @@ def websocket_remove_entity(
registry.async_remove(msg["entity_id"])
connection.send_message(websocket_api.result_message(msg["id"]))
@websocket_api.websocket_command(
{
vol.Required("type"): "config/entity_registry/get_automatic_entity_ids",
vol.Required("entity_ids"): cv.entity_ids,
}
)
@callback
def websocket_get_automatic_entity_ids(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return the automatic entity IDs for the given entity IDs.
This is used to help user reset entity IDs which have been customized by the user.
"""
registry = er.async_get(hass)
entity_ids = msg["entity_ids"]
automatic_entity_ids: dict[str, str | None] = {}
for entity_id in entity_ids:
if not (entry := registry.entities.get(entity_id)):
automatic_entity_ids[entity_id] = None
continue
if (
suggested := async_get_entity_suggested_object_id(hass, entity_id)
) == split_entity_id(entry.entity_id)[1]:
# No need to generate a new entity ID
automatic_entity_ids[entity_id] = None
continue
automatic_entity_ids[entity_id] = registry.async_generate_entity_id(
entry.domain,
suggested or f"{entry.platform}_{entry.unique_id}",
)
connection.send_message(
websocket_api.result_message(msg["id"], automatic_entity_ids)
)
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DANFOSS_AIR_DOMAIN
def setup_platform(
@@ -22,7 +22,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the available Danfoss Air sensors etc."""
data = hass.data[DOMAIN]
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [
[
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DANFOSS_AIR_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the available Danfoss Air sensors etc."""
data = hass.data[DOMAIN]
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [
[
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DANFOSS_AIR_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +24,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Danfoss Air HRV switch platform."""
data = hass.data[DOMAIN]
data = hass.data[DANFOSS_AIR_DOMAIN]
switches = [
[
+5 -5
View File
@@ -13,7 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from .const import DOMAIN as DECONZ_DOMAIN
from .hub import DeconzHub
from .util import serial_from_unique_id
@@ -59,12 +59,12 @@ class DeconzBase[_DeviceT: _DeviceType]:
return DeviceInfo(
connections={(CONNECTION_ZIGBEE, self.serial)},
identifiers={(DOMAIN, self.serial)},
identifiers={(DECONZ_DOMAIN, self.serial)},
manufacturer=self._device.manufacturer,
model=self._device.model_id,
name=self._device.name,
sw_version=self._device.software_version,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
)
@@ -176,9 +176,9 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]):
def device_info(self) -> DeviceInfo:
"""Return a device description for device registry."""
return DeviceInfo(
identifiers={(DOMAIN, self._group_identifier)},
identifiers={(DECONZ_DOMAIN, self._group_identifier)},
manufacturer="Dresden Elektronik",
model="deCONZ group",
name=self.group.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
)
+3 -3
View File
@@ -38,7 +38,7 @@ from homeassistant.util.color import (
)
from . import DeconzConfigEntry
from .const import DOMAIN, POWER_PLUGS
from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS
from .entity import DeconzDevice
from .hub import DeconzHub
@@ -395,11 +395,11 @@ class DeconzGroup(DeconzBaseLight[Group]):
def device_info(self) -> DeviceInfo:
"""Return a device description for device registry."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
identifiers={(DECONZ_DOMAIN, self.unique_id)},
manufacturer="Dresden Elektronik",
model="deCONZ group",
name=self._device.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
)
@property
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.1.0"],
"requirements": ["denonavr==1.0.1"],
"ssdp": [
{
"manufacturer": "Denon",
@@ -15,7 +15,7 @@
},
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"time_window": "If set, the sensor's value is a time-weighted moving average of derivatives within this window.",
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
}
}
@@ -3,10 +3,10 @@
"step": {
"init": {
"data": {
"events": "Comma-separated list of events."
"events": "Comma separated list of events."
},
"data_description": {
"events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
"events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
}
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DOVADO_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ def get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> DovadoSMSNotificationService:
"""Get the Dovado Router SMS notification service."""
return DovadoSMSNotificationService(hass.data[DOMAIN].client)
return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client)
class DovadoSMSNotificationService(BaseNotificationService):
+2 -2
View File
@@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DOVADO_DOMAIN
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
@@ -90,7 +90,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dovado sensor platform."""
dovado = hass.data[DOMAIN]
dovado = hass.data[DOVADO_DOMAIN]
sensors = config[CONF_SENSORS]
entities = [
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==13.2.1"]
}
+37 -3
View File
@@ -6,7 +6,8 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
from deebot_client.events import (
BatteryEvent,
ErrorEvent,
@@ -34,7 +35,7 @@ from homeassistant.const import (
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -59,6 +60,15 @@ class EcovacsSensorEntityDescription(
"""Ecovacs sensor entity description."""
value_fn: Callable[[EventT], StateType]
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
@callback
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
"""Get the area native unit of measurement based on device type."""
if device_type is DeviceType.MOWER:
return UnitOfArea.SQUARE_CENTIMETERS
return UnitOfArea.SQUARE_METERS
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
@@ -68,7 +78,9 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
capability_fn=lambda caps: caps.stats.clean,
value_fn=lambda e: e.area,
translation_key="stats_area",
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
@@ -85,6 +97,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
value_fn=lambda e: e.area,
key="total_stats_area",
translation_key="total_stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
@@ -249,6 +262,27 @@ class EcovacsSensor(
entity_description: EcovacsSensorEntityDescription
def __init__(
self,
device: Device,
capability: CapabilityEvent,
entity_description: EcovacsSensorEntityDescription,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description, **kwargs)
if (
entity_description.native_unit_of_measurement_fn
and (
native_unit_of_measurement
:= entity_description.native_unit_of_measurement_fn(
device.capabilities.device_type
)
)
is not None
):
self._attr_native_unit_of_measurement = native_unit_of_measurement
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
@@ -3,12 +3,12 @@
"step": {
"user": {
"data": {
"phone_number": "Phone number"
"phone_number": "Phone Number"
}
},
"one_time_password": {
"data": {
"one_time_password": "One-time password"
"one_time_password": "One Time Password"
}
}
},
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.7"]
"requirements": ["sense-energy==0.13.8"]
}
@@ -7,12 +7,12 @@
"step": {
"user": {
"data": {
"advertise_ip": "Advertise IP address",
"advertise_port": "Advertise port",
"host_ip": "Host IP address",
"listen_port": "Listen port",
"advertise_ip": "Advertise IP Address",
"advertise_port": "Advertise Port",
"host_ip": "Host IP Address",
"listen_port": "Listen Port",
"name": "[%key:common::config_flow::data::name%]",
"upnp_bind_multicast": "Bind multicast"
"upnp_bind_multicast": "Bind multicast (True/False)"
},
"title": "Define server configuration"
}
@@ -52,7 +52,6 @@ VALID_ENERGY_UNITS_GAS = {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.LITERS,
*VALID_ENERGY_UNITS,
}
VALID_VOLUME_UNITS_WATER: set[str] = {
@@ -50,7 +50,6 @@ GAS_USAGE_UNITS: dict[str, tuple[UnitOfEnergy | UnitOfVolume, ...]] = {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.LITERS,
),
}
GAS_PRICE_UNITS = tuple(
+1 -1
View File
@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
DEFAULT_PORT: Final = 6053
STABLE_BLE_VERSION_STR = "2025.2.2"
STABLE_BLE_VERSION_STR = "2025.5.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
+5 -17
View File
@@ -134,22 +134,6 @@ def esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
return _wrapper
def async_esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
func: Callable[[_EntityT], Awaitable[_R | None]],
) -> Callable[[_EntityT], Coroutine[Any, Any, _R | None]]:
"""Wrap a state property of an esphome entity.
This checks if the state object in the entity is set
and returns None if it is not set.
"""
@functools.wraps(func)
async def _wrapper(self: _EntityT) -> _R | None:
return await func(self) if self._has_state else None
return _wrapper
def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]](
func: Callable[[_EntityT], float | None],
) -> Callable[[_EntityT], float | None]:
@@ -239,7 +223,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
assert entry_data.device_info is not None
device_info = entry_data.device_info
self._device_info = device_info
self._on_entry_data_changed()
self._key = entity_info.key
self._state_type = state_type
@@ -327,6 +310,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
@callback
def _on_entry_data_changed(self) -> None:
entry_data = self._entry_data
# Update the device info since it can change
# when the device is reconnected
if TYPE_CHECKING:
assert entry_data.device_info is not None
self._device_info = entry_data.device_info
self._api_version = entry_data.api_version
self._client = entry_data.client
if self._device_info.has_deep_sleep:
+56 -9
View File
@@ -8,7 +8,6 @@ from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from functools import partial
import logging
from operator import delitem
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
from aioesphomeapi import (
@@ -184,7 +183,18 @@ class RuntimeEntryData:
"""Register to receive callbacks when static info changes for an EntityInfo type."""
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
callbacks.append(callback_)
return partial(callbacks.remove, callback_)
return partial(
self._async_unsubscribe_register_static_info, callbacks, callback_
)
@callback
def _async_unsubscribe_register_static_info(
self,
callbacks: list[Callable[[list[EntityInfo]], None]],
callback_: Callable[[list[EntityInfo]], None],
) -> None:
"""Unsubscribe to when static info is registered."""
callbacks.remove(callback_)
@callback
def async_register_key_static_info_updated_callback(
@@ -196,7 +206,18 @@ class RuntimeEntryData:
callback_key = (type(static_info), static_info.key)
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
callbacks.append(callback_)
return partial(callbacks.remove, callback_)
return partial(
self._async_unsubscribe_static_key_info_updated, callbacks, callback_
)
@callback
def _async_unsubscribe_static_key_info_updated(
self,
callbacks: list[Callable[[EntityInfo], None]],
callback_: Callable[[EntityInfo], None],
) -> None:
"""Unsubscribe to when static info is updated ."""
callbacks.remove(callback_)
@callback
def async_set_assist_pipeline_state(self, state: bool) -> None:
@@ -211,7 +232,14 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Subscribe to assist pipeline updates."""
self.assist_pipeline_update_callbacks.append(update_callback)
return partial(self.assist_pipeline_update_callbacks.remove, update_callback)
return partial(self._async_unsubscribe_assist_pipeline_update, update_callback)
@callback
def _async_unsubscribe_assist_pipeline_update(
self, update_callback: CALLBACK_TYPE
) -> None:
"""Unsubscribe to assist pipeline updates."""
self.assist_pipeline_update_callbacks.remove(update_callback)
@callback
def async_remove_entities(
@@ -309,7 +337,12 @@ class RuntimeEntryData:
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
"""Subscribe to state updates."""
self.device_update_subscriptions.add(callback_)
return partial(self.device_update_subscriptions.remove, callback_)
return partial(self._async_unsubscribe_device_update, callback_)
@callback
def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None:
"""Unsubscribe to device updates."""
self.device_update_subscriptions.remove(callback_)
@callback
def async_subscribe_static_info_updated(
@@ -317,7 +350,14 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Subscribe to static info updates."""
self.static_info_update_subscriptions.add(callback_)
return partial(self.static_info_update_subscriptions.remove, callback_)
return partial(self._async_unsubscribe_static_info_updated, callback_)
@callback
def _async_unsubscribe_static_info_updated(
self, callback_: Callable[[list[EntityInfo]], None]
) -> None:
"""Unsubscribe to static info updates."""
self.static_info_update_subscriptions.remove(callback_)
@callback
def async_subscribe_state_update(
@@ -329,7 +369,14 @@ class RuntimeEntryData:
"""Subscribe to state updates."""
subscription_key = (state_type, state_key)
self.state_subscriptions[subscription_key] = entity_callback
return partial(delitem, self.state_subscriptions, subscription_key)
return partial(self._async_unsubscribe_state_update, subscription_key)
@callback
def _async_unsubscribe_state_update(
self, subscription_key: tuple[type[EntityState], int]
) -> None:
"""Unsubscribe to state updates."""
self.state_subscriptions.pop(subscription_key)
@callback
def async_update_state(self, state: EntityState) -> None:
@@ -476,7 +523,7 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
self.assist_satellite_config_update_callbacks.append(callback_)
return partial(self.assist_satellite_config_update_callbacks.remove, callback_)
return lambda: self.assist_satellite_config_update_callbacks.remove(callback_)
@callback
def async_assist_satellite_config_updated(
@@ -493,7 +540,7 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's wake word is set."""
self.assist_satellite_set_wake_word_callbacks.append(callback_)
return partial(self.assist_satellite_set_wake_word_callbacks.remove, callback_)
return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_)
@callback
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==30.2.0",
"aioesphomeapi==30.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.15.1"
],
@@ -195,10 +195,7 @@
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
},
"error_uploading": {
"message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information."
},
"ota_in_progress": {
"message": "An OTA (Over-The-Air) update is already in progress for {configuration}."
"message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information."
}
}
}
+28 -75
View File
@@ -31,7 +31,6 @@ from .coordinator import ESPHomeDashboardCoordinator
from .dashboard import async_get_dashboard
from .entity import (
EsphomeEntity,
async_esphome_state_property,
convert_api_error_ha_error,
esphome_state_property,
platform_async_setup_entry,
@@ -126,17 +125,21 @@ class ESPHomeDashboardUpdateEntity(
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
}
)
self._install_lock = asyncio.Lock()
self._available_future: asyncio.Future[None] | None = None
self._update_attrs()
@callback
def _update_attrs(self) -> None:
"""Update the supported features."""
# If the device has deep sleep, we can't assume we can install updates
# as the ESP will not be connectable (by design).
coordinator = self.coordinator
device_info = self._device_info
# Install support can change at run time
if coordinator.last_update_success and coordinator.supports_update:
if (
coordinator.last_update_success
and coordinator.supports_update
and not device_info.has_deep_sleep
):
self._attr_supported_features = UpdateEntityFeature.INSTALL
else:
self._attr_supported_features = NO_FEATURES
@@ -175,13 +178,6 @@ class ESPHomeDashboardUpdateEntity(
self, static_info: list[EntityInfo] | None = None
) -> None:
"""Handle updated data from the device."""
if (
self._entry_data.available
and self._available_future
and not self._available_future.done()
):
self._available_future.set_result(None)
self._available_future = None
self._update_attrs()
self.async_write_ha_state()
@@ -196,46 +192,17 @@ class ESPHomeDashboardUpdateEntity(
entry_data.async_subscribe_device_updated(self._handle_device_update)
)
async def async_will_remove_from_hass(self) -> None:
"""Handle entity about to be removed from Home Assistant."""
if self._available_future and not self._available_future.done():
self._available_future.cancel()
self._available_future = None
async def _async_wait_available(self) -> None:
"""Wait until the device is available."""
# If the device has deep sleep, we need to wait for it to wake up
# and connect to the network to be able to install the update.
if self._entry_data.available:
return
self._available_future = self.hass.loop.create_future()
try:
await self._available_future
finally:
self._available_future = None
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
if self._install_lock.locked():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="ota_in_progress",
translation_placeholders={
"configuration": self._device_info.name,
},
)
# Ensure only one OTA per device at a time
async with self._install_lock:
# Ensure only one compile at a time for ALL devices
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
coordinator = self.coordinator
api = coordinator.api
device = coordinator.data.get(self._device_info.name)
assert device is not None
configuration = device["configuration"]
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
coordinator = self.coordinator
api = coordinator.api
device = coordinator.data.get(self._device_info.name)
assert device is not None
configuration = device["configuration"]
try:
if not await api.compile(configuration):
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -244,25 +211,14 @@ class ESPHomeDashboardUpdateEntity(
"configuration": configuration,
},
)
# If the device uses deep sleep, there's a small chance it goes
# to sleep right after the dashboard connects but before the OTA
# starts. In that case, the update won't go through, so we try
# again to catch it on its next wakeup.
attempts = 2 if self._device_info.has_deep_sleep else 1
try:
for attempt in range(1, attempts + 1):
await self._async_wait_available()
if await api.upload(configuration, "OTA"):
break
if attempt == attempts:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_uploading",
translation_placeholders={
"configuration": configuration,
},
)
if not await api.upload(configuration, "OTA"):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_uploading",
translation_placeholders={
"configuration": configuration,
},
)
finally:
await self.coordinator.async_request_refresh()
@@ -271,9 +227,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
"""A update implementation for esphome."""
_attr_supported_features = (
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS
| UpdateEntityFeature.RELEASE_NOTES
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
@callback
@@ -303,12 +257,11 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
"""Return the latest version."""
return self._state.latest_version
@async_esphome_state_property
async def async_release_notes(self) -> str | None:
"""Return the release notes."""
if self._state.release_summary:
return self._state.release_summary
return None
@property
@esphome_state_property
def release_summary(self) -> str:
"""Return the release summary."""
return self._state.release_summary
@property
@esphome_state_property
+2 -2
View File
@@ -2,8 +2,8 @@
import logging
from pyezvizapi.client import EzvizClient
from pyezvizapi.exceptions import (
from pyezviz.client import EzvizClient
from pyezviz.exceptions import (
EzvizAuthTokenExpired,
EzvizAuthVerificationCode,
HTTPError,
@@ -6,8 +6,8 @@ from dataclasses import dataclass
from datetime import timedelta
import logging
from pyezvizapi import PyEzvizError
from pyezvizapi.constants import DefenseModeType
from pyezviz import PyEzvizError
from pyezviz.constants import DefenseModeType
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
+3 -3
View File
@@ -6,9 +6,9 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from pyezvizapi import EzvizClient
from pyezvizapi.constants import SupportExt
from pyezvizapi.exceptions import HTTPError, PyEzvizError
from pyezviz import EzvizClient
from pyezviz.constants import SupportExt
from pyezviz.exceptions import HTTPError, PyEzvizError
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
+1 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from pyezvizapi.exceptions import HTTPError, InvalidHost, PyEzvizError
from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
from homeassistant.components import ffmpeg
from homeassistant.components.camera import Camera, CameraEntityFeature
@@ -6,15 +6,15 @@ from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
from pyezvizapi.client import EzvizClient
from pyezvizapi.exceptions import (
from pyezviz.client import EzvizClient
from pyezviz.exceptions import (
AuthTestResultFailed,
EzvizAuthVerificationCode,
InvalidHost,
InvalidURL,
PyEzvizError,
)
from pyezvizapi.test_cam_rtsp import TestRTSPAuth
from pyezviz.test_cam_rtsp import TestRTSPAuth
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
@@ -4,8 +4,8 @@ import asyncio
from datetime import timedelta
import logging
from pyezvizapi.client import EzvizClient
from pyezvizapi.exceptions import (
from pyezviz.client import EzvizClient
from pyezviz.exceptions import (
EzvizAuthTokenExpired,
EzvizAuthVerificationCode,
HTTPError,
+2 -2
View File
@@ -5,8 +5,8 @@ from __future__ import annotations
import logging
from propcache.api import cached_property
from pyezvizapi.exceptions import PyEzvizError
from pyezvizapi.utils import decrypt_image
from pyezviz.exceptions import PyEzvizError
from pyezviz.utils import decrypt_image
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
from homeassistant.config_entries import SOURCE_IGNORE
+2 -2
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
from typing import Any
from pyezvizapi.constants import DeviceCatagories, DeviceSwitchType, SupportExt
from pyezvizapi.exceptions import HTTPError, PyEzvizError
from pyezviz.constants import DeviceCatagories, DeviceSwitchType, SupportExt
from pyezviz.exceptions import HTTPError, PyEzvizError
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant, callback

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