mirror of
https://github.com/home-assistant/core.git
synced 2025-08-02 12:15:08 +02:00
Merge branch 'dev' of https://github.com/hanwg/core into feature/telegram-integration-config-flow
# Conflicts: # homeassistant/components/telegram/notify.py
This commit is contained in:
58
.github/workflows/ci.yaml
vendored
58
.github/workflows/ci.yaml
vendored
@@ -37,9 +37,9 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 12
|
CACHE_VERSION: 1
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 9
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.6"
|
HA_SHORT_VERSION: "2025.6"
|
||||||
DEFAULT_PYTHON: "3.13"
|
DEFAULT_PYTHON: "3.13"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||||
@@ -259,7 +259,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Create Python virtual environment
|
- name: Create Python virtual environment
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
@@ -276,7 +276,7 @@ jobs:
|
|||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Install pre-commit dependencies
|
- name: Install pre-commit dependencies
|
||||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||||
@@ -306,7 +306,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
@@ -315,7 +315,7 @@ jobs:
|
|||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Run ruff-format
|
- name: Run ruff-format
|
||||||
run: |
|
run: |
|
||||||
@@ -346,7 +346,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
@@ -355,7 +355,7 @@ jobs:
|
|||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Run ruff
|
- name: Run ruff
|
||||||
run: |
|
run: |
|
||||||
@@ -386,7 +386,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
@@ -395,7 +395,7 @@ jobs:
|
|||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
|
|
||||||
- name: Register yamllint problem matcher
|
- name: Register yamllint problem matcher
|
||||||
@@ -501,7 +501,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore uv wheel cache
|
- name: Restore uv wheel cache
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
@@ -509,10 +509,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
steps.generate-uv-key.outputs.key }}
|
steps.generate-uv-key.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
|
||||||
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
|
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
|
||||||
env.HA_SHORT_VERSION }}-
|
env.HA_SHORT_VERSION }}-
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
@@ -598,7 +598,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Run hassfest
|
- name: Run hassfest
|
||||||
run: |
|
run: |
|
||||||
@@ -631,7 +631,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Run gen_requirements_all.py
|
- name: Run gen_requirements_all.py
|
||||||
run: |
|
run: |
|
||||||
@@ -653,7 +653,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Dependency review
|
- name: Dependency review
|
||||||
uses: actions/dependency-review-action@v4.6.0
|
uses: actions/dependency-review-action@v4.7.0
|
||||||
with:
|
with:
|
||||||
license-check: false # We use our own license audit checks
|
license-check: false # We use our own license audit checks
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Extract license data
|
- name: Extract license data
|
||||||
run: |
|
run: |
|
||||||
@@ -731,7 +731,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Register pylint problem matcher
|
- name: Register pylint problem matcher
|
||||||
run: |
|
run: |
|
||||||
@@ -778,7 +778,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Register pylint problem matcher
|
- name: Register pylint problem matcher
|
||||||
run: |
|
run: |
|
||||||
@@ -830,17 +830,17 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore mypy cache
|
- name: Restore mypy cache
|
||||||
uses: actions/cache@v4.2.3
|
uses: actions/cache@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
steps.generate-mypy-key.outputs.key }}
|
steps.generate-mypy-key.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-mypy-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-mypy-${{
|
||||||
env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{
|
env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{
|
||||||
env.HA_SHORT_VERSION }}-
|
env.HA_SHORT_VERSION }}-
|
||||||
- name: Register mypy problem matcher
|
- name: Register mypy problem matcher
|
||||||
@@ -900,7 +900,7 @@ jobs:
|
|||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Run split_tests.py
|
- name: Run split_tests.py
|
||||||
run: |
|
run: |
|
||||||
@@ -959,7 +959,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
key: >-
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Register Python problem matcher
|
- name: Register Python problem matcher
|
||||||
run: |
|
run: |
|
||||||
@@ -1084,7 +1085,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
key: >-
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Register Python problem matcher
|
- name: Register Python problem matcher
|
||||||
run: |
|
run: |
|
||||||
@@ -1218,7 +1220,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
key: >-
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Register Python problem matcher
|
- name: Register Python problem matcher
|
||||||
run: |
|
run: |
|
||||||
@@ -1369,7 +1372,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
key: >-
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Register Python problem matcher
|
- name: Register Python problem matcher
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.28.16
|
uses: github/codeql-action/init@v3.28.17
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.28.16
|
uses: github/codeql-action/analyze@v3.28.17
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
@@ -332,6 +332,7 @@ homeassistant.components.media_player.*
|
|||||||
homeassistant.components.media_source.*
|
homeassistant.components.media_source.*
|
||||||
homeassistant.components.met_eireann.*
|
homeassistant.components.met_eireann.*
|
||||||
homeassistant.components.metoffice.*
|
homeassistant.components.metoffice.*
|
||||||
|
homeassistant.components.miele.*
|
||||||
homeassistant.components.mikrotik.*
|
homeassistant.components.mikrotik.*
|
||||||
homeassistant.components.min_max.*
|
homeassistant.components.min_max.*
|
||||||
homeassistant.components.minecraft_server.*
|
homeassistant.components.minecraft_server.*
|
||||||
@@ -433,7 +434,6 @@ homeassistant.components.roku.*
|
|||||||
homeassistant.components.romy.*
|
homeassistant.components.romy.*
|
||||||
homeassistant.components.rpi_power.*
|
homeassistant.components.rpi_power.*
|
||||||
homeassistant.components.rss_feed_template.*
|
homeassistant.components.rss_feed_template.*
|
||||||
homeassistant.components.rtsp_to_webrtc.*
|
|
||||||
homeassistant.components.russound_rio.*
|
homeassistant.components.russound_rio.*
|
||||||
homeassistant.components.ruuvi_gateway.*
|
homeassistant.components.ruuvi_gateway.*
|
||||||
homeassistant.components.ruuvitag_ble.*
|
homeassistant.components.ruuvitag_ble.*
|
||||||
|
24
CODEOWNERS
generated
24
CODEOWNERS
generated
@@ -46,8 +46,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/accuweather/ @bieniu
|
/tests/components/accuweather/ @bieniu
|
||||||
/homeassistant/components/acmeda/ @atmurray
|
/homeassistant/components/acmeda/ @atmurray
|
||||||
/tests/components/acmeda/ @atmurray
|
/tests/components/acmeda/ @atmurray
|
||||||
/homeassistant/components/adax/ @danielhiversen
|
/homeassistant/components/adax/ @danielhiversen @lazytarget
|
||||||
/tests/components/adax/ @danielhiversen
|
/tests/components/adax/ @danielhiversen @lazytarget
|
||||||
/homeassistant/components/adguard/ @frenck
|
/homeassistant/components/adguard/ @frenck
|
||||||
/tests/components/adguard/ @frenck
|
/tests/components/adguard/ @frenck
|
||||||
/homeassistant/components/ads/ @mrpasztoradam
|
/homeassistant/components/ads/ @mrpasztoradam
|
||||||
@@ -171,6 +171,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/avea/ @pattyland
|
/homeassistant/components/avea/ @pattyland
|
||||||
/homeassistant/components/awair/ @ahayworth @danielsjf
|
/homeassistant/components/awair/ @ahayworth @danielsjf
|
||||||
/tests/components/awair/ @ahayworth @danielsjf
|
/tests/components/awair/ @ahayworth @danielsjf
|
||||||
|
/homeassistant/components/aws_s3/ @tomasbedrich
|
||||||
|
/tests/components/aws_s3/ @tomasbedrich
|
||||||
/homeassistant/components/axis/ @Kane610
|
/homeassistant/components/axis/ @Kane610
|
||||||
/tests/components/axis/ @Kane610
|
/tests/components/axis/ @Kane610
|
||||||
/homeassistant/components/azure_data_explorer/ @kaareseras
|
/homeassistant/components/azure_data_explorer/ @kaareseras
|
||||||
@@ -453,8 +455,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/evil_genius_labs/ @balloob
|
/tests/components/evil_genius_labs/ @balloob
|
||||||
/homeassistant/components/evohome/ @zxdavb
|
/homeassistant/components/evohome/ @zxdavb
|
||||||
/tests/components/evohome/ @zxdavb
|
/tests/components/evohome/ @zxdavb
|
||||||
/homeassistant/components/ezviz/ @RenierM26 @baqs
|
/homeassistant/components/ezviz/ @RenierM26
|
||||||
/tests/components/ezviz/ @RenierM26 @baqs
|
/tests/components/ezviz/ @RenierM26
|
||||||
/homeassistant/components/faa_delays/ @ntilley905
|
/homeassistant/components/faa_delays/ @ntilley905
|
||||||
/tests/components/faa_delays/ @ntilley905
|
/tests/components/faa_delays/ @ntilley905
|
||||||
/homeassistant/components/fan/ @home-assistant/core
|
/homeassistant/components/fan/ @home-assistant/core
|
||||||
@@ -1109,8 +1111,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/opentherm_gw/ @mvn23
|
/tests/components/opentherm_gw/ @mvn23
|
||||||
/homeassistant/components/openuv/ @bachya
|
/homeassistant/components/openuv/ @bachya
|
||||||
/tests/components/openuv/ @bachya
|
/tests/components/openuv/ @bachya
|
||||||
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi
|
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
|
||||||
/tests/components/openweathermap/ @fabaff @freekode @nzapponi
|
/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
|
||||||
/homeassistant/components/opnsense/ @mtreinish
|
/homeassistant/components/opnsense/ @mtreinish
|
||||||
/tests/components/opnsense/ @mtreinish
|
/tests/components/opnsense/ @mtreinish
|
||||||
/homeassistant/components/opower/ @tronikos
|
/homeassistant/components/opower/ @tronikos
|
||||||
@@ -1305,8 +1307,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/rpi_power/ @shenxn @swetoast
|
/tests/components/rpi_power/ @shenxn @swetoast
|
||||||
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
||||||
/tests/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
|
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||||
/homeassistant/components/russound_rio/ @noahhusby
|
/homeassistant/components/russound_rio/ @noahhusby
|
||||||
@@ -1318,8 +1318,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ruuvitag_ble/ @akx
|
/tests/components/ruuvitag_ble/ @akx
|
||||||
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
|
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||||
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
|
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||||
/homeassistant/components/s3/ @tomasbedrich
|
|
||||||
/tests/components/s3/ @tomasbedrich
|
|
||||||
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
||||||
/tests/components/sabnzbd/ @shaiu @jpbede
|
/tests/components/sabnzbd/ @shaiu @jpbede
|
||||||
/homeassistant/components/saj/ @fredericvl
|
/homeassistant/components/saj/ @fredericvl
|
||||||
@@ -1678,8 +1676,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
||||||
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
||||||
/homeassistant/components/voip/ @balloob @synesthesiam
|
/homeassistant/components/voip/ @balloob @synesthesiam @jaminh
|
||||||
/tests/components/voip/ @balloob @synesthesiam
|
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||||
/homeassistant/components/volumio/ @OnFreund
|
/homeassistant/components/volumio/ @OnFreund
|
||||||
/tests/components/volumio/ @OnFreund
|
/tests/components/volumio/ @OnFreund
|
||||||
/homeassistant/components/volvooncall/ @molobrakos
|
/homeassistant/components/volvooncall/ @molobrakos
|
||||||
@@ -1796,6 +1794,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/zeversolar/ @kvanzuijlen
|
/tests/components/zeversolar/ @kvanzuijlen
|
||||||
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||||
/tests/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
|
/homeassistant/components/zodiac/ @JulienTant
|
||||||
/tests/components/zodiac/ @JulienTant
|
/tests/components/zodiac/ @JulienTant
|
||||||
/homeassistant/components/zone/ @home-assistant/core
|
/homeassistant/components/zone/ @home-assistant/core
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
{
|
{
|
||||||
"domain": "amazon",
|
"domain": "amazon",
|
||||||
"name": "Amazon",
|
"name": "Amazon",
|
||||||
"integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"]
|
"integrations": [
|
||||||
|
"alexa",
|
||||||
|
"amazon_polly",
|
||||||
|
"aws",
|
||||||
|
"aws_s3",
|
||||||
|
"fire_tv",
|
||||||
|
"route53"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
|
|||||||
2: "moderate",
|
2: "moderate",
|
||||||
3: "high",
|
3: "high",
|
||||||
4: "very_high",
|
4: "very_high",
|
||||||
|
5: "extreme",
|
||||||
}
|
}
|
||||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||||
|
@@ -72,6 +72,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "Level",
|
"name": "Level",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "Extreme",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "Moderate",
|
"moderate": "Moderate",
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@@ -123,6 +125,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@@ -167,6 +170,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@@ -181,6 +185,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@@ -195,6 +200,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "adax",
|
"domain": "adax",
|
||||||
"name": "Adax",
|
"name": "Adax",
|
||||||
"codeowners": ["@danielhiversen"],
|
"codeowners": ["@danielhiversen", "@lazytarget"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
@@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import AdvantageAirDataConfigEntry
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN
|
||||||
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
|
|||||||
self._id: str = light["id"]
|
self._id: str = light["id"]
|
||||||
self._attr_unique_id += f"-{self._id}"
|
self._attr_unique_id += f"-{self._id}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)},
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||||
via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]),
|
via_device=(DOMAIN, self.coordinator.data["system"]["rid"]),
|
||||||
manufacturer="Advantage Air",
|
manufacturer="Advantage Air",
|
||||||
model=light.get("moduleType"),
|
model=light.get("moduleType"),
|
||||||
name=light["name"],
|
name=light["name"],
|
||||||
|
@@ -6,7 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import AdvantageAirDataConfigEntry
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import AdvantageAirEntity
|
from .entity import AdvantageAirEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
@@ -32,9 +32,7 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
|
|||||||
"""Initialize the Advantage Air App."""
|
"""Initialize the Advantage Air App."""
|
||||||
super().__init__(instance)
|
super().__init__(instance)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={
|
identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])},
|
||||||
(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"])
|
|
||||||
},
|
|
||||||
manufacturer="Advantage Air",
|
manufacturer="Advantage Air",
|
||||||
model=self.coordinator.data["system"]["sysType"],
|
model=self.coordinator.data["system"]["sysType"],
|
||||||
name=self.coordinator.data["system"]["name"],
|
name=self.coordinator.data["system"]["name"],
|
||||||
|
@@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL
|
from .const import DOMAIN, SERVER_URL
|
||||||
|
|
||||||
ATTRIBUTION = "ispyconnect.com"
|
ATTRIBUTION = "ispyconnect.com"
|
||||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||||
@@ -46,7 +46,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=config_entry.entry_id,
|
config_entry_id=config_entry.entry_id,
|
||||||
identifiers={(AGENT_DOMAIN, agent_client.unique)},
|
identifiers={(DOMAIN, agent_client.unique)},
|
||||||
manufacturer="iSpyConnect",
|
manufacturer="iSpyConnect",
|
||||||
name=f"Agent {agent_client.name}",
|
name=f"Agent {agent_client.name}",
|
||||||
model="Agent DVR",
|
model="Agent DVR",
|
||||||
|
@@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import AgentDVRConfigEntry
|
from . import AgentDVRConfigEntry
|
||||||
from .const import DOMAIN as AGENT_DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
CONF_HOME_MODE_NAME = "home"
|
CONF_HOME_MODE_NAME = "home"
|
||||||
CONF_AWAY_MODE_NAME = "away"
|
CONF_AWAY_MODE_NAME = "away"
|
||||||
@@ -47,7 +47,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
|||||||
self._client = client
|
self._client = client
|
||||||
self._attr_unique_id = f"{client.unique}_CP"
|
self._attr_unique_id = f"{client.unique}_CP"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(AGENT_DOMAIN, client.unique)},
|
identifiers={(DOMAIN, client.unique)},
|
||||||
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
|
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
|
||||||
manufacturer="Agent",
|
manufacturer="Agent",
|
||||||
model=CONST_ALARM_CONTROL_PANEL_NAME,
|
model=CONST_ALARM_CONTROL_PANEL_NAME,
|
||||||
|
@@ -3,6 +3,19 @@
|
|||||||
"name": "Airthings",
|
"name": "Airthings",
|
||||||
"codeowners": ["@danielhiversen", "@LaStrada"],
|
"codeowners": ["@danielhiversen", "@LaStrada"],
|
||||||
"config_flow": true,
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["airthings"],
|
"loggers": ["airthings"],
|
||||||
|
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
|||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
LIGHT_LUX,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS,
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
@@ -78,6 +79,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
|||||||
translation_key="light",
|
translation_key="light",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
"lux": SensorEntityDescription(
|
||||||
|
key="lux",
|
||||||
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
|
native_unit_of_measurement=LIGHT_LUX,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
"virusRisk": SensorEntityDescription(
|
"virusRisk": SensorEntityDescription(
|
||||||
key="virusRisk",
|
key="virusRisk",
|
||||||
translation_key="virus_risk",
|
translation_key="virus_risk",
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"validation": {
|
"validation": {
|
||||||
"title": "Two factor authentication",
|
"title": "Two-factor authentication",
|
||||||
"data": {
|
"data": {
|
||||||
"verification_code": "Verification code"
|
"verification_code": "Verification code"
|
||||||
},
|
},
|
||||||
|
@@ -4,8 +4,8 @@
|
|||||||
"user": {
|
"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",
|
"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": {
|
"data": {
|
||||||
"port": "RS485 or USB-RS485 Adaptor Port",
|
"port": "RS485 or USB-RS485 adaptor port",
|
||||||
"address": "Inverter Address"
|
"address": "Inverter address"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"no_serial_ports": "No com ports found. Need a valid RS485 device to communicate."
|
"no_serial_ports": "No com ports found. The integration needs a valid RS485 device to communicate."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Set up two-factor authentication using TOTP",
|
"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": {
|
"error": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
"title": "Notify One-Time Password",
|
"title": "Notify one-time password",
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Set up one-time password delivered by notify component",
|
"title": "Set up one-time password delivered by notify component",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""The S3 integration."""
|
"""The AWS S3 integration."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
"""Backup platform for the S3 integration."""
|
"""Backup platform for the AWS S3 integration."""
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||||
import functools
|
import functools
|
@@ -1,8 +1,9 @@
|
|||||||
"""Config flow for the S3 integration."""
|
"""Config flow for the AWS S3 integration."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from aiobotocore.session import AioSession
|
from aiobotocore.session import AioSession
|
||||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||||
@@ -17,6 +18,7 @@ from homeassistant.helpers.selector import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
AWS_DOMAIN,
|
||||||
CONF_ACCESS_KEY_ID,
|
CONF_ACCESS_KEY_ID,
|
||||||
CONF_BUCKET,
|
CONF_BUCKET,
|
||||||
CONF_ENDPOINT_URL,
|
CONF_ENDPOINT_URL,
|
||||||
@@ -57,6 +59,12 @@ class S3ConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL],
|
CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not urlparse(user_input[CONF_ENDPOINT_URL]).hostname.endswith(
|
||||||
|
AWS_DOMAIN
|
||||||
|
):
|
||||||
|
errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url"
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
session = AioSession()
|
session = AioSession()
|
||||||
async with session.create_client(
|
async with session.create_client(
|
@@ -1,18 +1,19 @@
|
|||||||
"""Constants for the S3 integration."""
|
"""Constants for the AWS S3 integration."""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
DOMAIN: Final = "s3"
|
DOMAIN: Final = "aws_s3"
|
||||||
|
|
||||||
CONF_ACCESS_KEY_ID = "access_key_id"
|
CONF_ACCESS_KEY_ID = "access_key_id"
|
||||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||||
CONF_ENDPOINT_URL = "endpoint_url"
|
CONF_ENDPOINT_URL = "endpoint_url"
|
||||||
CONF_BUCKET = "bucket"
|
CONF_BUCKET = "bucket"
|
||||||
|
|
||||||
DEFAULT_ENDPOINT_URL = "https://s3.eu-central-1.amazonaws.com/"
|
AWS_DOMAIN = "amazonaws.com"
|
||||||
|
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"
|
||||||
|
|
||||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||||
f"{DOMAIN}.backup_agent_listeners"
|
f"{DOMAIN}.backup_agent_listeners"
|
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"domain": "s3",
|
"domain": "aws_s3",
|
||||||
"name": "S3",
|
"name": "AWS S3",
|
||||||
"codeowners": ["@tomasbedrich"],
|
"codeowners": ["@tomasbedrich"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/s3",
|
"documentation": "https://www.home-assistant.io/integrations/aws_s3",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiobotocore"],
|
"loggers": ["aiobotocore"],
|
@@ -9,19 +9,19 @@
|
|||||||
"endpoint_url": "Endpoint URL"
|
"endpoint_url": "Endpoint URL"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"access_key_id": "Access key ID to connect to S3 API",
|
"access_key_id": "Access key ID to connect to AWS S3 API",
|
||||||
"secret_access_key": "Secret access key to connect to S3 API",
|
"secret_access_key": "Secret access key to connect to AWS S3 API",
|
||||||
"bucket": "Bucket must already exist and be writable by the provided credentials.",
|
"bucket": "Bucket must already exist and be writable by the provided credentials.",
|
||||||
"endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs."
|
"endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs."
|
||||||
},
|
},
|
||||||
"title": "Add S3 bucket"
|
"title": "Add AWS S3 bucket"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:component::s3::exceptions::cannot_connect::message%]",
|
"cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]",
|
||||||
"invalid_bucket_name": "[%key:component::s3::exceptions::invalid_bucket_name::message%]",
|
"invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]",
|
||||||
"invalid_credentials": "[%key:component::s3::exceptions::invalid_credentials::message%]",
|
"invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]",
|
||||||
"invalid_endpoint_url": "Invalid endpoint URL"
|
"invalid_endpoint_url": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL."
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
|
|
||||||
from .const import DOMAIN as AXIS_DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .hub import AxisHub
|
from .hub import AxisHub
|
||||||
@@ -61,7 +61,7 @@ class AxisEntity(Entity):
|
|||||||
self.hub = hub
|
self.hub = hub
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(AXIS_DOMAIN, hub.unique_id)},
|
identifiers={(DOMAIN, hub.unique_id)},
|
||||||
serial_number=hub.unique_id,
|
serial_number=hub.unique_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -202,7 +202,7 @@ class BackupConfig:
|
|||||||
if agent_id not in self.data.agents:
|
if agent_id not in self.data.agents:
|
||||||
old_agent_retention = None
|
old_agent_retention = None
|
||||||
self.data.agents[agent_id] = AgentConfig(
|
self.data.agents[agent_id] = AgentConfig(
|
||||||
protected=agent_config.get("protected", False),
|
protected=agent_config.get("protected", True),
|
||||||
retention=new_agent_retention,
|
retention=new_agent_retention,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@@ -30,6 +30,7 @@ class BackupCoordinatorData:
|
|||||||
"""Class to hold backup data."""
|
"""Class to hold backup data."""
|
||||||
|
|
||||||
backup_manager_state: BackupManagerState
|
backup_manager_state: BackupManagerState
|
||||||
|
last_attempted_automatic_backup: datetime | None
|
||||||
last_successful_automatic_backup: datetime | None
|
last_successful_automatic_backup: datetime | None
|
||||||
next_scheduled_automatic_backup: datetime | None
|
next_scheduled_automatic_backup: datetime | None
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
|||||||
"""Update backup manager data."""
|
"""Update backup manager data."""
|
||||||
return BackupCoordinatorData(
|
return BackupCoordinatorData(
|
||||||
self.backup_manager.state,
|
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.last_completed_automatic_backup,
|
||||||
self.backup_manager.config.data.schedule.next_automatic_backup,
|
self.backup_manager.config.data.schedule.next_automatic_backup,
|
||||||
)
|
)
|
||||||
|
@@ -22,7 +22,7 @@ from . import util
|
|||||||
from .agent import BackupAgent
|
from .agent import BackupAgent
|
||||||
from .const import DATA_MANAGER
|
from .const import DATA_MANAGER
|
||||||
from .manager import BackupManager
|
from .manager import BackupManager
|
||||||
from .models import BackupNotFound
|
from .models import AgentBackup, BackupNotFound
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -85,7 +85,15 @@ class DownloadBackupView(HomeAssistantView):
|
|||||||
request, headers, backup_id, agent_id, agent, manager
|
request, headers, backup_id, agent_id, agent, manager
|
||||||
)
|
)
|
||||||
return await self._send_backup_with_password(
|
return await self._send_backup_with_password(
|
||||||
hass, request, headers, backup_id, agent_id, password, agent, manager
|
hass,
|
||||||
|
backup,
|
||||||
|
request,
|
||||||
|
headers,
|
||||||
|
backup_id,
|
||||||
|
agent_id,
|
||||||
|
password,
|
||||||
|
agent,
|
||||||
|
manager,
|
||||||
)
|
)
|
||||||
except BackupNotFound:
|
except BackupNotFound:
|
||||||
return Response(status=HTTPStatus.NOT_FOUND)
|
return Response(status=HTTPStatus.NOT_FOUND)
|
||||||
@@ -116,6 +124,7 @@ class DownloadBackupView(HomeAssistantView):
|
|||||||
async def _send_backup_with_password(
|
async def _send_backup_with_password(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
backup: AgentBackup,
|
||||||
request: Request,
|
request: Request,
|
||||||
headers: dict[istr, str],
|
headers: dict[istr, str],
|
||||||
backup_id: str,
|
backup_id: str,
|
||||||
@@ -144,7 +153,8 @@ class DownloadBackupView(HomeAssistantView):
|
|||||||
|
|
||||||
stream = util.AsyncIteratorWriter(hass)
|
stream = util.AsyncIteratorWriter(hass)
|
||||||
worker = threading.Thread(
|
worker = threading.Thread(
|
||||||
target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []]
|
target=util.decrypt_backup,
|
||||||
|
args=[backup, reader, stream, password, on_done, 0, []],
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
worker.start()
|
worker.start()
|
||||||
|
@@ -46,6 +46,12 @@ BACKUP_MANAGER_DESCRIPTIONS = (
|
|||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
value_fn=lambda data: data.last_successful_automatic_backup,
|
value_fn=lambda data: data.last_successful_automatic_backup,
|
||||||
),
|
),
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="last_attempted_automatic_backup",
|
||||||
|
translation_key="last_attempted_automatic_backup",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda data: data.last_attempted_automatic_backup,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -37,6 +37,9 @@
|
|||||||
"next_scheduled_automatic_backup": {
|
"next_scheduled_automatic_backup": {
|
||||||
"name": "Next scheduled automatic backup"
|
"name": "Next scheduled automatic backup"
|
||||||
},
|
},
|
||||||
|
"last_attempted_automatic_backup": {
|
||||||
|
"name": "Last attempted automatic backup"
|
||||||
|
},
|
||||||
"last_successful_automatic_backup": {
|
"last_successful_automatic_backup": {
|
||||||
"name": "Last successful automatic backup"
|
"name": "Last successful automatic backup"
|
||||||
}
|
}
|
||||||
|
@@ -295,13 +295,26 @@ def validate_password_stream(
|
|||||||
raise BackupEmpty
|
raise BackupEmpty
|
||||||
|
|
||||||
|
|
||||||
|
def _get_expected_archives(backup: AgentBackup) -> set[str]:
|
||||||
|
"""Get the expected archives in the backup."""
|
||||||
|
expected_archives = set()
|
||||||
|
if backup.homeassistant_included:
|
||||||
|
expected_archives.add("homeassistant")
|
||||||
|
for addon in backup.addons:
|
||||||
|
expected_archives.add(addon.slug)
|
||||||
|
for folder in backup.folders:
|
||||||
|
expected_archives.add(folder.value)
|
||||||
|
return expected_archives
|
||||||
|
|
||||||
|
|
||||||
def decrypt_backup(
|
def decrypt_backup(
|
||||||
|
backup: AgentBackup,
|
||||||
input_stream: IO[bytes],
|
input_stream: IO[bytes],
|
||||||
output_stream: IO[bytes],
|
output_stream: IO[bytes],
|
||||||
password: str | None,
|
password: str | None,
|
||||||
on_done: Callable[[Exception | None], None],
|
on_done: Callable[[Exception | None], None],
|
||||||
minimum_size: int,
|
minimum_size: int,
|
||||||
nonces: list[bytes],
|
nonces: NonceGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Decrypt a backup."""
|
"""Decrypt a backup."""
|
||||||
error: Exception | None = None
|
error: Exception | None = None
|
||||||
@@ -315,10 +328,13 @@ def decrypt_backup(
|
|||||||
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
|
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
|
||||||
) as output_tar,
|
) as output_tar,
|
||||||
):
|
):
|
||||||
_decrypt_backup(input_tar, output_tar, password)
|
_decrypt_backup(backup, input_tar, output_tar, password)
|
||||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||||
LOGGER.warning("Error decrypting backup: %s", err)
|
LOGGER.warning("Error decrypting backup: %s", err)
|
||||||
error = err
|
error = err
|
||||||
|
except Exception as err: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||||
|
error = err
|
||||||
else:
|
else:
|
||||||
# Pad the output stream to the requested minimum size
|
# Pad the output stream to the requested minimum size
|
||||||
padding = max(minimum_size - output_stream.tell(), 0)
|
padding = max(minimum_size - output_stream.tell(), 0)
|
||||||
@@ -333,15 +349,18 @@ def decrypt_backup(
|
|||||||
|
|
||||||
|
|
||||||
def _decrypt_backup(
|
def _decrypt_backup(
|
||||||
|
backup: AgentBackup,
|
||||||
input_tar: tarfile.TarFile,
|
input_tar: tarfile.TarFile,
|
||||||
output_tar: tarfile.TarFile,
|
output_tar: tarfile.TarFile,
|
||||||
password: str | None,
|
password: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Decrypt a backup."""
|
"""Decrypt a backup."""
|
||||||
|
expected_archives = _get_expected_archives(backup)
|
||||||
for obj in input_tar:
|
for obj in input_tar:
|
||||||
# We compare with PurePath to avoid issues with different path separators,
|
# We compare with PurePath to avoid issues with different path separators,
|
||||||
# for example when backup.json is added as "./backup.json"
|
# for example when backup.json is added as "./backup.json"
|
||||||
if PurePath(obj.name) == PurePath("backup.json"):
|
object_path = PurePath(obj.name)
|
||||||
|
if object_path == PurePath("backup.json"):
|
||||||
# Rewrite the backup.json file to indicate that the backup is decrypted
|
# Rewrite the backup.json file to indicate that the backup is decrypted
|
||||||
if not (reader := input_tar.extractfile(obj)):
|
if not (reader := input_tar.extractfile(obj)):
|
||||||
raise DecryptError
|
raise DecryptError
|
||||||
@@ -352,7 +371,13 @@ def _decrypt_backup(
|
|||||||
metadata_obj.size = len(updated_metadata_b)
|
metadata_obj.size = len(updated_metadata_b)
|
||||||
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
|
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
|
||||||
continue
|
continue
|
||||||
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
|
prefix, _, suffix = object_path.name.partition(".")
|
||||||
|
if suffix not in ("tar", "tgz", "tar.gz"):
|
||||||
|
LOGGER.debug("Unknown file %s will not be decrypted", obj.name)
|
||||||
|
output_tar.addfile(obj, input_tar.extractfile(obj))
|
||||||
|
continue
|
||||||
|
if prefix not in expected_archives:
|
||||||
|
LOGGER.debug("Unknown inner tar file %s will not be decrypted", obj.name)
|
||||||
output_tar.addfile(obj, input_tar.extractfile(obj))
|
output_tar.addfile(obj, input_tar.extractfile(obj))
|
||||||
continue
|
continue
|
||||||
istf = SecureTarFile(
|
istf = SecureTarFile(
|
||||||
@@ -371,12 +396,13 @@ def _decrypt_backup(
|
|||||||
|
|
||||||
|
|
||||||
def encrypt_backup(
|
def encrypt_backup(
|
||||||
|
backup: AgentBackup,
|
||||||
input_stream: IO[bytes],
|
input_stream: IO[bytes],
|
||||||
output_stream: IO[bytes],
|
output_stream: IO[bytes],
|
||||||
password: str | None,
|
password: str | None,
|
||||||
on_done: Callable[[Exception | None], None],
|
on_done: Callable[[Exception | None], None],
|
||||||
minimum_size: int,
|
minimum_size: int,
|
||||||
nonces: list[bytes],
|
nonces: NonceGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Encrypt a backup."""
|
"""Encrypt a backup."""
|
||||||
error: Exception | None = None
|
error: Exception | None = None
|
||||||
@@ -390,10 +416,13 @@ def encrypt_backup(
|
|||||||
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
|
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
|
||||||
) as output_tar,
|
) as output_tar,
|
||||||
):
|
):
|
||||||
_encrypt_backup(input_tar, output_tar, password, nonces)
|
_encrypt_backup(backup, input_tar, output_tar, password, nonces)
|
||||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||||
LOGGER.warning("Error encrypting backup: %s", err)
|
LOGGER.warning("Error encrypting backup: %s", err)
|
||||||
error = err
|
error = err
|
||||||
|
except Exception as err: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||||
|
error = err
|
||||||
else:
|
else:
|
||||||
# Pad the output stream to the requested minimum size
|
# Pad the output stream to the requested minimum size
|
||||||
padding = max(minimum_size - output_stream.tell(), 0)
|
padding = max(minimum_size - output_stream.tell(), 0)
|
||||||
@@ -408,17 +437,20 @@ def encrypt_backup(
|
|||||||
|
|
||||||
|
|
||||||
def _encrypt_backup(
|
def _encrypt_backup(
|
||||||
|
backup: AgentBackup,
|
||||||
input_tar: tarfile.TarFile,
|
input_tar: tarfile.TarFile,
|
||||||
output_tar: tarfile.TarFile,
|
output_tar: tarfile.TarFile,
|
||||||
password: str | None,
|
password: str | None,
|
||||||
nonces: list[bytes],
|
nonces: NonceGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Encrypt a backup."""
|
"""Encrypt a backup."""
|
||||||
inner_tar_idx = 0
|
inner_tar_idx = 0
|
||||||
|
expected_archives = _get_expected_archives(backup)
|
||||||
for obj in input_tar:
|
for obj in input_tar:
|
||||||
# We compare with PurePath to avoid issues with different path separators,
|
# We compare with PurePath to avoid issues with different path separators,
|
||||||
# for example when backup.json is added as "./backup.json"
|
# for example when backup.json is added as "./backup.json"
|
||||||
if PurePath(obj.name) == PurePath("backup.json"):
|
object_path = PurePath(obj.name)
|
||||||
|
if object_path == PurePath("backup.json"):
|
||||||
# Rewrite the backup.json file to indicate that the backup is encrypted
|
# Rewrite the backup.json file to indicate that the backup is encrypted
|
||||||
if not (reader := input_tar.extractfile(obj)):
|
if not (reader := input_tar.extractfile(obj)):
|
||||||
raise EncryptError
|
raise EncryptError
|
||||||
@@ -429,16 +461,21 @@ def _encrypt_backup(
|
|||||||
metadata_obj.size = len(updated_metadata_b)
|
metadata_obj.size = len(updated_metadata_b)
|
||||||
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
|
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
|
||||||
continue
|
continue
|
||||||
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
|
prefix, _, suffix = object_path.name.partition(".")
|
||||||
|
if suffix not in ("tar", "tgz", "tar.gz"):
|
||||||
|
LOGGER.debug("Unknown file %s will not be encrypted", obj.name)
|
||||||
output_tar.addfile(obj, input_tar.extractfile(obj))
|
output_tar.addfile(obj, input_tar.extractfile(obj))
|
||||||
continue
|
continue
|
||||||
|
if prefix not in expected_archives:
|
||||||
|
LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name)
|
||||||
|
continue
|
||||||
istf = SecureTarFile(
|
istf = SecureTarFile(
|
||||||
None, # Not used
|
None, # Not used
|
||||||
gzip=False,
|
gzip=False,
|
||||||
key=password_to_key(password) if password is not None else None,
|
key=password_to_key(password) if password is not None else None,
|
||||||
mode="r",
|
mode="r",
|
||||||
fileobj=input_tar.extractfile(obj),
|
fileobj=input_tar.extractfile(obj),
|
||||||
nonce=nonces[inner_tar_idx],
|
nonce=nonces.get(inner_tar_idx),
|
||||||
)
|
)
|
||||||
inner_tar_idx += 1
|
inner_tar_idx += 1
|
||||||
with istf.encrypt(obj) as encrypted:
|
with istf.encrypt(obj) as encrypted:
|
||||||
@@ -456,17 +493,33 @@ class _CipherWorkerStatus:
|
|||||||
writer: AsyncIteratorWriter
|
writer: AsyncIteratorWriter
|
||||||
|
|
||||||
|
|
||||||
|
class NonceGenerator:
|
||||||
|
"""Generate nonces for encryption."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the generator."""
|
||||||
|
self._nonces: dict[int, bytes] = {}
|
||||||
|
|
||||||
|
def get(self, index: int) -> bytes:
|
||||||
|
"""Get a nonce for the given index."""
|
||||||
|
if index not in self._nonces:
|
||||||
|
# Generate a new nonce for the given index
|
||||||
|
self._nonces[index] = os.urandom(16)
|
||||||
|
return self._nonces[index]
|
||||||
|
|
||||||
|
|
||||||
class _CipherBackupStreamer:
|
class _CipherBackupStreamer:
|
||||||
"""Encrypt or decrypt a backup."""
|
"""Encrypt or decrypt a backup."""
|
||||||
|
|
||||||
_cipher_func: Callable[
|
_cipher_func: Callable[
|
||||||
[
|
[
|
||||||
|
AgentBackup,
|
||||||
IO[bytes],
|
IO[bytes],
|
||||||
IO[bytes],
|
IO[bytes],
|
||||||
str | None,
|
str | None,
|
||||||
Callable[[Exception | None], None],
|
Callable[[Exception | None], None],
|
||||||
int,
|
int,
|
||||||
list[bytes],
|
NonceGenerator,
|
||||||
],
|
],
|
||||||
None,
|
None,
|
||||||
]
|
]
|
||||||
@@ -484,7 +537,7 @@ class _CipherBackupStreamer:
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._open_stream = open_stream
|
self._open_stream = open_stream
|
||||||
self._password = password
|
self._password = password
|
||||||
self._nonces: list[bytes] = []
|
self._nonces = NonceGenerator()
|
||||||
|
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
"""Return the maximum size of the decrypted or encrypted backup."""
|
"""Return the maximum size of the decrypted or encrypted backup."""
|
||||||
@@ -508,7 +561,15 @@ class _CipherBackupStreamer:
|
|||||||
writer = AsyncIteratorWriter(self._hass)
|
writer = AsyncIteratorWriter(self._hass)
|
||||||
worker = threading.Thread(
|
worker = threading.Thread(
|
||||||
target=self._cipher_func,
|
target=self._cipher_func,
|
||||||
args=[reader, writer, self._password, on_done, self.size(), self._nonces],
|
args=[
|
||||||
|
self._backup,
|
||||||
|
reader,
|
||||||
|
writer,
|
||||||
|
self._password,
|
||||||
|
on_done,
|
||||||
|
self.size(),
|
||||||
|
self._nonces,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
worker_status = _CipherWorkerStatus(
|
worker_status = _CipherWorkerStatus(
|
||||||
done=asyncio.Event(), reader=reader, thread=worker, writer=writer
|
done=asyncio.Event(), reader=reader, thread=worker, writer=writer
|
||||||
@@ -538,17 +599,6 @@ class DecryptedBackupStreamer(_CipherBackupStreamer):
|
|||||||
class EncryptedBackupStreamer(_CipherBackupStreamer):
|
class EncryptedBackupStreamer(_CipherBackupStreamer):
|
||||||
"""Encrypt a backup."""
|
"""Encrypt a backup."""
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
backup: AgentBackup,
|
|
||||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
|
||||||
password: str | None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize."""
|
|
||||||
super().__init__(hass, backup, open_stream, password)
|
|
||||||
self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())]
|
|
||||||
|
|
||||||
_cipher_func = staticmethod(encrypt_backup)
|
_cipher_func = staticmethod(encrypt_backup)
|
||||||
|
|
||||||
def backup(self) -> AgentBackup:
|
def backup(self) -> AgentBackup:
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Sign-in with Blink account",
|
"title": "Sign in with Blink account",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"simple_options": {
|
"simple_options": {
|
||||||
"data": {
|
"data": {
|
||||||
"scan_interval": "Scan Interval (seconds)"
|
"scan_interval": "Scan interval (seconds)"
|
||||||
},
|
},
|
||||||
"title": "Blink options",
|
"title": "Blink options",
|
||||||
"description": "Configure Blink integration"
|
"description": "Configure Blink integration"
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
"config_entry_id": {
|
"config_entry_id": {
|
||||||
"name": "Integration ID",
|
"name": "Integration ID",
|
||||||
"description": "The Blink Integration ID."
|
"description": "The Blink integration ID."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,5 +12,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
|
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["bluemaestro-ble==0.4.0"]
|
"requirements": ["bluemaestro-ble==0.4.1"]
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ from .coordinator import (
|
|||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
|
Platform.BUTTON,
|
||||||
Platform.MEDIA_PLAYER,
|
Platform.MEDIA_PLAYER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
128
homeassistant/components/bluesound/button.py
Normal file
128
homeassistant/components/bluesound/button.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
"""Button entities for Bluesound."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from pyblu import Player
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
|
from homeassistant.const import CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import (
|
||||||
|
CONNECTION_NETWORK_MAC,
|
||||||
|
DeviceInfo,
|
||||||
|
format_mac,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import BluesoundCoordinator
|
||||||
|
from .media_player import DEFAULT_PORT
|
||||||
|
from .utils import format_unique_id
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import BluesoundConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BluesoundConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Bluesound entry."""
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
BluesoundButton(
|
||||||
|
config_entry.runtime_data.coordinator,
|
||||||
|
config_entry.runtime_data.player,
|
||||||
|
config_entry.data[CONF_PORT],
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
for description in BUTTON_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class BluesoundButtonEntityDescription(ButtonEntityDescription):
|
||||||
|
"""Description for Bluesound button entities."""
|
||||||
|
|
||||||
|
press_fn: Callable[[Player], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_sleep_timer(player: Player) -> None:
|
||||||
|
"""Clear the sleep timer."""
|
||||||
|
sleep = -1
|
||||||
|
while sleep != 0:
|
||||||
|
sleep = await player.sleep_timer()
|
||||||
|
|
||||||
|
|
||||||
|
async def set_sleep_timer(player: Player) -> None:
|
||||||
|
"""Set the sleep timer."""
|
||||||
|
await player.sleep_timer()
|
||||||
|
|
||||||
|
|
||||||
|
BUTTON_DESCRIPTIONS = [
|
||||||
|
BluesoundButtonEntityDescription(
|
||||||
|
key="set_sleep_timer",
|
||||||
|
translation_key="set_sleep_timer",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
press_fn=set_sleep_timer,
|
||||||
|
),
|
||||||
|
BluesoundButtonEntityDescription(
|
||||||
|
key="clear_sleep_timer",
|
||||||
|
translation_key="clear_sleep_timer",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
press_fn=clear_sleep_timer,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
|
||||||
|
"""Base class for Bluesound buttons."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
entity_description: BluesoundButtonEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BluesoundCoordinator,
|
||||||
|
player: Player,
|
||||||
|
port: int,
|
||||||
|
description: BluesoundButtonEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Bluesound button."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
sync_status = coordinator.data.sync_status
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._player = player
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{description.key}-{format_unique_id(sync_status.mac, port)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if port == DEFAULT_PORT:
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, format_mac(sync_status.mac))},
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
|
||||||
|
name=sync_status.name,
|
||||||
|
manufacturer=sync_status.brand,
|
||||||
|
model=sync_status.model_name,
|
||||||
|
model_id=sync_status.model,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))},
|
||||||
|
name=sync_status.name,
|
||||||
|
manufacturer=sync_status.brand,
|
||||||
|
model=sync_status.model_name,
|
||||||
|
model_id=sync_status.model,
|
||||||
|
via_device=(DOMAIN, format_mac(sync_status.mac)),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Handle the button press."""
|
||||||
|
await self.entity_description.press_fn(self._player)
|
@@ -22,7 +22,11 @@ from homeassistant.components.media_player import (
|
|||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
entity_platform,
|
||||||
|
issue_registry as ir,
|
||||||
|
)
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import (
|
||||||
CONNECTION_NETWORK_MAC,
|
CONNECTION_NETWORK_MAC,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
@@ -34,7 +38,7 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util, slugify
|
||||||
|
|
||||||
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
||||||
from .coordinator import BluesoundCoordinator
|
from .coordinator import BluesoundCoordinator
|
||||||
@@ -488,10 +492,36 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
|||||||
|
|
||||||
async def async_increase_timer(self) -> int:
|
async def async_increase_timer(self) -> int:
|
||||||
"""Increase sleep time on player."""
|
"""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()
|
return await self._player.sleep_timer()
|
||||||
|
|
||||||
async def async_clear_timer(self) -> None:
|
async def async_clear_timer(self) -> None:
|
||||||
"""Clear sleep timer on player."""
|
"""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
|
sleep = 1
|
||||||
while sleep > 0:
|
while sleep > 0:
|
||||||
sleep = await self._player.sleep_timer()
|
sleep = await self._player.sleep_timer()
|
||||||
|
@@ -26,6 +26,16 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"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": {
|
"services": {
|
||||||
"join": {
|
"join": {
|
||||||
"name": "Join",
|
"name": "Join",
|
||||||
@@ -71,5 +81,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"set_sleep_timer": {
|
||||||
|
"name": "Set sleep timer"
|
||||||
|
},
|
||||||
|
"clear_sleep_timer": {
|
||||||
|
"name": "Clear sleep timer"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,9 +18,9 @@
|
|||||||
"bleak==0.22.3",
|
"bleak==0.22.3",
|
||||||
"bleak-retry-connector==3.9.0",
|
"bleak-retry-connector==3.9.0",
|
||||||
"bluetooth-adapters==0.21.4",
|
"bluetooth-adapters==0.21.4",
|
||||||
"bluetooth-auto-recovery==1.4.5",
|
"bluetooth-auto-recovery==1.5.1",
|
||||||
"bluetooth-data-tools==1.28.1",
|
"bluetooth-data-tools==1.28.1",
|
||||||
"dbus-fast==2.43.0",
|
"dbus-fast==2.43.0",
|
||||||
"habluetooth==3.45.0"
|
"habluetooth==3.48.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
|
from . import DOMAIN, BMWConfigEntry
|
||||||
from .entity import BMWBaseEntity
|
from .entity import BMWBaseEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -111,7 +111,7 @@ class BMWButton(BMWBaseEntity, ButtonEntity):
|
|||||||
await self.entity_description.remote_function(self.vehicle)
|
await self.entity_description.remote_function(self.vehicle)
|
||||||
except MyBMWAPIError as ex:
|
except MyBMWAPIError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
|
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
|
from . import DOMAIN, BMWConfigEntry
|
||||||
from .coordinator import BMWDataUpdateCoordinator
|
from .coordinator import BMWDataUpdateCoordinator
|
||||||
from .entity import BMWBaseEntity
|
from .entity import BMWBaseEntity
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class BMWLock(BMWBaseEntity, LockEntity):
|
|||||||
self._attr_is_locked = None
|
self._attr_is_locked = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
@@ -95,7 +95,7 @@ class BMWLock(BMWBaseEntity, LockEntity):
|
|||||||
self._attr_is_locked = None
|
self._attr_is_locked = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
|
@@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
|
from . import DOMAIN, BMWConfigEntry
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class BMWNotificationService(BaseNotificationService):
|
|||||||
|
|
||||||
except (vol.Invalid, TypeError, ValueError) as ex:
|
except (vol.Invalid, TypeError, ValueError) as ex:
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_poi",
|
translation_key="invalid_poi",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"poi_exception": str(ex),
|
"poi_exception": str(ex),
|
||||||
@@ -107,7 +107,7 @@ class BMWNotificationService(BaseNotificationService):
|
|||||||
await vehicle.remote_services.trigger_send_poi(poi)
|
await vehicle.remote_services.trigger_send_poi(poi)
|
||||||
except MyBMWAPIError as ex:
|
except MyBMWAPIError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
|
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
|
from . import DOMAIN, BMWConfigEntry
|
||||||
from .coordinator import BMWDataUpdateCoordinator
|
from .coordinator import BMWDataUpdateCoordinator
|
||||||
from .entity import BMWBaseEntity
|
from .entity import BMWBaseEntity
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ class BMWNumber(BMWBaseEntity, NumberEntity):
|
|||||||
await self.entity_description.remote_service(self.vehicle, value)
|
await self.entity_description.remote_service(self.vehicle, value)
|
||||||
except MyBMWAPIError as ex:
|
except MyBMWAPIError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
|
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
|
from . import DOMAIN, BMWConfigEntry
|
||||||
from .coordinator import BMWDataUpdateCoordinator
|
from .coordinator import BMWDataUpdateCoordinator
|
||||||
from .entity import BMWBaseEntity
|
from .entity import BMWBaseEntity
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ class BMWSelect(BMWBaseEntity, SelectEntity):
|
|||||||
await self.entity_description.remote_service(self.vehicle, option)
|
await self.entity_description.remote_service(self.vehicle, option)
|
||||||
except MyBMWAPIError as ex:
|
except MyBMWAPIError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
|
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
|
from . import DOMAIN, BMWConfigEntry
|
||||||
from .coordinator import BMWDataUpdateCoordinator
|
from .coordinator import BMWDataUpdateCoordinator
|
||||||
from .entity import BMWBaseEntity
|
from .entity import BMWBaseEntity
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
|
|||||||
await self.entity_description.remote_service_on(self.vehicle)
|
await self.entity_description.remote_service_on(self.vehicle)
|
||||||
except MyBMWAPIError as ex:
|
except MyBMWAPIError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
@@ -124,7 +124,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
|
|||||||
await self.entity_description.remote_service_off(self.vehicle)
|
await self.entity_description.remote_service_off(self.vehicle)
|
||||||
except MyBMWAPIError as ex:
|
except MyBMWAPIError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=BMW_DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_service_error",
|
translation_key="remote_service_error",
|
||||||
translation_placeholders={"exception": str(ex)},
|
translation_placeholders={"exception": str(ex)},
|
||||||
) from ex
|
) from ex
|
||||||
|
@@ -5,7 +5,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
||||||
from bond_async import Bond, BPUPSubscriptions, start_bpup
|
from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool
|
|||||||
token=token,
|
token=token,
|
||||||
timeout=ClientTimeout(total=_API_TIMEOUT),
|
timeout=ClientTimeout(total=_API_TIMEOUT),
|
||||||
session=async_get_clientsession(hass),
|
session=async_get_clientsession(hass),
|
||||||
|
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||||
)
|
)
|
||||||
hub = BondHub(bond, host)
|
hub = BondHub(bond, host)
|
||||||
try:
|
try:
|
||||||
|
@@ -8,7 +8,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientConnectionError, ClientResponseError
|
from aiohttp import ClientConnectionError, ClientResponseError
|
||||||
from bond_async import Bond
|
from bond_async import Bond, RequestorUUID
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
|
||||||
@@ -34,7 +34,12 @@ TOKEN_SCHEMA = vol.Schema({})
|
|||||||
|
|
||||||
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
|
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
|
||||||
"""Try to fetch the token from the bond device."""
|
"""Try to fetch the token from the bond device."""
|
||||||
bond = Bond(host, "", session=async_get_clientsession(hass))
|
bond = Bond(
|
||||||
|
host,
|
||||||
|
"",
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||||
|
)
|
||||||
response: dict[str, str] = {}
|
response: dict[str, str] = {}
|
||||||
with contextlib.suppress(ClientConnectionError):
|
with contextlib.suppress(ClientConnectionError):
|
||||||
response = await bond.token()
|
response = await bond.token()
|
||||||
@@ -45,7 +50,10 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st
|
|||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
bond = Bond(
|
bond = Bond(
|
||||||
data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
|
data[CONF_HOST],
|
||||||
|
data[CONF_ACCESS_TOKEN],
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
hub = BondHub(bond, data[CONF_HOST])
|
hub = BondHub(bond, data[CONF_HOST])
|
||||||
|
@@ -14,7 +14,11 @@ from homeassistant.helpers import device_registry as dr
|
|||||||
|
|
||||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [
|
||||||
|
Platform.ALARM_CONTROL_PANEL,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
|
]
|
||||||
|
|
||||||
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
||||||
|
|
||||||
|
@@ -86,3 +86,57 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
|
|||||||
self._area.ready_observer.detach(self.schedule_update_ha_state)
|
self._area.ready_observer.detach(self.schedule_update_ha_state)
|
||||||
if self._observe_status:
|
if self._observe_status:
|
||||||
self._area.status_observer.detach(self.schedule_update_ha_state)
|
self._area.status_observer.detach(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
|
||||||
|
class BoschAlarmDoorEntity(BoschAlarmEntity):
|
||||||
|
"""A base entity for area related entities within a bosch alarm panel."""
|
||||||
|
|
||||||
|
def __init__(self, panel: Panel, door_id: int, unique_id: str) -> None:
|
||||||
|
"""Set up a area related entity for a bosch alarm panel."""
|
||||||
|
super().__init__(panel, unique_id)
|
||||||
|
self._door_id = door_id
|
||||||
|
self._door = panel.doors[door_id]
|
||||||
|
self._door_unique_id = f"{unique_id}_door_{door_id}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._door_unique_id)},
|
||||||
|
name=self._door.name,
|
||||||
|
manufacturer="Bosch Security Systems",
|
||||||
|
via_device=(DOMAIN, unique_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Observe state changes."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._door.status_observer.attach(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Stop observing state changes."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._door.status_observer.detach(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
|
||||||
|
class BoschAlarmOutputEntity(BoschAlarmEntity):
|
||||||
|
"""A base entity for area related entities within a bosch alarm panel."""
|
||||||
|
|
||||||
|
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
|
||||||
|
"""Set up a output related entity for a bosch alarm panel."""
|
||||||
|
super().__init__(panel, unique_id)
|
||||||
|
self._output_id = output_id
|
||||||
|
self._output = panel.outputs[output_id]
|
||||||
|
self._output_unique_id = f"{unique_id}_output_{output_id}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._output_unique_id)},
|
||||||
|
name=self._output.name,
|
||||||
|
manufacturer="Bosch Security Systems",
|
||||||
|
via_device=(DOMAIN, unique_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Observe state changes."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._output.status_observer.attach(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Stop observing state changes."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._output.status_observer.detach(self.schedule_update_ha_state)
|
||||||
|
@@ -2,7 +2,27 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"faulting_points": {
|
"faulting_points": {
|
||||||
"default": "mdi:alert-circle-outline"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -54,9 +54,23 @@
|
|||||||
},
|
},
|
||||||
"authentication_failed": {
|
"authentication_failed": {
|
||||||
"message": "Incorrect credentials for panel."
|
"message": "Incorrect credentials for panel."
|
||||||
|
},
|
||||||
|
"incorrect_door_state": {
|
||||||
|
"message": "Door cannot be manipulated while it is being cycled."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"secured": {
|
||||||
|
"name": "Secured"
|
||||||
|
},
|
||||||
|
"cycling": {
|
||||||
|
"name": "Cycling"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"name": "Locked"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"faulting_points": {
|
"faulting_points": {
|
||||||
"name": "Faulting points",
|
"name": "Faulting points",
|
||||||
|
150
homeassistant/components/bosch_alarm/switch.py
Normal file
150
homeassistant/components/bosch_alarm/switch.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""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)
|
@@ -10,7 +10,12 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .coordinator import BringConfigEntry, BringDataUpdateCoordinator
|
from .coordinator import (
|
||||||
|
BringActivityCoordinator,
|
||||||
|
BringConfigEntry,
|
||||||
|
BringCoordinators,
|
||||||
|
BringDataUpdateCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
|
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
|
||||||
|
|
||||||
@@ -26,7 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo
|
|||||||
coordinator = BringDataUpdateCoordinator(hass, entry, bring)
|
coordinator = BringDataUpdateCoordinator(hass, entry, bring)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.runtime_data = coordinator
|
activity_coordinator = BringActivityCoordinator(hass, entry, coordinator)
|
||||||
|
await activity_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.runtime_data = BringCoordinators(coordinator, activity_coordinator)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
@@ -30,7 +30,15 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
|
type BringConfigEntry = ConfigEntry[BringCoordinators]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BringCoordinators:
|
||||||
|
"""Data class holding coordinators."""
|
||||||
|
|
||||||
|
data: BringDataUpdateCoordinator
|
||||||
|
activity: BringActivityCoordinator
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -39,17 +47,28 @@ class BringData(DataClassORJSONMixin):
|
|||||||
|
|
||||||
lst: BringList
|
lst: BringList
|
||||||
content: BringItemsResponse
|
content: BringItemsResponse
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BringActivityData(DataClassORJSONMixin):
|
||||||
|
"""Coordinator data class."""
|
||||||
|
|
||||||
activity: BringActivityResponse
|
activity: BringActivityResponse
|
||||||
users: BringUsersResponse
|
users: BringUsersResponse
|
||||||
|
|
||||||
|
|
||||||
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
class BringBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
"""A Bring Data Update Coordinator."""
|
"""Bring base coordinator."""
|
||||||
|
|
||||||
config_entry: BringConfigEntry
|
config_entry: BringConfigEntry
|
||||||
user_settings: BringUserSettingsResponse
|
|
||||||
lists: list[BringList]
|
lists: list[BringList]
|
||||||
|
|
||||||
|
|
||||||
|
class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
|
||||||
|
"""A Bring Data Update Coordinator."""
|
||||||
|
|
||||||
|
user_settings: BringUserSettingsResponse
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring
|
self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -90,16 +109,19 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
|||||||
current_lists := {lst.listUuid for lst in self.lists}
|
current_lists := {lst.listUuid for lst in self.lists}
|
||||||
):
|
):
|
||||||
self._purge_deleted_lists()
|
self._purge_deleted_lists()
|
||||||
|
new_lists = current_lists - self.previous_lists
|
||||||
self.previous_lists = current_lists
|
self.previous_lists = current_lists
|
||||||
|
|
||||||
list_dict: dict[str, BringData] = {}
|
list_dict: dict[str, BringData] = {}
|
||||||
for lst in self.lists:
|
for lst in self.lists:
|
||||||
if (ctx := set(self.async_contexts())) and lst.listUuid not in ctx:
|
if (
|
||||||
|
(ctx := set(self.async_contexts()))
|
||||||
|
and lst.listUuid not in ctx
|
||||||
|
and lst.listUuid not in new_lists
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
items = await self.bring.get_list(lst.listUuid)
|
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:
|
except BringRequestException as e:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
@@ -111,7 +133,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
|||||||
translation_key="setup_parse_exception",
|
translation_key="setup_parse_exception",
|
||||||
) from e
|
) from e
|
||||||
else:
|
else:
|
||||||
list_dict[lst.listUuid] = BringData(lst, items, activity, users)
|
list_dict[lst.listUuid] = BringData(lst, items)
|
||||||
|
|
||||||
return list_dict
|
return list_dict
|
||||||
|
|
||||||
@@ -156,3 +178,60 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
|||||||
device_reg.async_update_device(
|
device_reg.async_update_device(
|
||||||
device.id, remove_config_entry_id=self.config_entry.entry_id
|
device.id, remove_config_entry_id=self.config_entry.entry_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]]):
|
||||||
|
"""A Bring Activity Data Update Coordinator."""
|
||||||
|
|
||||||
|
user_settings: BringUserSettingsResponse
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BringConfigEntry,
|
||||||
|
coordinator: BringDataUpdateCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Bring Activity data coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=10),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self.lists = coordinator.lists
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, BringActivityData]:
|
||||||
|
"""Fetch activity data from bring."""
|
||||||
|
|
||||||
|
list_dict: dict[str, BringActivityData] = {}
|
||||||
|
for lst in self.lists:
|
||||||
|
if (
|
||||||
|
ctx := set(self.coordinator.async_contexts())
|
||||||
|
) and lst.listUuid not in ctx:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
activity = await self.coordinator.bring.get_activity(lst.listUuid)
|
||||||
|
users = await self.coordinator.bring.get_list_users(lst.listUuid)
|
||||||
|
except BringAuthException as e:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="setup_authentication_exception",
|
||||||
|
translation_placeholders={CONF_EMAIL: self.coordinator.bring.mail},
|
||||||
|
) from e
|
||||||
|
except BringRequestException as e:
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="setup_request_exception",
|
||||||
|
) from e
|
||||||
|
except BringParseException as e:
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="setup_parse_exception",
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
list_dict[lst.listUuid] = BringActivityData(activity, users)
|
||||||
|
|
||||||
|
return list_dict
|
||||||
|
@@ -20,9 +20,12 @@ async def async_get_config_entry_diagnostics(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"data": {
|
"data": {
|
||||||
k: async_redact_data(v.to_dict(), TO_REDACT)
|
k: v.to_dict() for k, v in config_entry.runtime_data.data.data.items()
|
||||||
for k, v in config_entry.runtime_data.data.items()
|
|
||||||
},
|
},
|
||||||
"lists": [lst.to_dict() for lst in config_entry.runtime_data.lists],
|
"activity": {
|
||||||
"user_settings": config_entry.runtime_data.user_settings.to_dict(),
|
k: async_redact_data(v.to_dict(), TO_REDACT)
|
||||||
|
for k, v in config_entry.runtime_data.activity.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(),
|
||||||
}
|
}
|
||||||
|
@@ -8,17 +8,17 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import BringDataUpdateCoordinator
|
from .coordinator import BringBaseCoordinator
|
||||||
|
|
||||||
|
|
||||||
class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]):
|
class BringBaseEntity(CoordinatorEntity[BringBaseCoordinator]):
|
||||||
"""Bring base entity."""
|
"""Bring base entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: BringDataUpdateCoordinator,
|
coordinator: BringBaseCoordinator,
|
||||||
bring_list: BringList,
|
bring_list: BringList,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
@@ -34,5 +34,7 @@ class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]):
|
|||||||
},
|
},
|
||||||
manufacturer="Bring! Labs AG",
|
manufacturer="Bring! Labs AG",
|
||||||
model="Bring! Grocery Shopping List",
|
model="Bring! Grocery Shopping List",
|
||||||
configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_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,
|
||||||
)
|
)
|
||||||
|
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import BringConfigEntry
|
from . import BringConfigEntry
|
||||||
from .coordinator import BringDataUpdateCoordinator
|
from .coordinator import BringActivityCoordinator
|
||||||
from .entity import BringBaseEntity
|
from .entity import BringBaseEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -32,18 +32,18 @@ async def async_setup_entry(
|
|||||||
"""Add event entities."""
|
"""Add event entities."""
|
||||||
nonlocal lists_added
|
nonlocal lists_added
|
||||||
|
|
||||||
if new_lists := {lst.listUuid for lst in coordinator.lists} - lists_added:
|
if new_lists := {lst.listUuid for lst in coordinator.data.lists} - lists_added:
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
BringEventEntity(
|
BringEventEntity(
|
||||||
coordinator,
|
coordinator.activity,
|
||||||
bring_list,
|
bring_list,
|
||||||
)
|
)
|
||||||
for bring_list in coordinator.lists
|
for bring_list in coordinator.data.lists
|
||||||
if bring_list.listUuid in new_lists
|
if bring_list.listUuid in new_lists
|
||||||
)
|
)
|
||||||
lists_added |= new_lists
|
lists_added |= new_lists
|
||||||
|
|
||||||
coordinator.async_add_listener(add_entities)
|
coordinator.activity.async_add_listener(add_entities)
|
||||||
add_entities()
|
add_entities()
|
||||||
|
|
||||||
|
|
||||||
@@ -51,10 +51,11 @@ class BringEventEntity(BringBaseEntity, EventEntity):
|
|||||||
"""An event entity."""
|
"""An event entity."""
|
||||||
|
|
||||||
_attr_translation_key = "activities"
|
_attr_translation_key = "activities"
|
||||||
|
coordinator: BringActivityCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: BringDataUpdateCoordinator,
|
coordinator: BringActivityCoordinator,
|
||||||
bring_list: BringList,
|
bring_list: BringList,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
|
@@ -88,7 +88,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the sensor platform."""
|
"""Set up the sensor platform."""
|
||||||
coordinator = config_entry.runtime_data
|
coordinator = config_entry.runtime_data.data
|
||||||
lists_added: set[str] = set()
|
lists_added: set[str] = set()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -117,6 +117,7 @@ class BringSensorEntity(BringBaseEntity, SensorEntity):
|
|||||||
"""A sensor entity."""
|
"""A sensor entity."""
|
||||||
|
|
||||||
entity_description: BringSensorEntityDescription
|
entity_description: BringSensorEntityDescription
|
||||||
|
coordinator: BringDataUpdateCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@@ -44,7 +44,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the sensor from a config entry created in the integrations UI."""
|
"""Set up the sensor from a config entry created in the integrations UI."""
|
||||||
coordinator = config_entry.runtime_data
|
coordinator = config_entry.runtime_data.data
|
||||||
lists_added: set[str] = set()
|
lists_added: set[str] = set()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -88,6 +88,7 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity):
|
|||||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||||
)
|
)
|
||||||
|
coordinator: BringDataUpdateCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, coordinator: BringDataUpdateCoordinator, bring_list: BringList
|
self, coordinator: BringDataUpdateCoordinator, bring_list: BringList
|
||||||
|
@@ -11,6 +11,13 @@
|
|||||||
},
|
},
|
||||||
"audio_output": {
|
"audio_output": {
|
||||||
"default": "mdi:audio-input-stereo-minijack"
|
"default": "mdi:audio-input-stereo-minijack"
|
||||||
|
},
|
||||||
|
"control_bus_mode": {
|
||||||
|
"default": "mdi:audio-video-off",
|
||||||
|
"state": {
|
||||||
|
"amplifier": "mdi:speaker",
|
||||||
|
"receiver": "mdi:audio-video"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@@ -11,6 +11,7 @@ from aiostreammagic import (
|
|||||||
StreamMagicClient,
|
StreamMagicClient,
|
||||||
TransportControl,
|
TransportControl,
|
||||||
)
|
)
|
||||||
|
from aiostreammagic.models import ControlBusMode
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
BrowseMedia,
|
BrowseMedia,
|
||||||
@@ -91,6 +92,8 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
|||||||
features = BASE_FEATURES
|
features = BASE_FEATURES
|
||||||
if self.client.state.pre_amp_mode:
|
if self.client.state.pre_amp_mode:
|
||||||
features |= PREAMP_FEATURES
|
features |= PREAMP_FEATURES
|
||||||
|
if self.client.state.control_bus == ControlBusMode.AMPLIFIER:
|
||||||
|
features |= MediaPlayerEntityFeature.VOLUME_STEP
|
||||||
if TransportControl.PLAY_PAUSE in controls:
|
if TransportControl.PLAY_PAUSE in controls:
|
||||||
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
|
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
|
||||||
for control in controls:
|
for control in controls:
|
||||||
|
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from aiostreammagic import StreamMagicClient
|
from aiostreammagic import StreamMagicClient
|
||||||
from aiostreammagic.models import DisplayBrightness
|
from aiostreammagic.models import ControlBusMode, DisplayBrightness
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
@@ -76,6 +76,20 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
|
|||||||
value_fn=_audio_output_value_fn,
|
value_fn=_audio_output_value_fn,
|
||||||
set_value_fn=_audio_output_set_value_fn,
|
set_value_fn=_audio_output_set_value_fn,
|
||||||
),
|
),
|
||||||
|
CambridgeAudioSelectEntityDescription(
|
||||||
|
key="control_bus_mode",
|
||||||
|
translation_key="control_bus_mode",
|
||||||
|
options=[
|
||||||
|
ControlBusMode.AMPLIFIER.value,
|
||||||
|
ControlBusMode.RECEIVER.value,
|
||||||
|
ControlBusMode.OFF.value,
|
||||||
|
],
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda client: client.state.control_bus,
|
||||||
|
set_value_fn=lambda client, value: client.set_control_bus_mode(
|
||||||
|
ControlBusMode(value)
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -46,6 +46,14 @@
|
|||||||
},
|
},
|
||||||
"audio_output": {
|
"audio_output": {
|
||||||
"name": "Audio output"
|
"name": "Audio output"
|
||||||
|
},
|
||||||
|
"control_bus_mode": {
|
||||||
|
"name": "Control Bus mode",
|
||||||
|
"state": {
|
||||||
|
"amplifier": "Amplifier",
|
||||||
|
"receiver": "Receiver",
|
||||||
|
"off": "[%key:common::state::off%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@@ -61,7 +61,6 @@ from homeassistant.helpers.deprecation import (
|
|||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
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.network import get_url
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||||
@@ -86,7 +85,6 @@ from .img_util import scale_jpeg_camera_image
|
|||||||
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
|
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
|
||||||
from .webrtc import (
|
from .webrtc import (
|
||||||
DATA_ICE_SERVERS,
|
DATA_ICE_SERVERS,
|
||||||
CameraWebRTCLegacyProvider,
|
|
||||||
CameraWebRTCProvider,
|
CameraWebRTCProvider,
|
||||||
WebRTCAnswer,
|
WebRTCAnswer,
|
||||||
WebRTCCandidate, # noqa: F401
|
WebRTCCandidate, # noqa: F401
|
||||||
@@ -94,10 +92,8 @@ from .webrtc import (
|
|||||||
WebRTCError,
|
WebRTCError,
|
||||||
WebRTCMessage, # noqa: F401
|
WebRTCMessage, # noqa: F401
|
||||||
WebRTCSendMessage,
|
WebRTCSendMessage,
|
||||||
async_get_supported_legacy_provider,
|
|
||||||
async_get_supported_provider,
|
async_get_supported_provider,
|
||||||
async_register_ice_servers,
|
async_register_ice_servers,
|
||||||
async_register_rtsp_to_web_rtc_provider, # noqa: F401
|
|
||||||
async_register_webrtc_provider, # noqa: F401
|
async_register_webrtc_provider, # noqa: F401
|
||||||
async_register_ws,
|
async_register_ws,
|
||||||
)
|
)
|
||||||
@@ -436,7 +432,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
CACHED_PROPERTIES_WITH_ATTR_ = {
|
CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||||
"brand",
|
"brand",
|
||||||
"frame_interval",
|
"frame_interval",
|
||||||
"frontend_stream_type",
|
|
||||||
"is_on",
|
"is_on",
|
||||||
"is_recording",
|
"is_recording",
|
||||||
"is_streaming",
|
"is_streaming",
|
||||||
@@ -456,8 +451,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
# Entity Properties
|
# Entity Properties
|
||||||
_attr_brand: str | None = None
|
_attr_brand: str | None = None
|
||||||
_attr_frame_interval: float = MIN_STREAM_INTERVAL
|
_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_on: bool = True
|
||||||
_attr_is_recording: bool = False
|
_attr_is_recording: bool = False
|
||||||
_attr_is_streaming: bool = False
|
_attr_is_streaming: bool = False
|
||||||
@@ -480,7 +473,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
self.async_update_token()
|
self.async_update_token()
|
||||||
self._create_stream_lock: asyncio.Lock | None = None
|
self._create_stream_lock: asyncio.Lock | None = None
|
||||||
self._webrtc_provider: CameraWebRTCProvider | None = None
|
self._webrtc_provider: CameraWebRTCProvider | None = None
|
||||||
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
|
|
||||||
self._supports_native_sync_webrtc = (
|
self._supports_native_sync_webrtc = (
|
||||||
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
|
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
|
||||||
)
|
)
|
||||||
@@ -488,16 +480,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
type(self).async_handle_async_webrtc_offer
|
type(self).async_handle_async_webrtc_offer
|
||||||
!= Camera.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
|
@cached_property
|
||||||
def entity_picture(self) -> str:
|
def entity_picture(self) -> str:
|
||||||
@@ -559,40 +541,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
"""Return the interval between frames of the mjpeg stream."""
|
"""Return the interval between frames of the mjpeg stream."""
|
||||||
return self._attr_frame_interval
|
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
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
@@ -694,13 +642,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
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")
|
raise HomeAssistantError("Camera does not support WebRTC")
|
||||||
|
|
||||||
def camera_image(
|
def camera_image(
|
||||||
@@ -797,9 +738,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
if motion_detection_enabled := self.motion_detection_enabled:
|
if motion_detection_enabled := self.motion_detection_enabled:
|
||||||
attrs["motion_detection"] = 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
|
return attrs
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -823,9 +761,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
providers or inputs to the state attributes change.
|
providers or inputs to the state attributes change.
|
||||||
"""
|
"""
|
||||||
old_provider = self._webrtc_provider
|
old_provider = self._webrtc_provider
|
||||||
old_legacy_provider = self._legacy_webrtc_provider
|
|
||||||
new_provider = None
|
new_provider = None
|
||||||
new_legacy_provider = None
|
|
||||||
|
|
||||||
# Skip all providers if the camera has a native WebRTC implementation
|
# Skip all providers if the camera has a native WebRTC implementation
|
||||||
if not (
|
if not (
|
||||||
@@ -836,15 +772,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
async_get_supported_provider
|
async_get_supported_provider
|
||||||
)
|
)
|
||||||
|
|
||||||
if new_provider is None:
|
if old_provider != new_provider:
|
||||||
# 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._webrtc_provider = new_provider
|
||||||
self._legacy_webrtc_provider = new_legacy_provider
|
|
||||||
self._invalidate_camera_capabilities_cache()
|
self._invalidate_camera_capabilities_cache()
|
||||||
if write_state:
|
if write_state:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -879,10 +808,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
]
|
]
|
||||||
config.configuration.ice_servers.extend(ice_servers)
|
config.configuration.ice_servers.extend(ice_servers)
|
||||||
|
|
||||||
config.get_candidates_upfront = (
|
config.get_candidates_upfront = self._supports_native_sync_webrtc
|
||||||
self._supports_native_sync_webrtc
|
|
||||||
or self._legacy_webrtc_provider is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -918,7 +844,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
else:
|
else:
|
||||||
frontend_stream_types.add(StreamType.HLS)
|
frontend_stream_types.add(StreamType.HLS)
|
||||||
|
|
||||||
if self._webrtc_provider or self._legacy_webrtc_provider:
|
if self._webrtc_provider:
|
||||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||||
|
|
||||||
return CameraCapabilities(frontend_stream_types)
|
return CameraCapabilities(frontend_stream_types)
|
||||||
|
@@ -46,10 +46,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"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": {
|
"services": {
|
||||||
|
@@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable
|
|||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from functools import cache, partial, wraps
|
from functools import cache, partial, wraps
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Protocol
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from mashumaro import MissingField
|
from mashumaro import MissingField
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -22,8 +22,7 @@ from webrtc_models import (
|
|||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.deprecation import deprecated_function
|
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
from homeassistant.util.ulid import ulid
|
from homeassistant.util.ulid import ulid
|
||||||
|
|
||||||
@@ -39,9 +38,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
|
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
|
||||||
"camera_webrtc_providers"
|
"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(
|
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
|
||||||
"camera_webrtc_ice_servers"
|
"camera_webrtc_ice_servers"
|
||||||
)
|
)
|
||||||
@@ -163,18 +159,6 @@ class CameraWebRTCProvider(ABC):
|
|||||||
return ## This is an optional method so we need a default here.
|
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
|
@callback
|
||||||
def async_register_webrtc_provider(
|
def async_register_webrtc_provider(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -204,8 +188,6 @@ def async_register_webrtc_provider(
|
|||||||
|
|
||||||
async def _async_refresh_providers(hass: HomeAssistant) -> None:
|
async def _async_refresh_providers(hass: HomeAssistant) -> None:
|
||||||
"""Check all cameras for any state changes for registered providers."""
|
"""Check all cameras for any state changes for registered providers."""
|
||||||
_async_check_conflicting_legacy_provider(hass)
|
|
||||||
|
|
||||||
component = hass.data[DATA_COMPONENT]
|
component = hass.data[DATA_COMPONENT]
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*(camera.async_refresh_providers() for camera in component.entities)
|
*(camera.async_refresh_providers() for camera in component.entities)
|
||||||
@@ -380,21 +362,6 @@ async def async_get_supported_provider(
|
|||||||
return None
|
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
|
@callback
|
||||||
def async_register_ice_servers(
|
def async_register_ice_servers(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -411,94 +378,3 @@ def async_register_ice_servers(
|
|||||||
|
|
||||||
servers.append(get_ice_server_fn)
|
servers.append(get_ice_server_fn)
|
||||||
return remove
|
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@@ -10,12 +10,12 @@
|
|||||||
"known_hosts": "Add known host"
|
"known_hosts": "Add known host"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"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": {
|
"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": {
|
"options": {
|
||||||
|
@@ -61,7 +61,6 @@ from .const import (
|
|||||||
CONF_RELAYER_SERVER,
|
CONF_RELAYER_SERVER,
|
||||||
CONF_REMOTESTATE_SERVER,
|
CONF_REMOTESTATE_SERVER,
|
||||||
CONF_SERVICEHANDLERS_SERVER,
|
CONF_SERVICEHANDLERS_SERVER,
|
||||||
CONF_THINGTALK_SERVER,
|
|
||||||
CONF_USER_POOL_ID,
|
CONF_USER_POOL_ID,
|
||||||
DATA_CLOUD,
|
DATA_CLOUD,
|
||||||
DATA_CLOUD_LOG_HANDLER,
|
DATA_CLOUD_LOG_HANDLER,
|
||||||
@@ -134,7 +133,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
|
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
|
||||||
vol.Optional(CONF_RELAYER_SERVER): str,
|
vol.Optional(CONF_RELAYER_SERVER): str,
|
||||||
vol.Optional(CONF_REMOTESTATE_SERVER): str,
|
vol.Optional(CONF_REMOTESTATE_SERVER): str,
|
||||||
vol.Optional(CONF_THINGTALK_SERVER): str,
|
|
||||||
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
|
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -26,7 +26,11 @@ from homeassistant.core import Context, HassJob, HomeAssistant, callback
|
|||||||
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
|
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import (
|
||||||
|
IssueSeverity,
|
||||||
|
async_create_issue,
|
||||||
|
async_delete_issue,
|
||||||
|
)
|
||||||
from homeassistant.util.aiohttp import MockRequest, serialize_response
|
from homeassistant.util.aiohttp import MockRequest, serialize_response
|
||||||
|
|
||||||
from . import alexa_config, google_config
|
from . import alexa_config, google_config
|
||||||
@@ -36,6 +40,7 @@ from .prefs import CloudPreferences
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
VALID_REPAIR_TRANSLATION_KEYS = {
|
VALID_REPAIR_TRANSLATION_KEYS = {
|
||||||
|
"no_subscription",
|
||||||
"warn_bad_custom_domain_configuration",
|
"warn_bad_custom_domain_configuration",
|
||||||
"reset_bad_custom_domain_configuration",
|
"reset_bad_custom_domain_configuration",
|
||||||
}
|
}
|
||||||
@@ -409,3 +414,7 @@ class CloudClient(Interface):
|
|||||||
severity=IssueSeverity(severity),
|
severity=IssueSeverity(severity),
|
||||||
is_fixable=False,
|
is_fixable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_delete_repair_issue(self, identifier: str) -> None:
|
||||||
|
"""Delete a repair issue."""
|
||||||
|
async_delete_issue(hass=self._hass, domain=DOMAIN, issue_id=identifier)
|
||||||
|
@@ -81,7 +81,6 @@ CONF_ACME_SERVER = "acme_server"
|
|||||||
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
|
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
|
||||||
CONF_RELAYER_SERVER = "relayer_server"
|
CONF_RELAYER_SERVER = "relayer_server"
|
||||||
CONF_REMOTESTATE_SERVER = "remotestate_server"
|
CONF_REMOTESTATE_SERVER = "remotestate_server"
|
||||||
CONF_THINGTALK_SERVER = "thingtalk_server"
|
|
||||||
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
|
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
|
||||||
|
|
||||||
MODE_DEV = "development"
|
MODE_DEV = "development"
|
||||||
|
@@ -16,7 +16,7 @@ from typing import Any, Concatenate, cast
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import attr
|
import attr
|
||||||
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
|
from hass_nabucasa import AlreadyConnectedError, Cloud, auth
|
||||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||||
from hass_nabucasa.voice_data import TTS_VOICES
|
from hass_nabucasa.voice_data import TTS_VOICES
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -104,7 +104,6 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||||||
websocket_api.async_register_command(hass, alexa_list)
|
websocket_api.async_register_command(hass, alexa_list)
|
||||||
websocket_api.async_register_command(hass, alexa_sync)
|
websocket_api.async_register_command(hass, alexa_sync)
|
||||||
|
|
||||||
websocket_api.async_register_command(hass, thingtalk_convert)
|
|
||||||
websocket_api.async_register_command(hass, tts_info)
|
websocket_api.async_register_command(hass, tts_info)
|
||||||
|
|
||||||
hass.http.register_view(GoogleActionsSyncView)
|
hass.http.register_view(GoogleActionsSyncView)
|
||||||
@@ -998,25 +997,6 @@ async def alexa_sync(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str})
|
|
||||||
@websocket_api.async_response
|
|
||||||
async def thingtalk_convert(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
connection: websocket_api.ActiveConnection,
|
|
||||||
msg: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Convert a query."""
|
|
||||||
cloud = hass.data[DATA_CLOUD]
|
|
||||||
|
|
||||||
async with asyncio.timeout(10):
|
|
||||||
try:
|
|
||||||
connection.send_result(
|
|
||||||
msg["id"], await thingtalk.async_convert(cloud, msg["query"])
|
|
||||||
)
|
|
||||||
except thingtalk.ThingTalkConversionError as err:
|
|
||||||
connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
|
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command({"type": "cloud/tts/info"})
|
@websocket_api.websocket_command({"type": "cloud/tts/info"})
|
||||||
def tts_info(
|
def tts_info(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==0.96.0"],
|
"requirements": ["hass-nabucasa==0.100.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -62,6 +62,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"no_subscription": {
|
||||||
|
"title": "No subscription detected",
|
||||||
|
"description": "You do not have a Home Assistant Cloud subscription. Subscribe at {account_url}."
|
||||||
|
},
|
||||||
"warn_bad_custom_domain_configuration": {
|
"warn_bad_custom_domain_configuration": {
|
||||||
"title": "Detected wrong custom domain configuration",
|
"title": "Detected wrong custom domain configuration",
|
||||||
"description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. Please check the DNS configuration of your domain and make sure it points to the correct CNAME."
|
"description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. Please check the DNS configuration of your domain and make sure it points to the correct CNAME."
|
||||||
|
@@ -65,8 +65,8 @@ rules:
|
|||||||
status: todo
|
status: todo
|
||||||
comment: missing implementation
|
comment: missing implementation
|
||||||
entity-category:
|
entity-category:
|
||||||
status: todo
|
status: exempt
|
||||||
comment: PR in progress
|
comment: no config or diagnostic entities
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
|
@@ -165,9 +165,7 @@ class ConfigManagerFlowIndexView(
|
|||||||
"""Not implemented."""
|
"""Not implemented."""
|
||||||
raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"])
|
raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"])
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
|
||||||
)
|
|
||||||
@RequestDataValidator(
|
@RequestDataValidator(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -218,16 +216,12 @@ class ConfigManagerFlowResourceView(
|
|||||||
url = "/api/config/config_entries/flow/{flow_id}"
|
url = "/api/config/config_entries/flow/{flow_id}"
|
||||||
name = "api:config:config_entries:flow:resource"
|
name = "api:config:config_entries:flow:resource"
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
|
||||||
)
|
|
||||||
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
||||||
"""Get the current state of a data_entry_flow."""
|
"""Get the current state of a data_entry_flow."""
|
||||||
return await super().get(request, flow_id)
|
return await super().get(request, flow_id)
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
|
||||||
)
|
|
||||||
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
||||||
"""Handle a POST request."""
|
"""Handle a POST request."""
|
||||||
return await super().post(request, flow_id)
|
return await super().post(request, flow_id)
|
||||||
@@ -262,9 +256,7 @@ class OptionManagerFlowIndexView(
|
|||||||
url = "/api/config/config_entries/options/flow"
|
url = "/api/config/config_entries/options/flow"
|
||||||
name = "api:config:config_entries:option:flow"
|
name = "api:config:config_entries:option:flow"
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
|
||||||
)
|
|
||||||
async def post(self, request: web.Request) -> web.Response:
|
async def post(self, request: web.Request) -> web.Response:
|
||||||
"""Handle a POST request.
|
"""Handle a POST request.
|
||||||
|
|
||||||
@@ -281,16 +273,12 @@ class OptionManagerFlowResourceView(
|
|||||||
url = "/api/config/config_entries/options/flow/{flow_id}"
|
url = "/api/config/config_entries/options/flow/{flow_id}"
|
||||||
name = "api:config:config_entries:options:flow:resource"
|
name = "api:config:config_entries:options:flow:resource"
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
|
||||||
)
|
|
||||||
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
||||||
"""Get the current state of a data_entry_flow."""
|
"""Get the current state of a data_entry_flow."""
|
||||||
return await super().get(request, flow_id)
|
return await super().get(request, flow_id)
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
|
||||||
)
|
|
||||||
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
||||||
"""Handle a POST request."""
|
"""Handle a POST request."""
|
||||||
return await super().post(request, flow_id)
|
return await super().post(request, flow_id)
|
||||||
@@ -304,9 +292,7 @@ class SubentryManagerFlowIndexView(
|
|||||||
url = "/api/config/config_entries/subentries/flow"
|
url = "/api/config/config_entries/subentries/flow"
|
||||||
name = "api:config:config_entries:subentries:flow"
|
name = "api:config:config_entries:subentries:flow"
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
|
||||||
)
|
|
||||||
@RequestDataValidator(
|
@RequestDataValidator(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -341,16 +327,12 @@ class SubentryManagerFlowResourceView(
|
|||||||
url = "/api/config/config_entries/subentries/flow/{flow_id}"
|
url = "/api/config/config_entries/subentries/flow/{flow_id}"
|
||||||
name = "api:config:config_entries:subentries:flow:resource"
|
name = "api:config:config_entries:subentries:flow:resource"
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
|
||||||
)
|
|
||||||
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
||||||
"""Get the current state of a data_entry_flow."""
|
"""Get the current state of a data_entry_flow."""
|
||||||
return await super().get(request, flow_id)
|
return await super().get(request, flow_id)
|
||||||
|
|
||||||
@require_admin(
|
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
|
||||||
)
|
|
||||||
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
||||||
"""Handle a POST request."""
|
"""Handle a POST request."""
|
||||||
return await super().post(request, flow_id)
|
return await super().post(request, flow_id)
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.4.30"]
|
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN as DANFOSS_AIR_DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
def setup_platform(
|
||||||
@@ -22,7 +22,7 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the available Danfoss Air sensors etc."""
|
"""Set up the available Danfoss Air sensors etc."""
|
||||||
data = hass.data[DANFOSS_AIR_DOMAIN]
|
data = hass.data[DOMAIN]
|
||||||
|
|
||||||
sensors = [
|
sensors = [
|
||||||
[
|
[
|
||||||
|
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN as DANFOSS_AIR_DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the available Danfoss Air sensors etc."""
|
"""Set up the available Danfoss Air sensors etc."""
|
||||||
data = hass.data[DANFOSS_AIR_DOMAIN]
|
data = hass.data[DOMAIN]
|
||||||
|
|
||||||
sensors = [
|
sensors = [
|
||||||
[
|
[
|
||||||
|
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN as DANFOSS_AIR_DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Danfoss Air HRV switch platform."""
|
"""Set up the Danfoss Air HRV switch platform."""
|
||||||
data = hass.data[DANFOSS_AIR_DOMAIN]
|
data = hass.data[DOMAIN]
|
||||||
|
|
||||||
switches = [
|
switches = [
|
||||||
[
|
[
|
||||||
|
@@ -13,7 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DOMAIN as DECONZ_DOMAIN
|
from .const import DOMAIN
|
||||||
from .hub import DeconzHub
|
from .hub import DeconzHub
|
||||||
from .util import serial_from_unique_id
|
from .util import serial_from_unique_id
|
||||||
|
|
||||||
@@ -59,12 +59,12 @@ class DeconzBase[_DeviceT: _DeviceType]:
|
|||||||
|
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
connections={(CONNECTION_ZIGBEE, self.serial)},
|
connections={(CONNECTION_ZIGBEE, self.serial)},
|
||||||
identifiers={(DECONZ_DOMAIN, self.serial)},
|
identifiers={(DOMAIN, self.serial)},
|
||||||
manufacturer=self._device.manufacturer,
|
manufacturer=self._device.manufacturer,
|
||||||
model=self._device.model_id,
|
model=self._device.model_id,
|
||||||
name=self._device.name,
|
name=self._device.name,
|
||||||
sw_version=self._device.software_version,
|
sw_version=self._device.software_version,
|
||||||
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
|
via_device=(DOMAIN, self.hub.api.config.bridge_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -176,9 +176,9 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]):
|
|||||||
def device_info(self) -> DeviceInfo:
|
def device_info(self) -> DeviceInfo:
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
identifiers={(DECONZ_DOMAIN, self._group_identifier)},
|
identifiers={(DOMAIN, self._group_identifier)},
|
||||||
manufacturer="Dresden Elektronik",
|
manufacturer="Dresden Elektronik",
|
||||||
model="deCONZ group",
|
model="deCONZ group",
|
||||||
name=self.group.name,
|
name=self.group.name,
|
||||||
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
|
via_device=(DOMAIN, self.hub.api.config.bridge_id),
|
||||||
)
|
)
|
||||||
|
@@ -38,7 +38,7 @@ from homeassistant.util.color import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from . import DeconzConfigEntry
|
from . import DeconzConfigEntry
|
||||||
from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS
|
from .const import DOMAIN, POWER_PLUGS
|
||||||
from .entity import DeconzDevice
|
from .entity import DeconzDevice
|
||||||
from .hub import DeconzHub
|
from .hub import DeconzHub
|
||||||
|
|
||||||
@@ -395,11 +395,11 @@ class DeconzGroup(DeconzBaseLight[Group]):
|
|||||||
def device_info(self) -> DeviceInfo:
|
def device_info(self) -> DeviceInfo:
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
identifiers={(DECONZ_DOMAIN, self.unique_id)},
|
identifiers={(DOMAIN, self.unique_id)},
|
||||||
manufacturer="Dresden Elektronik",
|
manufacturer="Dresden Elektronik",
|
||||||
model="deCONZ group",
|
model="deCONZ group",
|
||||||
name=self._device.name,
|
name=self._device.name,
|
||||||
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
|
via_device=(DOMAIN, self.hub.api.config.bridge_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["denonavr"],
|
"loggers": ["denonavr"],
|
||||||
"requirements": ["denonavr==1.0.1"],
|
"requirements": ["denonavr==1.1.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Denon",
|
"manufacturer": "Denon",
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["devolo_home_control_api"],
|
"loggers": ["devolo_home_control_api"],
|
||||||
"requirements": ["devolo-home-control-api==0.18.3"],
|
"requirements": ["devolo-home-control-api==0.19.0"],
|
||||||
"zeroconf": ["_dvl-deviceapi._tcp.local."]
|
"zeroconf": ["_dvl-deviceapi._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aiodhcpwatcher==1.1.1",
|
"aiodhcpwatcher==1.1.1",
|
||||||
"aiodiscover==2.6.1",
|
"aiodiscover==2.7.0",
|
||||||
"cached-ipaddress==0.10.0"
|
"cached-ipaddress==0.10.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -68,7 +68,7 @@ async def async_validate_hostname(
|
|||||||
result = False
|
result = False
|
||||||
with contextlib.suppress(DNSError):
|
with contextlib.suppress(DNSError):
|
||||||
result = bool(
|
result = bool(
|
||||||
await aiodns.DNSResolver(
|
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||||
).query(hostname, qtype)
|
).query(hostname, qtype)
|
||||||
)
|
)
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["aiodns==3.2.0"]
|
"requirements": ["aiodns==3.4.0"]
|
||||||
}
|
}
|
||||||
|
@@ -106,7 +106,7 @@ class WanIpSensor(SensorEntity):
|
|||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the current DNS IP address for hostname."""
|
"""Get the current DNS IP address for hostname."""
|
||||||
try:
|
try:
|
||||||
response = await self.resolver.query(self.hostname, self.querytype)
|
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
|
||||||
except DNSError as err:
|
except DNSError as err:
|
||||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||||
response = None
|
response = None
|
||||||
|
@@ -3,10 +3,10 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"events": "Comma separated list of events."
|
"events": "Comma-separated list of events."
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
|
"events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN as DOVADO_DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ def get_service(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> DovadoSMSNotificationService:
|
) -> DovadoSMSNotificationService:
|
||||||
"""Get the Dovado Router SMS notification service."""
|
"""Get the Dovado Router SMS notification service."""
|
||||||
return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client)
|
return DovadoSMSNotificationService(hass.data[DOMAIN].client)
|
||||||
|
|
||||||
|
|
||||||
class DovadoSMSNotificationService(BaseNotificationService):
|
class DovadoSMSNotificationService(BaseNotificationService):
|
||||||
|
@@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN as DOVADO_DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Dovado sensor platform."""
|
"""Set up the Dovado sensor platform."""
|
||||||
dovado = hass.data[DOVADO_DOMAIN]
|
dovado = hass.data[DOMAIN]
|
||||||
|
|
||||||
sensors = config[CONF_SENSORS]
|
sensors = config[CONF_SENSORS]
|
||||||
entities = [
|
entities = [
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.10", "deebot-client==13.0.1"]
|
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,12 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"phone_number": "Phone Number"
|
"phone_number": "Phone number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"one_time_password": {
|
"one_time_password": {
|
||||||
"data": {
|
"data": {
|
||||||
"one_time_password": "One Time Password"
|
"one_time_password": "One-time password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -7,12 +7,12 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"advertise_ip": "Advertise IP Address",
|
"advertise_ip": "Advertise IP address",
|
||||||
"advertise_port": "Advertise Port",
|
"advertise_port": "Advertise port",
|
||||||
"host_ip": "Host IP Address",
|
"host_ip": "Host IP address",
|
||||||
"listen_port": "Listen Port",
|
"listen_port": "Listen port",
|
||||||
"name": "[%key:common::config_flow::data::name%]",
|
"name": "[%key:common::config_flow::data::name%]",
|
||||||
"upnp_bind_multicast": "Bind multicast (True/False)"
|
"upnp_bind_multicast": "Bind multicast"
|
||||||
},
|
},
|
||||||
"title": "Define server configuration"
|
"title": "Define server configuration"
|
||||||
}
|
}
|
||||||
|
@@ -52,6 +52,7 @@ VALID_ENERGY_UNITS_GAS = {
|
|||||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||||
UnitOfVolume.CUBIC_FEET,
|
UnitOfVolume.CUBIC_FEET,
|
||||||
UnitOfVolume.CUBIC_METERS,
|
UnitOfVolume.CUBIC_METERS,
|
||||||
|
UnitOfVolume.LITERS,
|
||||||
*VALID_ENERGY_UNITS,
|
*VALID_ENERGY_UNITS,
|
||||||
}
|
}
|
||||||
VALID_VOLUME_UNITS_WATER: set[str] = {
|
VALID_VOLUME_UNITS_WATER: set[str] = {
|
||||||
|
@@ -50,6 +50,7 @@ GAS_USAGE_UNITS: dict[str, tuple[UnitOfEnergy | UnitOfVolume, ...]] = {
|
|||||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||||
UnitOfVolume.CUBIC_FEET,
|
UnitOfVolume.CUBIC_FEET,
|
||||||
UnitOfVolume.CUBIC_METERS,
|
UnitOfVolume.CUBIC_METERS,
|
||||||
|
UnitOfVolume.LITERS,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
GAS_PRICE_UNITS = tuple(
|
GAS_PRICE_UNITS = tuple(
|
||||||
|
@@ -22,5 +22,5 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["eq3btsmart"],
|
"loggers": ["eq3btsmart"],
|
||||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
|
SOURCE_IGNORE,
|
||||||
SOURCE_REAUTH,
|
SOURCE_REAUTH,
|
||||||
SOURCE_RECONFIGURE,
|
SOURCE_RECONFIGURE,
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
@@ -31,6 +32,7 @@ from homeassistant.config_entries import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.data_entry_flow import AbortFlow
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||||
@@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
if entry.source == SOURCE_IGNORE:
|
||||||
|
# Don't call _fetch_device_info() for ignored entries
|
||||||
|
raise AbortFlow("already_configured")
|
||||||
|
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||||
|
if configured_host == host and configured_port == port:
|
||||||
|
# Don't probe to verify the mac is correct since
|
||||||
|
# the host and port matches.
|
||||||
|
raise AbortFlow("already_configured")
|
||||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||||
updates: dict[str, Any] = {}
|
updates: dict[str, Any] = {}
|
||||||
|
@@ -134,6 +134,22 @@ def esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
|
|||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def async_esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
|
||||||
|
func: Callable[[_EntityT], Awaitable[_R | None]],
|
||||||
|
) -> Callable[[_EntityT], Coroutine[Any, Any, _R | None]]:
|
||||||
|
"""Wrap a state property of an esphome entity.
|
||||||
|
|
||||||
|
This checks if the state object in the entity is set
|
||||||
|
and returns None if it is not set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
async def _wrapper(self: _EntityT) -> _R | None:
|
||||||
|
return await func(self) if self._has_state else None
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]](
|
def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]](
|
||||||
func: Callable[[_EntityT], float | None],
|
func: Callable[[_EntityT], float | None],
|
||||||
) -> Callable[[_EntityT], float | None]:
|
) -> Callable[[_EntityT], float | None]:
|
||||||
|
@@ -8,6 +8,7 @@ from collections.abc import Callable, Iterable
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from operator import delitem
|
||||||
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
||||||
|
|
||||||
from aioesphomeapi import (
|
from aioesphomeapi import (
|
||||||
@@ -183,18 +184,7 @@ class RuntimeEntryData:
|
|||||||
"""Register to receive callbacks when static info changes for an EntityInfo type."""
|
"""Register to receive callbacks when static info changes for an EntityInfo type."""
|
||||||
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
|
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
|
||||||
callbacks.append(callback_)
|
callbacks.append(callback_)
|
||||||
return partial(
|
return partial(callbacks.remove, callback_)
|
||||||
self._async_unsubscribe_register_static_info, callbacks, callback_
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_register_static_info(
|
|
||||||
self,
|
|
||||||
callbacks: list[Callable[[list[EntityInfo]], None]],
|
|
||||||
callback_: Callable[[list[EntityInfo]], None],
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to when static info is registered."""
|
|
||||||
callbacks.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_key_static_info_updated_callback(
|
def async_register_key_static_info_updated_callback(
|
||||||
@@ -206,18 +196,7 @@ class RuntimeEntryData:
|
|||||||
callback_key = (type(static_info), static_info.key)
|
callback_key = (type(static_info), static_info.key)
|
||||||
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
|
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
|
||||||
callbacks.append(callback_)
|
callbacks.append(callback_)
|
||||||
return partial(
|
return partial(callbacks.remove, callback_)
|
||||||
self._async_unsubscribe_static_key_info_updated, callbacks, callback_
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_static_key_info_updated(
|
|
||||||
self,
|
|
||||||
callbacks: list[Callable[[EntityInfo], None]],
|
|
||||||
callback_: Callable[[EntityInfo], None],
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to when static info is updated ."""
|
|
||||||
callbacks.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_assist_pipeline_state(self, state: bool) -> None:
|
def async_set_assist_pipeline_state(self, state: bool) -> None:
|
||||||
@@ -232,14 +211,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to assist pipeline updates."""
|
"""Subscribe to assist pipeline updates."""
|
||||||
self.assist_pipeline_update_callbacks.append(update_callback)
|
self.assist_pipeline_update_callbacks.append(update_callback)
|
||||||
return partial(self._async_unsubscribe_assist_pipeline_update, update_callback)
|
return partial(self.assist_pipeline_update_callbacks.remove, update_callback)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_assist_pipeline_update(
|
|
||||||
self, update_callback: CALLBACK_TYPE
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to assist pipeline updates."""
|
|
||||||
self.assist_pipeline_update_callbacks.remove(update_callback)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_remove_entities(
|
def async_remove_entities(
|
||||||
@@ -337,12 +309,7 @@ class RuntimeEntryData:
|
|||||||
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to state updates."""
|
"""Subscribe to state updates."""
|
||||||
self.device_update_subscriptions.add(callback_)
|
self.device_update_subscriptions.add(callback_)
|
||||||
return partial(self._async_unsubscribe_device_update, callback_)
|
return partial(self.device_update_subscriptions.remove, callback_)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None:
|
|
||||||
"""Unsubscribe to device updates."""
|
|
||||||
self.device_update_subscriptions.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_subscribe_static_info_updated(
|
def async_subscribe_static_info_updated(
|
||||||
@@ -350,14 +317,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to static info updates."""
|
"""Subscribe to static info updates."""
|
||||||
self.static_info_update_subscriptions.add(callback_)
|
self.static_info_update_subscriptions.add(callback_)
|
||||||
return partial(self._async_unsubscribe_static_info_updated, callback_)
|
return partial(self.static_info_update_subscriptions.remove, callback_)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_static_info_updated(
|
|
||||||
self, callback_: Callable[[list[EntityInfo]], None]
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to static info updates."""
|
|
||||||
self.static_info_update_subscriptions.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_subscribe_state_update(
|
def async_subscribe_state_update(
|
||||||
@@ -369,14 +329,7 @@ class RuntimeEntryData:
|
|||||||
"""Subscribe to state updates."""
|
"""Subscribe to state updates."""
|
||||||
subscription_key = (state_type, state_key)
|
subscription_key = (state_type, state_key)
|
||||||
self.state_subscriptions[subscription_key] = entity_callback
|
self.state_subscriptions[subscription_key] = entity_callback
|
||||||
return partial(self._async_unsubscribe_state_update, subscription_key)
|
return partial(delitem, self.state_subscriptions, subscription_key)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_state_update(
|
|
||||||
self, subscription_key: tuple[type[EntityState], int]
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to state updates."""
|
|
||||||
self.state_subscriptions.pop(subscription_key)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_state(self, state: EntityState) -> None:
|
def async_update_state(self, state: EntityState) -> None:
|
||||||
@@ -523,7 +476,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
|
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
|
||||||
self.assist_satellite_config_update_callbacks.append(callback_)
|
self.assist_satellite_config_update_callbacks.append(callback_)
|
||||||
return lambda: self.assist_satellite_config_update_callbacks.remove(callback_)
|
return partial(self.assist_satellite_config_update_callbacks.remove, callback_)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_assist_satellite_config_updated(
|
def async_assist_satellite_config_updated(
|
||||||
@@ -540,7 +493,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Register to receive callbacks when the Assist satellite's wake word is set."""
|
"""Register to receive callbacks when the Assist satellite's wake word is set."""
|
||||||
self.assist_satellite_set_wake_word_callbacks.append(callback_)
|
self.assist_satellite_set_wake_word_callbacks.append(callback_)
|
||||||
return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_)
|
return partial(self.assist_satellite_set_wake_word_callbacks.remove, callback_)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
|
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
|
||||||
|
@@ -17,9 +17,9 @@
|
|||||||
"mqtt": ["esphome/discover/#"],
|
"mqtt": ["esphome/discover/#"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==30.1.0",
|
"aioesphomeapi==30.2.0",
|
||||||
"esphome-dashboard-api==1.3.0",
|
"esphome-dashboard-api==1.3.0",
|
||||||
"bleak-esphome==2.14.0"
|
"bleak-esphome==2.15.1"
|
||||||
],
|
],
|
||||||
"zeroconf": ["_esphomelib._tcp.local."]
|
"zeroconf": ["_esphomelib._tcp.local."]
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user