Compare commits

..

73 Commits

Author SHA1 Message Date
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
2107 changed files with 25485 additions and 84944 deletions

View File

@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

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.1
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: |
@@ -944,8 +944,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
libgammu-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -960,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: |
@@ -1021,12 +1019,6 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1077,8 +1069,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libmariadb-dev-compat \
libxml2-utils
libmariadb-dev-compat
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1093,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: |
@@ -1162,12 +1152,6 @@ jobs:
steps.pytest-partial.outputs.mariadb }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1216,8 +1200,7 @@ jobs:
sudo apt-get -y install \
bluez \
ffmpeg \
libturbojpeg \
libxml2-utils
libturbojpeg
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \
postgresql-server-dev-14
@@ -1235,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: |
@@ -1305,12 +1287,6 @@ jobs:
steps.pytest-partial.outputs.postgresql }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1341,7 +1317,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
flags: full-suite
@@ -1378,8 +1354,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
libgammu-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1394,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: |
@@ -1458,12 +1432,6 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1491,7 +1459,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.18
uses: github/codeql-action/init@v3.28.16
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.18
uses: github/codeql-action/analyze@v3.28.16
with:
category: "/language:python"

View File

@@ -270,7 +270,6 @@ homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
@@ -333,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.*
@@ -386,7 +384,6 @@ homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
homeassistant.components.pandora.*
homeassistant.components.panel_custom.*
homeassistant.components.paperless_ngx.*
homeassistant.components.peblar.*
homeassistant.components.peco.*
homeassistant.components.pegel_online.*
@@ -436,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.*

39
CODEOWNERS generated
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
@@ -202,8 +202,8 @@ build.json @home-assistant/supervisor
/tests/components/blebox/ @bbx-a @swistakm
/homeassistant/components/blink/ @fronzbot @mkmer
/tests/components/blink/ @fronzbot @mkmer
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
/tests/components/blue_current/ @Floris272 @gleeuwen
/homeassistant/components/bluemaestro/ @bdraco
/tests/components/bluemaestro/ @bdraco
/homeassistant/components/blueprint/ @home-assistant/core
@@ -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
@@ -710,8 +710,6 @@ build.json @home-assistant/supervisor
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
@@ -1113,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
@@ -1140,8 +1138,6 @@ build.json @home-assistant/supervisor
/tests/components/palazzetti/ @dotvav
/homeassistant/components/panel_custom/ @home-assistant/frontend
/tests/components/panel_custom/ @home-assistant/frontend
/homeassistant/components/paperless_ngx/ @fvgarrel
/tests/components/paperless_ngx/ @fvgarrel
/homeassistant/components/peblar/ @frenck
/tests/components/peblar/ @frenck
/homeassistant/components/peco/ @IceBotYT
@@ -1180,8 +1176,6 @@ build.json @home-assistant/supervisor
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
/homeassistant/components/private_ble_device/ @Jc2k
/tests/components/private_ble_device/ @Jc2k
/homeassistant/components/probe_plus/ @pantherale0
/tests/components/probe_plus/ @pantherale0
/homeassistant/components/profiler/ @bdraco
/tests/components/profiler/ @bdraco
/homeassistant/components/progettihwsw/ @ardaseremet
@@ -1228,7 +1222,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/qnap_qsw/ @Noltari
/tests/components/qnap_qsw/ @Noltari
/homeassistant/components/quantum_gateway/ @cisasteelersfan
/tests/components/quantum_gateway/ @cisasteelersfan
/homeassistant/components/qvr_pro/ @oblogic7
/homeassistant/components/qwikswitch/ @kellerza
/tests/components/qwikswitch/ @kellerza
@@ -1314,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
@@ -1491,8 +1486,8 @@ build.json @home-assistant/supervisor
/tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii @jb101010-2
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen
@@ -1505,8 +1500,8 @@ build.json @home-assistant/supervisor
/tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbee/ @jafar-atili
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
@@ -1546,8 +1541,8 @@ build.json @home-assistant/supervisor
/tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @Petro31 @home-assistant/core
/tests/components/template/ @Petro31 @home-assistant/core
/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks
@@ -1801,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

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",

View File

@@ -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"],

View File

@@ -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"],

View File

@@ -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",

View File

@@ -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,

View File

@@ -6,7 +6,6 @@ from typing import Any, Concatenate
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -30,7 +29,6 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
model_id=measures.model,
serial_number=coordinator.serial_number,
sw_version=measures.firmware_version,
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
)

View File

@@ -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"],

View File

@@ -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",

View File

@@ -142,7 +142,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode]
@property
def hvac_modes(self) -> list[HVACMode]:
def hvac_modes(self):
"""Return the list of available operation modes."""
airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number)
modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes]
@@ -226,12 +226,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
return super()._handle_coordinator_update()
@property
def min_temp(self) -> float:
def min_temp(self):
"""Return Minimum Temperature for AC of this group."""
return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint
@property
def max_temp(self) -> float:
def max_temp(self):
"""Return Max Temperature for AC of this group."""
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint

View File

@@ -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": {

View File

@@ -17,11 +17,4 @@ CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
THINKING_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
"claude-opus-4-20250514",
"claude-opus-4-0",
"claude-sonnet-4-20250514",
"claude-sonnet-4-0",
]
THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-3-7-sonnet-latest"]

View File

@@ -294,8 +294,6 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
@@ -328,7 +326,6 @@ class AnthropicConversationEntity(
_attr_has_entity_name = True
_attr_name = None
_attr_supports_streaming = True
def __init__(self, entry: AnthropicConfigEntry) -> None:
"""Initialize the agent."""

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.52.0"]
"requirements": ["anthropic==0.47.2"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.9.0"]
"requirements": ["pyaprilaire==0.8.1"]
}

View File

@@ -20,6 +20,9 @@ import hass_nabucasa
import voluptuous as vol
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
from homeassistant.components.tts import (
generate_media_source_id as tts_generate_media_source_id,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -89,8 +92,6 @@ KEY_ASSIST_PIPELINE: HassKey[PipelineData] = HassKey(DOMAIN)
KEY_PIPELINE_CONVERSATION_DATA: HassKey[dict[str, PipelineConversationData]] = HassKey(
"pipeline_conversation_data"
)
# Number of response parts to handle before streaming the response
STREAM_RESPONSE_CHARS = 60
def validate_language(data: dict[str, Any]) -> Any:
@@ -554,7 +555,7 @@ class PipelineRun:
event_callback: PipelineEventCallback
language: str = None # type: ignore[assignment]
runner_data: Any | None = None
intent_agent: conversation.AgentInfo | None = None
intent_agent: str | None = None
tts_audio_output: str | dict[str, Any] | None = None
wake_word_settings: WakeWordSettings | None = None
audio_settings: AudioSettings = field(default_factory=AudioSettings)
@@ -590,9 +591,6 @@ class PipelineRun:
_intent_agent_only = False
"""If request should only be handled by agent, ignoring sentence triggers and local processing."""
_streamed_response_text = False
"""If the conversation agent streamed response text to TTS result."""
def __post_init__(self) -> None:
"""Set language for pipeline."""
self.language = self.pipeline.language or self.hass.config.language
@@ -654,11 +652,6 @@ class PipelineRun:
"token": self.tts_stream.token,
"url": self.tts_stream.url,
"mime_type": self.tts_stream.content_type,
"stream_response": (
self.tts_stream.supports_streaming_input
and self.intent_agent
and self.intent_agent.supports_streaming
),
}
self.process_event(PipelineEvent(PipelineEventType.RUN_START, data))
@@ -906,12 +899,12 @@ class PipelineRun:
) -> str:
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
# Create a background task to prepare the conversation agent
if self.end_stage >= PipelineStage.INTENT and self.intent_agent:
if self.end_stage >= PipelineStage.INTENT:
self.hass.async_create_background_task(
conversation.async_prepare_agent(
self.hass, self.intent_agent.id, self.language
self.hass, self.intent_agent, self.language
),
f"prepare conversation agent {self.intent_agent.id}",
f"prepare conversation agent {self.intent_agent}",
)
if isinstance(self.stt_provider, stt.Provider):
@@ -1052,7 +1045,7 @@ class PipelineRun:
message=f"Intent recognition engine {engine} is not found",
)
self.intent_agent = agent_info
self.intent_agent = agent_info.id
async def recognize_intent(
self,
@@ -1085,7 +1078,7 @@ class PipelineRun:
PipelineEvent(
PipelineEventType.INTENT_START,
{
"engine": self.intent_agent.id,
"engine": self.intent_agent,
"language": input_language,
"intent_input": intent_input,
"conversation_id": conversation_id,
@@ -1102,11 +1095,11 @@ class PipelineRun:
conversation_id=conversation_id,
device_id=device_id,
language=input_language,
agent_id=self.intent_agent.id,
agent_id=self.intent_agent,
extra_system_prompt=conversation_extra_system_prompt,
)
agent_id = self.intent_agent.id
agent_id = self.intent_agent
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
@@ -1128,7 +1121,7 @@ class PipelineRun:
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
intent_agent_state := self.hass.states.get(self.intent_agent)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
@@ -1150,13 +1143,6 @@ class PipelineRun:
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
if self.tts_stream and self.tts_stream.supports_streaming_input:
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
else:
tts_input_stream = None
chat_log_role = None
delta_character_count = 0
@callback
def chat_log_delta_listener(
chat_log: conversation.ChatLog, delta: dict
@@ -1170,42 +1156,6 @@ class PipelineRun:
},
)
)
if tts_input_stream is None:
return
nonlocal chat_log_role
if role := delta.get("role"):
chat_log_role = role
# We are only interested in assistant deltas with content
if chat_log_role != "assistant" or not (
content := delta.get("content")
):
return
tts_input_stream.put_nowait(content)
if self._streamed_response_text:
return
nonlocal delta_character_count
delta_character_count += len(content)
if delta_character_count < STREAM_RESPONSE_CHARS:
return
# Streamed responses are not cached. We only start streaming text after
# we have received a couple of words that indicates it will be a long response.
self._streamed_response_text = True
async def tts_input_stream_generator() -> AsyncGenerator[str]:
"""Yield TTS input stream."""
while (tts_input := await tts_input_stream.get()) is not None:
yield tts_input
assert self.tts_stream is not None
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
with (
chat_session.async_get_chat_session(
@@ -1249,8 +1199,6 @@ class PipelineRun:
speech = conversation_result.response.speech.get("plain", {}).get(
"speech", ""
)
if tts_input_stream and self._streamed_response_text:
tts_input_stream.put_nowait(None)
except Exception as src_error:
_LOGGER.exception("Unexpected error during intent recognition")
@@ -1328,11 +1276,26 @@ class PipelineRun:
)
)
if not self._streamed_response_text:
self.tts_stream.async_set_message(tts_input)
try:
# Synthesize audio and get URL
tts_media_id = tts_generate_media_source_id(
self.hass,
tts_input,
engine=self.tts_stream.engine,
language=self.tts_stream.language,
options=self.tts_stream.options,
)
except Exception as src_error:
_LOGGER.exception("Unexpected error during text-to-speech")
raise TextToSpeechError(
code="tts-failed",
message="Unexpected error during text-to-speech",
) from src_error
self.tts_stream.async_set_message(tts_input)
tts_output = {
"media_id": self.tts_stream.media_source_id,
"media_id": tts_media_id,
"token": self.tts_stream.token,
"url": self.tts_stream.url,
"mime_type": self.tts_stream.content_type,

View File

@@ -18,7 +18,7 @@
},
"step": {
"validation": {
"title": "Two-factor authentication",
"title": "Two factor authentication",
"data": {
"verification_code": "Verification code"
},

View File

@@ -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": {

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",

View File

@@ -47,7 +47,7 @@ from .const import (
CONF_VIDEO_SOURCE,
DEFAULT_STREAM_PROFILE,
DEFAULT_VIDEO_SOURCE,
DOMAIN,
DOMAIN as AXIS_DOMAIN,
)
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
@@ -58,7 +58,7 @@ DEFAULT_PROTOCOL = "https"
PROTOCOL_CHOICES = ["https", "http"]
class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
"""Handle a Axis config flow."""
VERSION = 3
@@ -146,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
model = self.config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]

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,
)

View File

@@ -2,8 +2,8 @@
from aiohttp import ClientTimeout
from azure.core.exceptions import (
AzureError,
ClientAuthenticationError,
HttpResponseError,
ResourceNotFoundError,
)
from azure.core.pipeline.transport._aiohttp import (
@@ -70,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 AzureError as err:
except HttpResponseError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",

View File

@@ -8,7 +8,7 @@ import json
import logging
from typing import Any, Concatenate
from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError
from azure.core.exceptions import HttpResponseError
from azure.storage.blob import BlobProperties
from homeassistant.components.backup import (
@@ -80,20 +80,6 @@ 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

View File

@@ -23,7 +23,6 @@ from .const import DATA_MANAGER, DOMAIN
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
from .http import async_register_http_views
from .manager import (
AddonErrorData,
BackupManager,
BackupManagerError,
BackupPlatformEvent,
@@ -49,7 +48,6 @@ from .util import suggested_filename, suggested_filename_from_name_date
from .websocket import async_register_websocket_handlers
__all__ = [
"AddonErrorData",
"AddonInfo",
"AgentBackup",
"BackupAgent",
@@ -81,7 +79,7 @@ __all__ = [
"suggested_filename_from_name_date",
]
PLATFORMS = [Platform.EVENT, Platform.SENSOR]
PLATFORMS = [Platform.SENSOR]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View File

@@ -30,10 +30,8 @@ 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
last_event: ManagerStateEvent | BackupPlatformEvent | None
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
@@ -61,23 +59,19 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
]
self.backup_manager = backup_manager
self._last_event: ManagerStateEvent | BackupPlatformEvent | None = None
@callback
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
"""Handle new event."""
LOGGER.debug("Received backup event: %s", event)
self._last_event = event
self.config_entry.async_create_task(self.hass, self.async_refresh())
async def _async_update_data(self) -> 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,
self._last_event,
)
@callback

View File

@@ -11,7 +11,7 @@ from .const import DOMAIN
from .coordinator import BackupDataUpdateCoordinator
class BackupManagerBaseEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
"""Base entity for backup manager."""
_attr_has_entity_name = True
@@ -19,9 +19,12 @@ class BackupManagerBaseEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
def __init__(
self,
coordinator: BackupDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize base entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = entity_description.key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, "backup_manager")},
manufacturer="Home Assistant",
@@ -31,17 +34,3 @@ class BackupManagerBaseEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
entry_type=DeviceEntryType.SERVICE,
configuration_url="homeassistant://config/backup",
)
class BackupManagerEntity(BackupManagerBaseEntity):
"""Entity for backup manager."""
def __init__(
self,
coordinator: BackupDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = entity_description.key

View File

@@ -1,59 +0,0 @@
"""Event platform for Home Assistant Backup integration."""
from __future__ import annotations
from typing import Final
from homeassistant.components.event import EventEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
from .entity import BackupManagerBaseEntity
from .manager import CreateBackupEvent, CreateBackupState
ATTR_BACKUP_STAGE: Final[str] = "backup_stage"
ATTR_FAILED_REASON: Final[str] = "failed_reason"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BackupConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Event set up for backup config entry."""
coordinator = config_entry.runtime_data
async_add_entities([AutomaticBackupEvent(coordinator)])
class AutomaticBackupEvent(BackupManagerBaseEntity, EventEntity):
"""Representation of an automatic backup event."""
_attr_event_types = [s.value for s in CreateBackupState]
_unrecorded_attributes = frozenset({ATTR_FAILED_REASON, ATTR_BACKUP_STAGE})
coordinator: BackupDataUpdateCoordinator
def __init__(self, coordinator: BackupDataUpdateCoordinator) -> None:
"""Initialize the automatic backup event."""
super().__init__(coordinator)
self._attr_unique_id = "automatic_backup_event"
self._attr_translation_key = "automatic_backup_event"
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
not (data := self.coordinator.data)
or (event := data.last_event) is None
or not isinstance(event, CreateBackupEvent)
):
return
self._trigger_event(
event.state,
{
ATTR_BACKUP_STAGE: event.stage,
ATTR_FAILED_REASON: event.reason,
},
)
self.async_write_ha_state()

View File

@@ -1,11 +1,4 @@
{
"entity": {
"event": {
"automatic_backup_event": {
"default": "mdi:database"
}
}
},
"services": {
"create": {
"service": "mdi:cloud-upload"

View File

@@ -106,21 +106,11 @@ class ManagerBackup(BaseBackup):
with_automatic_settings: bool | None
@dataclass(frozen=True, kw_only=True, slots=True)
class AddonErrorData:
"""Addon error class."""
name: str
errors: list[tuple[str, str]]
@dataclass(frozen=True, kw_only=True, slots=True)
class WrittenBackup:
"""Written backup class."""
addon_errors: dict[str, AddonErrorData]
backup: AgentBackup
folder_errors: dict[Folder, list[tuple[str, str]]]
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]]
release_stream: Callable[[], Coroutine[Any, Any, None]]
@@ -1218,9 +1208,7 @@ class BackupManager:
backup_success = True
if with_automatic_settings:
self._update_issue_after_agent_upload(
written_backup, agent_errors, unavailable_agents
)
self._update_issue_after_agent_upload(agent_errors, unavailable_agents)
# delete old backups more numerous than copies
# try this regardless of agent errors above
await delete_backups_exceeding_configured_count(self)
@@ -1366,10 +1354,8 @@ class BackupManager:
for subscription in self._backup_event_subscriptions:
subscription(event)
def _create_automatic_backup_failed_issue(
self, translation_key: str, translation_placeholders: dict[str, str] | None
) -> None:
"""Create an issue in the issue registry for automatic backup failures."""
def _update_issue_backup_failed(self) -> None:
"""Update issue registry when a backup fails."""
ir.async_create_issue(
self.hass,
DOMAIN,
@@ -1378,64 +1364,37 @@ class BackupManager:
is_persistent=True,
learn_more_url="homeassistant://config/backup",
severity=ir.IssueSeverity.WARNING,
translation_key=translation_key,
translation_placeholders=translation_placeholders,
)
def _update_issue_backup_failed(self) -> None:
"""Update issue registry when a backup fails."""
self._create_automatic_backup_failed_issue(
"automatic_backup_failed_create", None
translation_key="automatic_backup_failed_create",
)
def _update_issue_after_agent_upload(
self,
written_backup: WrittenBackup,
agent_errors: dict[str, Exception],
unavailable_agents: list[str],
self, agent_errors: dict[str, Exception], unavailable_agents: list[str]
) -> None:
"""Update issue registry after a backup is uploaded to agents."""
addon_errors = written_backup.addon_errors
failed_agents = unavailable_agents + [
self.backup_agents[agent_id].name for agent_id in agent_errors
]
folder_errors = written_backup.folder_errors
if not failed_agents and not addon_errors and not folder_errors:
# No issues to report, clear previous error
if not agent_errors and not unavailable_agents:
ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed")
return
if failed_agents and not (addon_errors or folder_errors):
# No issues with add-ons or folders, but issues with agents
self._create_automatic_backup_failed_issue(
"automatic_backup_failed_upload_agents",
{"failed_agents": ", ".join(failed_agents)},
)
elif addon_errors and not (failed_agents or folder_errors):
# No issues with agents or folders, but issues with add-ons
self._create_automatic_backup_failed_issue(
"automatic_backup_failed_addons",
{"failed_addons": ", ".join(val.name for val in addon_errors.values())},
)
elif folder_errors and not (failed_agents or addon_errors):
# No issues with agents or add-ons, but issues with folders
self._create_automatic_backup_failed_issue(
"automatic_backup_failed_folders",
{"failed_folders": ", ".join(folder for folder in folder_errors)},
)
else:
# Issues with agents, add-ons, and/or folders
self._create_automatic_backup_failed_issue(
"automatic_backup_failed_agents_addons_folders",
{
"failed_agents": ", ".join(failed_agents) or "-",
"failed_addons": (
", ".join(val.name for val in addon_errors.values()) or "-"
),
"failed_folders": ", ".join(f for f in folder_errors) or "-",
},
)
ir.async_create_issue(
self.hass,
DOMAIN,
"automatic_backup_failed",
is_fixable=False,
is_persistent=True,
learn_more_url="homeassistant://config/backup",
severity=ir.IssueSeverity.WARNING,
translation_key="automatic_backup_failed_upload_agents",
translation_placeholders={
"failed_agents": ", ".join(
chain(
(
self.backup_agents[agent_id].name
for agent_id in agent_errors
),
unavailable_agents,
)
)
},
)
async def async_can_decrypt_on_download(
self,
@@ -1718,11 +1677,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
raise BackupReaderWriterError(str(err)) from err
return WrittenBackup(
addon_errors={},
backup=backup,
folder_errors={},
open_stream=open_backup,
release_stream=remove_backup,
backup=backup, open_stream=open_backup, release_stream=remove_backup
)
finally:
# Inform integrations the backup is done
@@ -1861,11 +1816,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
await async_add_executor_job(temp_file.unlink, True)
return WrittenBackup(
addon_errors={},
backup=backup,
folder_errors={},
open_stream=open_backup,
release_stream=remove_backup,
backup=backup, open_stream=open_backup, release_stream=remove_backup
)
async def async_restore_backup(

View File

@@ -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,
),
)

View File

@@ -77,10 +77,7 @@ class _BackupStore(Store[StoredBackupData]):
for agent in data["config"]["agents"]:
data["config"]["agents"][agent]["retention"] = None
# Note: We allow reading data with major version 2 in which the unused key
# data["config"]["schedule"]["state"] will be removed. The bump to 2 is
# planned to happen after a 6 month quiet period with no minor version
# changes.
# Note: We allow reading data with major version 2.
# Reject if major version is higher than 2.
if old_major_version > 2:
raise NotImplementedError

View File

@@ -11,18 +11,6 @@
"automatic_backup_failed_upload_agents": {
"title": "Automatic backup could not be uploaded to the configured locations",
"description": "The automatic backup could not be uploaded to the configured locations {failed_agents}. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_addons": {
"title": "Not all add-ons could be included in automatic backup",
"description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_agents_addons_folders": {
"title": "Automatic backup was created with errors",
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the core and supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_folders": {
"title": "Not all folders could be included in automatic backup",
"description": "Folders {failed_folders} could not be included in automatic backup. Please check the supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
}
},
"services": {
@@ -36,22 +24,6 @@
}
},
"entity": {
"event": {
"automatic_backup_event": {
"name": "Automatic backup",
"state_attributes": {
"event_type": {
"state": {
"completed": "Completed successfully",
"failed": "Failed",
"in_progress": "In progress"
}
},
"backup_stage": { "name": "Backup stage" },
"failed_reason": { "name": "Failure reason" }
}
}
},
"sensor": {
"backup_manager_state": {
"name": "Backup Manager state",
@@ -65,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"
}

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)

View File

@@ -21,6 +21,7 @@ from .entity import BleBoxEntity
SCAN_INTERVAL = timedelta(seconds=5)
BLEBOX_TO_HVACMODE = {
None: None,
0: HVACMode.OFF,
1: HVACMode.HEAT,
2: HVACMode.COOL,
@@ -58,14 +59,12 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@property
def hvac_modes(self) -> list[HVACMode]:
def hvac_modes(self):
"""Return list of supported HVAC modes."""
if self._feature.mode is None:
return [HVACMode.OFF]
return [HVACMode.OFF, BLEBOX_TO_HVACMODE[self._feature.mode]]
@property
def hvac_mode(self) -> HVACMode | None:
def hvac_mode(self):
"""Return the desired HVAC mode."""
if self._feature.is_on is None:
return None
@@ -76,7 +75,7 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn
return HVACMode.HEAT if self._feature.is_on else HVACMode.OFF
@property
def hvac_action(self) -> HVACAction | None:
def hvac_action(self):
"""Return the actual current HVAC action."""
if self._feature.hvac_action is not None:
if not self._feature.is_on:
@@ -89,22 +88,22 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn
return HVACAction.HEATING if self._feature.is_heating else HVACAction.IDLE
@property
def max_temp(self) -> float:
def max_temp(self):
"""Return the maximum temperature supported."""
return self._feature.max_temp
@property
def min_temp(self) -> float:
def min_temp(self):
"""Return the maximum temperature supported."""
return self._feature.min_temp
@property
def current_temperature(self) -> float | None:
def current_temperature(self):
"""Return the current temperature."""
return self._feature.current
@property
def target_temperature(self) -> float | None:
def target_temperature(self):
"""Return the desired thermostat temperature."""
return self._feature.desired

View File

@@ -84,7 +84,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp)
@property
def color_mode(self) -> ColorMode:
def color_mode(self):
"""Return the color mode.
Set values to _attr_ibutes if needed.
@@ -92,7 +92,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
@property
def supported_color_modes(self) -> set[ColorMode]:
def supported_color_modes(self):
"""Return supported color modes."""
return {self.color_mode}
@@ -107,7 +107,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
return self._feature.effect
@property
def rgb_color(self) -> tuple[int, int, int] | None:
def rgb_color(self):
"""Return value for rgb."""
if (rgb_hex := self._feature.rgb_hex) is None:
return None
@@ -118,14 +118,14 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
)
@property
def rgbw_color(self) -> tuple[int, int, int, int] | None:
def rgbw_color(self):
"""Return the hue and saturation."""
if (rgbw_hex := self._feature.rgbw_hex) is None:
return None
return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbw_hex)[0:4])
@property
def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
def rgbww_color(self):
"""Return value for rgbww."""
if (rgbww_hex := self._feature.rgbww_hex) is None:
return None

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."
}
}
}

View File

@@ -24,7 +24,7 @@ from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
type BlueCurrentConfigEntry = ConfigEntry[Connector]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
PLATFORMS = [Platform.SENSOR]
CHARGE_POINTS = "CHARGE_POINTS"
DATA = "data"
DELAY = 5

View File

@@ -1,89 +0,0 @@
"""Support for Blue Current buttons."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from bluecurrent_api.client import Client
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BlueCurrentConfigEntry, Connector
from .entity import ChargepointEntity
@dataclass(kw_only=True, frozen=True)
class ChargePointButtonEntityDescription(ButtonEntityDescription):
"""Describes a Blue Current button entity."""
function: Callable[[Client, str], Coroutine[Any, Any, None]]
CHARGE_POINT_BUTTONS = (
ChargePointButtonEntityDescription(
key="reset",
translation_key="reset",
function=lambda client, evse_id: client.reset(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="reboot",
translation_key="reboot",
function=lambda client, evse_id: client.reboot(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="stop_charge_session",
translation_key="stop_charge_session",
function=lambda client, evse_id: client.stop_session(evse_id),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: BlueCurrentConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Blue Current buttons."""
connector: Connector = entry.runtime_data
async_add_entities(
ChargePointButton(
connector,
button,
evse_id,
)
for evse_id in connector.charge_points
for button in CHARGE_POINT_BUTTONS
)
class ChargePointButton(ChargepointEntity, ButtonEntity):
"""Define a charge point button."""
has_value = True
entity_description: ChargePointButtonEntityDescription
def __init__(
self,
connector: Connector,
description: ChargePointButtonEntityDescription,
evse_id: str,
) -> None:
"""Initialize the button."""
super().__init__(connector, evse_id)
self.entity_description = description
self._attr_unique_id = f"{description.key}_{evse_id}"
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.function(self.connector.client, self.evse_id)

View File

@@ -1,5 +1,7 @@
"""Entity representing a Blue Current charge point."""
from abc import abstractmethod
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -15,12 +17,12 @@ class BlueCurrentEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
has_value = False
def __init__(self, connector: Connector, signal: str) -> None:
"""Initialize the entity."""
self.connector = connector
self.signal = signal
self.has_value = False
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -41,6 +43,7 @@ class BlueCurrentEntity(Entity):
return self.connector.connected and self.has_value
@callback
@abstractmethod
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""

View File

@@ -19,17 +19,6 @@
"current_left": {
"default": "mdi:gauge"
}
},
"button": {
"reset": {
"default": "mdi:restart"
},
"reboot": {
"default": "mdi:restart-alert"
},
"stop_charge_session": {
"default": "mdi:stop"
}
}
}
}

View File

@@ -1,7 +1,7 @@
{
"domain": "blue_current",
"name": "Blue Current",
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
"codeowners": ["@Floris272", "@gleeuwen"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blue_current",
"iot_class": "cloud_push",

View File

@@ -113,17 +113,6 @@
"grid_max_current": {
"name": "Max grid current"
}
},
"button": {
"stop_charge_session": {
"name": "Stop charge session"
},
"reboot": {
"name": "Reboot"
},
"reset": {
"name": "Reset"
}
}
}
}

View File

@@ -21,7 +21,6 @@ from .coordinator import (
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BUTTON,
Platform.MEDIA_PLAYER,
]

View File

@@ -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)

View File

@@ -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()

View File

@@ -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"
}
}
}
}

View File

@@ -18,7 +18,7 @@
"bleak==0.22.3",
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-auto-recovery==1.5.1",
"bluetooth-data-tools==1.28.1",
"dbus-fast==2.43.0",
"habluetooth==3.48.2"

View File

@@ -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

View File

@@ -22,7 +22,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
from .const import (
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
DOMAIN as BMW_DOMAIN,
SCAN_INTERVALS,
)
_LOGGER = logging.getLogger(__name__)
@@ -57,7 +63,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}",
update_interval=timedelta(
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
),
@@ -75,26 +81,26 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
# Clear refresh token and trigger reauth if previous update failed as well
self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="invalid_auth",
) from err
except (MyBMWAPIError, RequestError) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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": {

View File

@@ -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

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:

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])

View File

@@ -6,31 +6,17 @@ from ssl import SSLError
from bosch_alarm_mode2 import Panel
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import device_registry as dr
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
from .services import setup_services
from .types import BoschAlarmConfigEntry
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up bosch alarm services."""
setup_services(hass)
return True
type BoschAlarmConfigEntry = ConfigEntry[Panel]
async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
@@ -62,11 +48,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
device_registry = dr.async_get(hass)
mac = entry.data.get(CONF_MAC)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",

View File

@@ -12,8 +12,8 @@ from homeassistant.components.alarm_control_panel import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity
from .types import BoschAlarmConfigEntry
async def async_setup_entry(
@@ -34,9 +34,6 @@ async def async_setup_entry(
)
PARALLEL_UPDATES = 0
class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel."""

View File

@@ -1,220 +0,0 @@
"""Support for Bosch Alarm Panel binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity, BoschAlarmEntity, BoschAlarmPointEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmFaultEntityDescription(BinarySensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
fault: int
FAULT_TYPES = [
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_low",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.BATTERY,
fault=ALARM_PANEL_FAULTS.BATTERY_LOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_mising",
translation_key="panel_fault_battery_mising",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.BATTERY_MISING,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_ac_fail",
translation_key="panel_fault_ac_fail",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.AC_FAIL,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_phone_line_failure",
translation_key="panel_fault_phone_line_failure",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
fault=ALARM_PANEL_FAULTS.PHONE_LINE_FAILURE,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_parameter_crc_fail_in_pif",
translation_key="panel_fault_parameter_crc_fail_in_pif",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.PARAMETER_CRC_FAIL_IN_PIF,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_communication_fail_since_rps_hang_up",
translation_key="panel_fault_communication_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.COMMUNICATION_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_sdi_fail_since_rps_hang_up",
translation_key="panel_fault_sdi_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.SDI_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_user_code_tamper_since_rps_hang_up",
translation_key="panel_fault_user_code_tamper_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.USER_CODE_TAMPER_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_fail_to_call_rps_since_rps_hang_up",
translation_key="panel_fault_fail_to_call_rps_since_rps_hang_up",
entity_registry_enabled_default=False,
fault=ALARM_PANEL_FAULTS.FAIL_TO_CALL_RPS_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_point_bus_fail_since_rps_hang_up",
translation_key="panel_fault_point_bus_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.POINT_BUS_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_overflow",
translation_key="panel_fault_log_overflow",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_OVERFLOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_threshold",
translation_key="panel_fault_log_threshold",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_THRESHOLD,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors for alarm points and the connection status."""
panel = config_entry.runtime_data
entities: list[BinarySensorEntity] = [
PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id)
for point_id in panel.points
]
entities.extend(
PanelFaultsSensor(
panel,
config_entry.unique_id or config_entry.entry_id,
fault_type,
)
for fault_type in FAULT_TYPES
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "away"
)
for area_id in panel.areas
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "home"
)
for area_id in panel.areas
)
async_add_entities(entities)
PARALLEL_UPDATES = 0
class PanelFaultsSensor(BoschAlarmEntity, BinarySensorEntity):
"""A binary sensor entity for each fault type in a bosch alarm panel."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: BoschAlarmFaultEntityDescription
def __init__(
self,
panel: Panel,
unique_id: str,
entity_description: BoschAlarmFaultEntityDescription,
) -> None:
"""Set up a binary sensor entity for each fault type in a bosch alarm panel."""
super().__init__(panel, unique_id, True)
self.entity_description = entity_description
self._fault_type = entity_description.fault
self._attr_unique_id = f"{unique_id}_fault_{entity_description.key}"
@property
def is_on(self) -> bool:
"""Return if this fault has occurred."""
return self._fault_type in self.panel.panel_faults_ids
class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity):
"""A binary sensor entity showing if a panel is ready to arm."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self, panel: Panel, area_id: int, unique_id: str, arm_type: str
) -> None:
"""Set up a binary sensor entity for the arming status in a bosch alarm panel."""
super().__init__(panel, area_id, unique_id, False, False, True)
self.panel = panel
self._arm_type = arm_type
self._attr_translation_key = f"area_ready_to_arm_{arm_type}"
self._attr_unique_id = f"{self._area_unique_id}_ready_to_arm_{arm_type}"
@property
def is_on(self) -> bool:
"""Return if this panel is ready to arm."""
if self._arm_type == "away":
return self._area.all_ready
if self._arm_type == "home":
return self._area.all_ready or self._area.part_ready
return False
class PointSensor(BoschAlarmPointEntity, BinarySensorEntity):
"""A binary sensor entity for a point in a bosch alarm panel."""
_attr_name = None
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a binary sensor entity for a point in a bosch alarm panel."""
super().__init__(panel, point_id, unique_id)
self._attr_unique_id = self._point_unique_id
@property
def is_on(self) -> bool:
"""Return if this point sensor is on."""
return self._point.is_open()

View File

@@ -6,30 +6,25 @@ import asyncio
from collections.abc import Mapping
import logging
import ssl
from typing import Any, Self
from typing import Any
from bosch_alarm_mode2 import Panel
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_DHCP,
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import (
CONF_CODE,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_PASSWORD,
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
@@ -93,12 +88,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Init config flow."""
self._data: dict[str, Any] = {}
self.mac: str | None = None
self.host: str | None = None
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return self.mac == other_flow.mac or self.host == other_flow.host
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -107,12 +96,9 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
if self.source == SOURCE_USER:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(user_input, 0)
(model, serial) = await try_connect(user_input, 0)
except (
OSError,
ConnectionRefusedError,
@@ -143,70 +129,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
self.mac = format_mac(discovery_info.macaddress)
self.host = discovery_info.ip
if self.hass.config_entries.flow.async_has_matching_flow(self):
return self.async_abort(reason="already_in_progress")
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data.get(CONF_MAC) == self.mac:
result = self.hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_HOST: discovery_info.ip,
},
)
if result:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
if entry.data[CONF_HOST] == discovery_info.ip:
if (
not entry.data.get(CONF_MAC)
and entry.state is ConfigEntryState.LOADED
):
result = self.hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_MAC: self.mac,
},
)
if result:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(
{CONF_HOST: discovery_info.ip, CONF_PORT: 7700}, 0
)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
asyncio.exceptions.TimeoutError,
):
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
self.context["title_placeholders"] = {
"model": model,
"host": discovery_info.ip,
}
self._data = {
CONF_HOST: discovery_info.ip,
CONF_MAC: self.mac,
CONF_MODEL: model,
CONF_PORT: 7700,
}
return await self.async_step_auth()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -250,7 +172,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if serial_number:
await self.async_set_unique_id(str(serial_number))
if self.source in (SOURCE_USER, SOURCE_DHCP):
if self.source == SOURCE_USER:
if serial_number:
self._abort_if_unique_id_configured()
else:
@@ -262,7 +184,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
)
if serial_number:
self._abort_if_unique_id_mismatch(reason="device_mismatch")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data=self._data,

View File

@@ -1,9 +1,6 @@
"""Constants for the Bosch Alarm integration."""
DOMAIN = "bosch_alarm"
ATTR_HISTORY = "history"
HISTORY_ATTR = "history"
CONF_INSTALLER_CODE = "installer_code"
CONF_USER_CODE = "user_code"
ATTR_DATETIME = "datetime"
SERVICE_SET_DATE_TIME = "set_date_time"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"

View File

@@ -6,8 +6,8 @@ from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from . import BoschAlarmConfigEntry
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
from .types import BoschAlarmConfigEntry
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]

View File

@@ -17,13 +17,9 @@ class BoschAlarmEntity(Entity):
_attr_has_entity_name = True
def __init__(
self, panel: Panel, unique_id: str, observe_faults: bool = False
) -> None:
def __init__(self, panel: Panel, unique_id: str) -> None:
"""Set up a entity for a bosch alarm panel."""
self.panel = panel
self._observe_faults = observe_faults
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
@@ -38,14 +34,10 @@ class BoschAlarmEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
class BoschAlarmAreaEntity(BoschAlarmEntity):
@@ -94,84 +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 BoschAlarmPointEntity(BoschAlarmEntity):
"""A base entity for point related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._point_id = point_id
self._point_unique_id = f"{unique_id}_point_{point_id}"
self._point = panel.points[point_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._point_unique_id)},
name=self._point.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._point.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._point.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)

View File

@@ -1,80 +1,8 @@
{
"services": {
"set_date_time": {
"service": "mdi:clock-edit"
}
},
"entity": {
"sensor": {
"alarms_gas": {
"default": "mdi:alert-circle"
},
"alarms_fire": {
"default": "mdi:alert-circle"
},
"alarms_burglary": {
"default": "mdi:alert-circle"
},
"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"
}
}
},
"binary_sensor": {
"panel_fault_parameter_crc_fail_in_pif": {
"default": "mdi:alert-circle"
},
"panel_fault_phone_line_failure": {
"default": "mdi:alert-circle"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_log_overflow": {
"default": "mdi:alert-circle"
},
"panel_fault_log_threshold": {
"default": "mdi:alert-circle"
},
"area_ready_to_arm_away": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-lock"
}
},
"area_ready_to_arm_home": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-home"
}
"default": "mdi:alert-circle-outline"
}
}
}

View File

@@ -3,11 +3,6 @@
"name": "Bosch Alarm",
"codeowners": ["@mag1024", "@sanjay900"],
"config_flow": true,
"dhcp": [
{
"macaddress": "000463*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",

View File

@@ -13,7 +13,10 @@ rules:
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
@@ -26,22 +29,25 @@ rules:
unique-config-entry: done
# Silver
action-exceptions: done
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
parallel-updates: todo
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: done
discovery: done
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo

View File

@@ -6,7 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES
from bosch_alarm_mode2.panel import Area
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
@@ -16,53 +15,18 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity
ALARM_TYPES = {
"burglary": {
ALARM_MEMORY_PRIORITIES.BURGLARY_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.BURGLARY_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.BURGLARY_ALARM: "alarm",
},
"gas": {
ALARM_MEMORY_PRIORITIES.GAS_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.GAS_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.GAS_ALARM: "alarm",
},
"fire": {
ALARM_MEMORY_PRIORITIES.FIRE_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.FIRE_ALARM: "alarm",
},
}
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
value_fn: Callable[[Area], str | int]
value_fn: Callable[[Area], int]
observe_alarms: bool = False
observe_ready: bool = False
observe_status: bool = False
def priority_value_fn(priority_info: dict[int, str]) -> Callable[[Area], str]:
"""Build a value_fn for a given priority type."""
return lambda area: next(
(key for priority, key in priority_info.items() if priority in area.alarms_ids),
"no_issues",
)
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
*[
BoschAlarmSensorEntityDescription(
key=f"alarms_{key}",
translation_key=f"alarms_{key}",
value_fn=priority_value_fn(priority_type),
observe_alarms=True,
)
for key, priority_type in ALARM_TYPES.items()
],
BoschAlarmSensorEntityDescription(
key="faulting_points",
translation_key="faulting_points",
@@ -117,6 +81,6 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
@property
def native_value(self) -> str | int:
def native_value(self) -> int:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._area)

View File

@@ -1,77 +0,0 @@
"""Services for the bosch_alarm integration."""
from __future__ import annotations
import asyncio
import datetime as dt
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
from .types import BoschAlarmConfigEntry
def validate_datetime(value: Any) -> dt.datetime:
"""Validate that a provided datetime is supported on a bosch alarm panel."""
date_val = cv.datetime(value)
if date_val.year < 2010:
raise vol.RangeInvalid("datetime must be after 2009")
if date_val.year > 2037:
raise vol.RangeInvalid("datetime must be before 2038")
return date_val
SET_DATE_TIME_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_DATETIME): validate_datetime,
}
)
async def async_set_panel_date(call: ServiceCall) -> None:
"""Set the date and time on a bosch alarm panel."""
config_entry: BoschAlarmConfigEntry | None
value: dt.datetime = call.data.get(ATTR_DATETIME, dt_util.now())
entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
if not (config_entry := call.hass.config_entries.async_get_entry(entry_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_not_found",
translation_placeholders={"target": entry_id},
)
if config_entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="not_loaded",
translation_placeholders={"target": config_entry.title},
)
panel = config_entry.runtime_data
try:
await panel.set_panel_date(value)
except asyncio.InvalidStateError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={"target": config_entry.title},
) from err
def setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the bosch alarm integration."""
hass.services.async_register(
DOMAIN,
SERVICE_SET_DATE_TIME,
async_set_panel_date,
schema=SET_DATE_TIME_SCHEMA,
)

View File

@@ -1,12 +0,0 @@
set_date_time:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: bosch_alarm
datetime:
required: false
example: "2025-05-10 00:00:00"
selector:
datetime:

View File

@@ -1,6 +1,5 @@
{
"config": {
"flow_title": "{model} ({host})",
"step": {
"user": {
"data": {
@@ -43,7 +42,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
@@ -51,130 +49,15 @@
}
},
"exceptions": {
"integration_not_found": {
"message": "Integration \"{target}\" not found in registry."
},
"not_loaded": {
"message": "{target} is not loaded."
},
"connection_error": {
"message": "Could not connect to \"{target}\"."
},
"unknown_error": {
"message": "An unknown error occurred while setting the date and time on \"{target}\"."
},
"cannot_connect": {
"message": "Could not connect to panel."
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
},
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is momentarily unlocked."
}
},
"services": {
"set_date_time": {
"name": "Set date & time",
"description": "Sets the date and time on the alarm panel.",
"fields": {
"datetime": {
"name": "Date & time",
"description": "The date and time to set. The time zone of the Home Assistant instance is assumed. If omitted, the current date and time is used."
},
"config_entry_id": {
"name": "Config entry",
"description": "The Bosch Alarm integration ID."
}
}
}
},
"entity": {
"binary_sensor": {
"panel_fault_battery_mising": {
"name": "Battery missing"
},
"panel_fault_ac_fail": {
"name": "AC Failure"
},
"panel_fault_parameter_crc_fail_in_pif": {
"name": "CRC failure in panel configuration"
},
"panel_fault_phone_line_failure": {
"name": "Phone line failure"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"name": "SDI failure since last RPS connection"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"name": "User code tamper since last RPS connection"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"name": "Failure to call RPS since last RPS connection"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"name": "Point bus failure since last RPS connection"
},
"panel_fault_log_overflow": {
"name": "Log overflow"
},
"panel_fault_log_threshold": {
"name": "Log threshold reached"
},
"area_ready_to_arm_away": {
"name": "Area ready to arm away",
"state": {
"on": "Ready",
"off": "Not ready"
}
},
"area_ready_to_arm_home": {
"name": "Area ready to arm home",
"state": {
"on": "Ready",
"off": "Not ready"
}
}
},
"switch": {
"secured": {
"name": "Secured"
},
"cycling": {
"name": "Momentarily unlocked"
},
"locked": {
"name": "Locked"
}
},
"sensor": {
"alarms_gas": {
"name": "Gas alarm issues",
"state": {
"supervisory": "Supervisory",
"trouble": "Trouble",
"alarm": "Alarm",
"no_issues": "No issues"
}
},
"alarms_fire": {
"name": "Fire alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"alarms_burglary": {
"name": "Burglary alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"faulting_points": {
"name": "Faulting points",
"unit_of_measurement": "points"

View File

@@ -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)

View File

@@ -1,7 +0,0 @@
"""Types for the Bosch Alarm integration."""
from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry
type BoschAlarmConfigEntry = ConfigEntry[Panel]

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)

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

View File

@@ -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(),
}

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)}",
)

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."""

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,

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(

View File

@@ -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": {

View File

@@ -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:

View File

@@ -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)
),
),
)

View File

@@ -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": {

View File

@@ -55,11 +55,13 @@ from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
deprecated_function,
dir_with_deprecated_constants,
)
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
@@ -84,15 +86,18 @@ from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCLegacyProvider,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCAnswer,
WebRTCCandidate, # noqa: F401
WebRTCClientConfiguration,
WebRTCError, # noqa: F401
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,
)
@@ -431,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",
@@ -450,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
@@ -472,10 +480,24 @@ 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
)
self._supports_native_async_webrtc = (
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:
@@ -537,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."""
@@ -575,6 +631,15 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
return None
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Handle the WebRTC offer and return an answer.
This is used by cameras with CameraEntityFeature.STREAM
and StreamType.WEB_RTC.
Integrations can override with a native WebRTC implementation.
"""
async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
@@ -587,13 +652,56 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation.
"""
if self._supports_native_sync_webrtc:
try:
answer = await deprecated_function(
"async_handle_async_webrtc_offer",
breaks_in_ha_version="2025.6",
)(self.async_handle_web_rtc_offer)(offer_sdp)
except ValueError as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
send_message(
WebRTCError(
"webrtc_offer_failed",
str(ex),
)
)
except TimeoutError:
# This catch was already here and should stay through the deprecation
_LOGGER.error("Timeout handling WebRTC offer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"Timeout handling WebRTC offer",
)
)
else:
if answer:
send_message(WebRTCAnswer(answer))
else:
_LOGGER.error("Error handling WebRTC offer: No answer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"No answer on WebRTC offer",
)
)
return
if self._webrtc_provider:
await self._webrtc_provider.async_handle_async_webrtc_offer(
self, offer_sdp, session_id, send_message
)
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
@@ -689,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
@@ -712,17 +823,28 @@ 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 self._supports_native_async_webrtc:
if not (
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
):
# Camera doesn't have a native WebRTC implementation
new_provider = await self._async_get_supported_webrtc_provider(
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()
@@ -747,12 +869,20 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration()
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
if not self._supports_native_sync_webrtc:
# Until 2024.11, the frontend was not resolving any ice servers
# The async approach was added 2024.11 and new integrations need to use it
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = (
self._supports_native_sync_webrtc
or self._legacy_webrtc_provider is not None
)
return config
@@ -782,13 +912,13 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_async_webrtc:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
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)

View File

@@ -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": {

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"
)
@@ -111,11 +115,13 @@ class WebRTCClientConfiguration:
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
data_channel: str | None = None
get_candidates_upfront: bool = False
def to_frontend_dict(self) -> dict[str, Any]:
"""Return a dict that can be used by the frontend."""
data: dict[str, Any] = {
"configuration": self.configuration.to_dict(),
"getCandidatesUpfront": self.get_candidates_upfront,
}
if self.data_channel is not None:
data["dataChannel"] = self.data_channel
@@ -157,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,
@@ -186,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)
@@ -360,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,
@@ -376,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,
},
)

View File

@@ -60,7 +60,7 @@ from .const import (
ADDED_CAST_DEVICES_KEY,
CAST_MULTIZONE_MANAGER_KEY,
CONF_IGNORE_CEC,
DOMAIN,
DOMAIN as CAST_DOMAIN,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
@@ -315,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self._cast_view_remove_handler: CALLBACK_TYPE | None = None
self._attr_unique_id = str(cast_info.uuid)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(cast_info.uuid).replace("-", ""))},
identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))},
manufacturer=str(cast_info.cast_info.manufacturer),
model=cast_info.cast_info.model_name,
name=str(cast_info.friendly_name),
@@ -591,7 +591,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
"""Generate root node."""
children = []
# Add media browsers
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
@@ -650,7 +650,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
platform: CastProtocol
assert media_content_type is not None
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
@@ -680,7 +680,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
# Handle media supported by a known cast app
if media_type == DOMAIN:
if media_type == CAST_DOMAIN:
try:
app_data = json.loads(media_id)
if metadata := extra.get("metadata"):
@@ -712,7 +712,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return
# Try the cast platforms
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
result = await platform.async_play_media(
self.hass, self.entity_id, chromecast, media_type, media_id
)

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": {

View File

@@ -18,20 +18,23 @@ from homeassistant.const import (
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_suggest_report_issue
from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import ( # noqa: F401
ATTR_AUX_HEAT,
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
@@ -74,6 +77,7 @@ from .const import ( # noqa: F401
PRESET_HOME,
PRESET_NONE,
PRESET_SLEEP,
SERVICE_SET_AUX_HEAT,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE,
@@ -164,6 +168,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"async_handle_set_preset_mode_service",
[ClimateEntityFeature.PRESET_MODE],
)
component.async_register_entity_service(
SERVICE_SET_AUX_HEAT,
{vol.Required(ATTR_AUX_HEAT): cv.boolean},
async_service_aux_heat,
[ClimateEntityFeature.AUX_HEAT],
)
component.async_register_entity_service(
SERVICE_SET_TEMPERATURE,
SET_TEMPERATURE_SCHEMA,
@@ -229,6 +239,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"target_temperature_low",
"preset_mode",
"preset_modes",
"is_aux_heat",
"fan_mode",
"fan_modes",
"swing_mode",
@@ -268,6 +279,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_hvac_action: HVACAction | None = None
_attr_hvac_mode: HVACMode | None
_attr_hvac_modes: list[HVACMode]
_attr_is_aux_heat: bool | None
_attr_max_humidity: float = DEFAULT_MAX_HUMIDITY
_attr_max_temp: float
_attr_min_humidity: float = DEFAULT_MIN_HUMIDITY
@@ -287,6 +299,52 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_target_temperature: float | None = None
_attr_temperature_unit: str
__climate_reported_legacy_aux = False
def _report_legacy_aux(self) -> None:
"""Log warning and create an issue if the entity implements legacy auxiliary heater."""
report_issue = async_suggest_report_issue(
self.hass,
integration_domain=self.platform.platform_name,
module=type(self).__module__,
)
_LOGGER.warning(
(
"%s::%s implements the `is_aux_heat` property or uses the auxiliary "
"heater methods in a subclass of ClimateEntity which is "
"deprecated and will be unsupported from Home Assistant 2025.4."
" Please %s"
),
self.platform.platform_name,
self.__class__.__name__,
report_issue,
)
translation_placeholders = {"platform": self.platform.platform_name}
translation_key = "deprecated_climate_aux_no_url"
issue_tracker = async_get_issue_tracker(
self.hass,
integration_domain=self.platform.platform_name,
module=type(self).__module__,
)
if issue_tracker:
translation_placeholders["issue_tracker"] = issue_tracker
translation_key = "deprecated_climate_aux_url_custom"
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_climate_aux_{self.platform.platform_name}",
breaks_in_ha_version="2025.4.0",
is_fixable=False,
is_persistent=False,
issue_domain=self.platform.platform_name,
severity=ir.IssueSeverity.WARNING,
translation_key=translation_key,
translation_placeholders=translation_placeholders,
)
self.__climate_reported_legacy_aux = True
@final
@property
def state(self) -> str | None:
@@ -395,6 +453,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
data[ATTR_SWING_HORIZONTAL_MODE] = self.swing_horizontal_mode
if ClimateEntityFeature.AUX_HEAT in supported_features:
data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF
if (
self.__climate_reported_legacy_aux is False
and "custom_components" in type(self).__module__
):
self._report_legacy_aux()
return data
@cached_property
@@ -474,6 +540,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
return self._attr_preset_modes
@cached_property
def is_aux_heat(self) -> bool | None:
"""Return true if aux heater.
Requires ClimateEntityFeature.AUX_HEAT.
"""
return self._attr_is_aux_heat
@cached_property
def fan_mode(self) -> str | None:
"""Return the fan setting.
@@ -658,6 +732,22 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Set new preset mode."""
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
raise NotImplementedError
async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
await self.hass.async_add_executor_job(self.turn_aux_heat_on)
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
raise NotImplementedError
async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
await self.hass.async_add_executor_job(self.turn_aux_heat_off)
def turn_on(self) -> None:
"""Turn the entity on."""
raise NotImplementedError
@@ -755,6 +845,16 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self._attr_max_humidity
async def async_service_aux_heat(
entity: ClimateEntity, service_call: ServiceCall
) -> None:
"""Handle aux heat service."""
if service_call.data[ATTR_AUX_HEAT]:
await entity.async_turn_aux_heat_on()
else:
await entity.async_turn_aux_heat_off()
async def async_service_humidity_set(
entity: ClimateEntity, service_call: ServiceCall
) -> None:

View File

@@ -96,6 +96,7 @@ class HVACAction(StrEnum):
CURRENT_HVAC_ACTIONS = [cls.value for cls in HVACAction]
ATTR_AUX_HEAT = "aux_heat"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_FAN_MODES = "fan_modes"
@@ -127,6 +128,7 @@ DOMAIN = "climate"
INTENT_SET_TEMPERATURE = "HassClimateSetTemperature"
SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_PRESET_MODE = "set_preset_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
@@ -145,6 +147,7 @@ class ClimateEntityFeature(IntFlag):
FAN_MODE = 8
PRESET_MODE = 16
SWING_MODE = 32
AUX_HEAT = 64
TURN_OFF = 128
TURN_ON = 256
SWING_HORIZONTAL_MODE = 512

View File

@@ -1,5 +1,17 @@
# Describes the format for available climate services
set_aux_heat:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.AUX_HEAT
fields:
aux_heat:
required: true
selector:
boolean:
set_preset_mode:
target:
entity:

View File

@@ -12,6 +12,7 @@ from homeassistant.helpers.significant_change import (
)
from . import (
ATTR_AUX_HEAT,
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
@@ -26,6 +27,7 @@ from . import (
)
SIGNIFICANT_ATTRIBUTES: set[str] = {
ATTR_AUX_HEAT,
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
@@ -65,6 +67,7 @@ def async_check_significant_change(
for attr_name in changed_attrs:
if attr_name in [
ATTR_AUX_HEAT,
ATTR_FAN_MODE,
ATTR_HVAC_ACTION,
ATTR_PRESET_MODE,

View File

@@ -36,6 +36,9 @@
"fan_only": "Fan only"
},
"state_attributes": {
"aux_heat": {
"name": "Aux heat"
},
"current_humidity": {
"name": "Current humidity"
},
@@ -146,6 +149,16 @@
}
},
"services": {
"set_aux_heat": {
"name": "Turn on/off auxiliary heater",
"description": "Turns auxiliary heater on/off.",
"fields": {
"aux_heat": {
"name": "Auxiliary heating",
"description": "New value of auxiliary heater."
}
}
},
"set_preset_mode": {
"name": "Set preset mode",
"description": "Sets preset mode.",
@@ -254,6 +267,16 @@
}
}
},
"issues": {
"deprecated_climate_aux_url_custom": {
"title": "The {platform} custom integration is using deprecated climate auxiliary heater",
"description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease create a bug report at {issue_tracker}.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue."
},
"deprecated_climate_aux_no_url": {
"title": "[%key:component::climate::issues::deprecated_climate_aux_url_custom::title%]",
"description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease report it to the author of the {platform} integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue."
}
},
"exceptions": {
"not_valid_preset_mode": {
"message": "Preset mode {mode} is not valid. Valid preset modes are: {modes}."

View File

@@ -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,
}
)

View File

@@ -43,7 +43,7 @@ from homeassistant.util.dt import utcnow
from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DOMAIN,
DOMAIN as CLOUD_DOMAIN,
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_SHOULD_EXPOSE,
@@ -55,7 +55,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_ALEXA = f"{DOMAIN}.{ALEXA_DOMAIN}"
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
# Time to wait when entity preferences have changed before syncing it to
# the cloud.

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