mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Merge branch 'dev' into prepare_protobuf6
This commit is contained in:
commit
ec0f808450
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -457,7 +457,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
34
.github/workflows/ci.yaml
vendored
34
.github/workflows/ci.yaml
vendored
@ -40,7 +40,7 @@ env:
|
|||||||
CACHE_VERSION: 12
|
CACHE_VERSION: 12
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 9
|
MYPY_CACHE_VERSION: 9
|
||||||
HA_SHORT_VERSION: "2025.4"
|
HA_SHORT_VERSION: "2025.5"
|
||||||
DEFAULT_PYTHON: "3.13"
|
DEFAULT_PYTHON: "3.13"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@ -249,7 +249,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -294,7 +294,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: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -334,7 +334,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: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -374,7 +374,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: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -484,7 +484,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -587,7 +587,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -620,7 +620,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -677,7 +677,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -720,7 +720,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -767,7 +767,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -812,7 +812,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -889,7 +889,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -949,7 +949,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -1074,7 +1074,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -1208,7 +1208,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -1359,7 +1359,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
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.12
|
uses: github/codeql-action/init@v3.28.13
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.28.12
|
uses: github/codeql-action/analyze@v3.28.13
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
6
.github/workflows/wheels.yml
vendored
6
.github/workflows/wheels.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.4.0
|
uses: actions/setup-python@v5.5.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -159,7 +159,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.02.0
|
uses: home-assistant/wheels@2025.03.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@ -219,7 +219,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.02.0
|
uses: home-assistant/wheels@2025.03.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
@ -119,6 +119,7 @@ homeassistant.components.bluetooth_adapters.*
|
|||||||
homeassistant.components.bluetooth_tracker.*
|
homeassistant.components.bluetooth_tracker.*
|
||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
|
homeassistant.components.bosch_alarm.*
|
||||||
homeassistant.components.braviatv.*
|
homeassistant.components.braviatv.*
|
||||||
homeassistant.components.bring.*
|
homeassistant.components.bring.*
|
||||||
homeassistant.components.brother.*
|
homeassistant.components.brother.*
|
||||||
|
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@ -216,6 +216,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
|
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
|
||||||
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||||
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||||
|
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
|
||||||
|
/tests/components/bosch_alarm/ @mag1024 @sanjay900
|
||||||
/homeassistant/components/bosch_shc/ @tschamm
|
/homeassistant/components/bosch_shc/ @tschamm
|
||||||
/tests/components/bosch_shc/ @tschamm
|
/tests/components/bosch_shc/ @tschamm
|
||||||
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
||||||
@ -1183,6 +1185,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/prusalink/ @balloob
|
/tests/components/prusalink/ @balloob
|
||||||
/homeassistant/components/ps4/ @ktnrg45
|
/homeassistant/components/ps4/ @ktnrg45
|
||||||
/tests/components/ps4/ @ktnrg45
|
/tests/components/ps4/ @ktnrg45
|
||||||
|
/homeassistant/components/pterodactyl/ @elmurato
|
||||||
|
/tests/components/pterodactyl/ @elmurato
|
||||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||||
/tests/components/pure_energie/ @klaasnicolaas
|
/tests/components/pure_energie/ @klaasnicolaas
|
||||||
/homeassistant/components/purpleair/ @bachya
|
/homeassistant/components/purpleair/ @bachya
|
||||||
@ -1476,8 +1480,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/suez_water/ @ooii @jb101010-2
|
/tests/components/suez_water/ @ooii @jb101010-2
|
||||||
/homeassistant/components/sun/ @Swamp-Ig
|
/homeassistant/components/sun/ @Swamp-Ig
|
||||||
/tests/components/sun/ @Swamp-Ig
|
/tests/components/sun/ @Swamp-Ig
|
||||||
/homeassistant/components/sunweg/ @rokam
|
|
||||||
/tests/components/sunweg/ @rokam
|
|
||||||
/homeassistant/components/supla/ @mwegrzynek
|
/homeassistant/components/supla/ @mwegrzynek
|
||||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||||
|
2
Dockerfile
generated
2
Dockerfile
generated
@ -31,7 +31,7 @@ RUN \
|
|||||||
&& go2rtc --version
|
&& go2rtc --version
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.6.8
|
RUN pip3 install uv==0.6.10
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@ -19,4 +19,4 @@ labels:
|
|||||||
org.opencontainers.image.authors: The Home Assistant Authors
|
org.opencontainers.image.authors: The Home Assistant Authors
|
||||||
org.opencontainers.image.url: https://www.home-assistant.io/
|
org.opencontainers.image.url: https://www.home-assistant.io/
|
||||||
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
|
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
|
||||||
org.opencontainers.image.licenses: Apache License 2.0
|
org.opencontainers.image.licenses: Apache-2.0
|
||||||
|
5
homeassistant/brands/bosch.json
Normal file
5
homeassistant/brands/bosch.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "bosch",
|
||||||
|
"name": "Bosch",
|
||||||
|
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"domain": "motionblinds",
|
"domain": "motionblinds",
|
||||||
"name": "Motionblinds",
|
"name": "Motionblinds",
|
||||||
"integrations": ["motion_blinds", "motionblinds_ble"]
|
"integrations": ["motion_blinds", "motionblinds_ble"],
|
||||||
|
"iot_standards": ["matter"]
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,8 @@
|
|||||||
"led_bar_mode": {
|
"led_bar_mode": {
|
||||||
"name": "LED bar mode",
|
"name": "LED bar mode",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Off",
|
"off": "[%key:common::state::off%]",
|
||||||
"co2": "Carbon dioxide",
|
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||||
"pm": "Particulate matter"
|
"pm": "Particulate matter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -143,8 +143,8 @@
|
|||||||
"led_bar_mode": {
|
"led_bar_mode": {
|
||||||
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
|
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||||
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ from aiohttp import ClientSession
|
|||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
from pyairnow import WebServiceAPI
|
from pyairnow import WebServiceAPI
|
||||||
from pyairnow.conv import aqi_to_concentration
|
from pyairnow.conv import aqi_to_concentration
|
||||||
from pyairnow.errors import AirNowError
|
from pyairnow.errors import AirNowError, InvalidJsonError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
distance=self.distance,
|
distance=self.distance,
|
||||||
)
|
)
|
||||||
|
|
||||||
except (AirNowError, ClientConnectorError) as error:
|
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(error) from error
|
||||||
|
|
||||||
if not obs:
|
if not obs:
|
||||||
|
@ -102,7 +102,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
device = await self._get_device_data(discovery_info)
|
device = await self._get_device_data(discovery_info)
|
||||||
except AirthingsDeviceUpdateError:
|
except AirthingsDeviceUpdateError:
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
|
_LOGGER.exception("Unknown error occurred")
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
|
|
||||||
name = get_name(device)
|
name = get_name(device)
|
||||||
@ -160,7 +161,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
device = await self._get_device_data(discovery_info)
|
device = await self._get_device_data(discovery_info)
|
||||||
except AirthingsDeviceUpdateError:
|
except AirthingsDeviceUpdateError:
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
|
_LOGGER.exception("Unknown error occurred")
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
name = get_name(device)
|
name = get_name(device)
|
||||||
self._discovered_devices[address] = Discovery(name, discovery_info, device)
|
self._discovered_devices[address] = Discovery(name, discovery_info, device)
|
||||||
|
@ -32,7 +32,8 @@ class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
client = Airtouch5SimpleClient(user_input[CONF_HOST])
|
client = Airtouch5SimpleClient(user_input[CONF_HOST])
|
||||||
try:
|
try:
|
||||||
await client.test_connection()
|
await client.test_connection()
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors = {"base": "cannot_connect"}
|
errors = {"base": "cannot_connect"}
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(user_input[CONF_HOST])
|
await self.async_set_unique_id(user_input[CONF_HOST])
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"geography_by_coords": {
|
"geography_by_coords": {
|
||||||
"title": "Configure a Geography",
|
"title": "Configure a geography",
|
||||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
@ -56,12 +56,12 @@
|
|||||||
"sensor": {
|
"sensor": {
|
||||||
"pollutant_label": {
|
"pollutant_label": {
|
||||||
"state": {
|
"state": {
|
||||||
"co": "Carbon Monoxide",
|
"co": "Carbon monoxide",
|
||||||
"n2": "Nitrogen Dioxide",
|
"n2": "Nitrogen dioxide",
|
||||||
"o3": "Ozone",
|
"o3": "Ozone",
|
||||||
"p1": "PM10",
|
"p1": "PM10",
|
||||||
"p2": "PM2.5",
|
"p2": "PM2.5",
|
||||||
"s2": "Sulfur Dioxide"
|
"s2": "Sulfur dioxide"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pollutant_level": {
|
"pollutant_level": {
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.10"]
|
"requirements": ["aioairzone-cloud==0.6.11"]
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,8 @@
|
|||||||
"air_quality": {
|
"air_quality": {
|
||||||
"name": "Air Quality mode",
|
"name": "Air Quality mode",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Off",
|
"off": "[%key:common::state::off%]",
|
||||||
"on": "On",
|
"on": "[%key:common::state::on%]",
|
||||||
"auto": "Auto"
|
"auto": "Auto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1438,7 +1438,7 @@ class AlexaModeController(AlexaCapability):
|
|||||||
# Fan preset_mode
|
# Fan preset_mode
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
||||||
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
|
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
|
||||||
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
|
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, ()):
|
||||||
return f"{fan.ATTR_PRESET_MODE}.{mode}"
|
return f"{fan.ATTR_PRESET_MODE}.{mode}"
|
||||||
|
|
||||||
# Humidifier mode
|
# Humidifier mode
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||||
"quality_scale": "legacy",
|
"quality_scale": "legacy",
|
||||||
"requirements": ["boto3==1.34.131"]
|
"requirements": ["boto3==1.37.1"]
|
||||||
}
|
}
|
||||||
|
@ -240,6 +240,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
suggested_display_precision=0,
|
suggested_display_precision=0,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=TYPE_WINDGUSTMPH,
|
key=TYPE_WINDGUSTMPH,
|
||||||
|
@ -609,6 +609,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
translation_key="wind_direction",
|
translation_key="wind_direction",
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=TYPE_WINDDIR_AVG10M,
|
key=TYPE_WINDDIR_AVG10M,
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["pydroid-ipcam==2.0.0"]
|
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["androidtvremote2"],
|
"loggers": ["androidtvremote2"],
|
||||||
"requirements": ["androidtvremote2==0.2.0"],
|
"requirements": ["androidtvremote2==0.2.1"],
|
||||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from anova_wifi import AnovaApi, InvalidLogin
|
from anova_wifi import AnovaApi, InvalidLogin
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -11,8 +13,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
|
class AnovaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Sets up a config flow for Anova."""
|
"""Sets up a config flow for Anova."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
@ -35,7 +39,8 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
await api.authenticate()
|
await api.authenticate()
|
||||||
except InvalidLogin:
|
except InvalidLogin:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
|
@ -43,6 +43,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
|||||||
|
|
||||||
config_entry: ApSystemsConfigEntry
|
config_entry: ApSystemsConfigEntry
|
||||||
device_version: str
|
device_version: str
|
||||||
|
battery_system: bool
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -68,6 +69,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
|||||||
self.api.max_power = device_info.maxPower
|
self.api.max_power = device_info.maxPower
|
||||||
self.api.min_power = device_info.minPower
|
self.api.min_power = device_info.minPower
|
||||||
self.device_version = device_info.devVer
|
self.device_version = device_info.devVer
|
||||||
|
self.battery_system = device_info.isBatterySystem
|
||||||
|
|
||||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||||
try:
|
try:
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["apsystems-ez1==2.4.0"]
|
"requirements": ["apsystems-ez1==2.5.0"]
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,8 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
|||||||
super().__init__(data)
|
super().__init__(data)
|
||||||
self._api = data.coordinator.api
|
self._api = data.coordinator.api
|
||||||
self._attr_unique_id = f"{data.device_id}_inverter_status"
|
self._attr_unique_id = f"{data.device_id}_inverter_status"
|
||||||
|
if data.coordinator.battery_system:
|
||||||
|
self._attr_available = False
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update switch status and availability."""
|
"""Update switch status and availability."""
|
||||||
|
@ -60,7 +60,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except AuthenticationFailed:
|
except AuthenticationFailed:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
|
@ -6,7 +6,11 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components import mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature
|
from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@ -98,6 +102,7 @@ def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] |
|
|||||||
DEGREE,
|
DEGREE,
|
||||||
"mdi:compass",
|
"mdi:compass",
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return None
|
return None
|
||||||
@ -178,6 +183,7 @@ class ArwnSensor(SensorEntity):
|
|||||||
units: str,
|
units: str,
|
||||||
icon: str | None = None,
|
icon: str | None = None,
|
||||||
device_class: SensorDeviceClass | None = None,
|
device_class: SensorDeviceClass | None = None,
|
||||||
|
state_class: SensorStateClass | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.entity_id = _slug(name)
|
self.entity_id = _slug(name)
|
||||||
@ -188,6 +194,7 @@ class ArwnSensor(SensorEntity):
|
|||||||
self._attr_native_unit_of_measurement = units
|
self._attr_native_unit_of_measurement = units
|
||||||
self._attr_icon = icon
|
self._attr_icon = icon
|
||||||
self._attr_device_class = device_class
|
self._attr_device_class = device_class
|
||||||
|
self._attr_state_class = state_class
|
||||||
|
|
||||||
def set_event(self, event: dict[str, Any]) -> None:
|
def set_event(self, event: dict[str, Any]) -> None:
|
||||||
"""Update the sensor with the most recent event."""
|
"""Update the sensor with the most recent event."""
|
||||||
|
@ -125,7 +125,7 @@ SAVE_DELAY = 10
|
|||||||
@callback
|
@callback
|
||||||
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
|
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
|
||||||
"""Filter out intents that are not local fallback."""
|
"""Filter out intents that are not local fallback."""
|
||||||
return result.intent.name in (intent.INTENT_GET_STATE, intent.INTENT_NEVERMIND)
|
return result.intent.name in (intent.INTENT_GET_STATE)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""Base class for assist satellite entities."""
|
"""Base class for assist satellite entities."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
@ -15,6 +17,8 @@ from .const import (
|
|||||||
CONNECTION_TEST_DATA,
|
CONNECTION_TEST_DATA,
|
||||||
DATA_COMPONENT,
|
DATA_COMPONENT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
PREANNOUNCE_FILENAME,
|
||||||
|
PREANNOUNCE_URL,
|
||||||
AssistSatelliteEntityFeature,
|
AssistSatelliteEntityFeature,
|
||||||
)
|
)
|
||||||
from .entity import (
|
from .entity import (
|
||||||
@ -56,6 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
{
|
{
|
||||||
vol.Optional("message"): str,
|
vol.Optional("message"): str,
|
||||||
vol.Optional("media_id"): str,
|
vol.Optional("media_id"): str,
|
||||||
|
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.has_at_least_one_key("message", "media_id"),
|
cv.has_at_least_one_key("message", "media_id"),
|
||||||
@ -70,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
{
|
{
|
||||||
vol.Optional("start_message"): str,
|
vol.Optional("start_message"): str,
|
||||||
vol.Optional("start_media_id"): str,
|
vol.Optional("start_media_id"): str,
|
||||||
|
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||||
vol.Optional("extra_system_prompt"): str,
|
vol.Optional("extra_system_prompt"): str,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -82,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async_register_websocket_api(hass)
|
async_register_websocket_api(hass)
|
||||||
hass.http.register_view(ConnectionTestView())
|
hass.http.register_view(ConnectionTestView())
|
||||||
|
|
||||||
|
# Default preannounce sound
|
||||||
|
await hass.http.async_register_static_paths(
|
||||||
|
[
|
||||||
|
StaticPathConfig(
|
||||||
|
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
|
|||||||
f"{DOMAIN}_connection_tests"
|
f"{DOMAIN}_connection_tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PREANNOUNCE_FILENAME = "preannounce.mp3"
|
||||||
|
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
|
||||||
|
|
||||||
|
|
||||||
class AssistSatelliteEntityFeature(IntFlag):
|
class AssistSatelliteEntityFeature(IntFlag):
|
||||||
"""Supported features of Assist satellite entity."""
|
"""Supported features of Assist satellite entity."""
|
||||||
|
@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers import chat_session, entity
|
from homeassistant.helpers import chat_session, entity
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
|
||||||
from .const import AssistSatelliteEntityFeature
|
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
|
||||||
from .errors import AssistSatelliteError, SatelliteBusyError
|
from .errors import AssistSatelliteError, SatelliteBusyError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -101,6 +101,9 @@ class AssistSatelliteAnnouncement:
|
|||||||
media_id_source: Literal["url", "media_id", "tts"]
|
media_id_source: Literal["url", "media_id", "tts"]
|
||||||
"""Source of the media ID."""
|
"""Source of the media ID."""
|
||||||
|
|
||||||
|
preannounce_media_id: str | None = None
|
||||||
|
"""Media ID to be played before announcement."""
|
||||||
|
|
||||||
|
|
||||||
class AssistSatelliteEntity(entity.Entity):
|
class AssistSatelliteEntity(entity.Entity):
|
||||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||||
@ -177,6 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
self,
|
self,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
media_id: str | None = None,
|
media_id: str | None = None,
|
||||||
|
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Play and show an announcement on the satellite.
|
"""Play and show an announcement on the satellite.
|
||||||
|
|
||||||
@ -186,6 +190,9 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
If media_id is provided, it is played directly. It is possible
|
If media_id is provided, it is played directly. It is possible
|
||||||
to omit the message and the satellite will not show any text.
|
to omit the message and the satellite will not show any text.
|
||||||
|
|
||||||
|
If preannounce_media_id is provided, it overrides the default sound.
|
||||||
|
If preannounce_media_id is None, no sound is played.
|
||||||
|
|
||||||
Calls async_announce with message and media id.
|
Calls async_announce with message and media id.
|
||||||
"""
|
"""
|
||||||
await self._cancel_running_pipeline()
|
await self._cancel_running_pipeline()
|
||||||
@ -193,7 +200,9 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
if message is None:
|
if message is None:
|
||||||
message = ""
|
message = ""
|
||||||
|
|
||||||
announcement = await self._resolve_announcement_media_id(message, media_id)
|
announcement = await self._resolve_announcement_media_id(
|
||||||
|
message, media_id, preannounce_media_id
|
||||||
|
)
|
||||||
|
|
||||||
if self._is_announcing:
|
if self._is_announcing:
|
||||||
raise SatelliteBusyError
|
raise SatelliteBusyError
|
||||||
@ -220,6 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
start_message: str | None = None,
|
start_message: str | None = None,
|
||||||
start_media_id: str | None = None,
|
start_media_id: str | None = None,
|
||||||
extra_system_prompt: str | None = None,
|
extra_system_prompt: str | None = None,
|
||||||
|
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Start a conversation from the satellite.
|
"""Start a conversation from the satellite.
|
||||||
|
|
||||||
@ -229,6 +239,9 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
If start_media_id is provided, it is played directly. It is possible
|
If start_media_id is provided, it is played directly. It is possible
|
||||||
to omit the message and the satellite will not show any text.
|
to omit the message and the satellite will not show any text.
|
||||||
|
|
||||||
|
If preannounce_media_id is provided, it is played before the announcement.
|
||||||
|
If preannounce_media_id is None, no sound is played.
|
||||||
|
|
||||||
Calls async_start_conversation.
|
Calls async_start_conversation.
|
||||||
"""
|
"""
|
||||||
await self._cancel_running_pipeline()
|
await self._cancel_running_pipeline()
|
||||||
@ -244,13 +257,15 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
start_message = ""
|
start_message = ""
|
||||||
|
|
||||||
announcement = await self._resolve_announcement_media_id(
|
announcement = await self._resolve_announcement_media_id(
|
||||||
start_message, start_media_id
|
start_message, start_media_id, preannounce_media_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._is_announcing:
|
if self._is_announcing:
|
||||||
raise SatelliteBusyError
|
raise SatelliteBusyError
|
||||||
|
|
||||||
self._is_announcing = True
|
self._is_announcing = True
|
||||||
|
self._set_state(AssistSatelliteState.RESPONDING)
|
||||||
|
|
||||||
# Provide our start info to the LLM so it understands context of incoming message
|
# Provide our start info to the LLM so it understands context of incoming message
|
||||||
if extra_system_prompt is not None:
|
if extra_system_prompt is not None:
|
||||||
self._extra_system_prompt = extra_system_prompt
|
self._extra_system_prompt = extra_system_prompt
|
||||||
@ -280,6 +295,7 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self._is_announcing = False
|
self._is_announcing = False
|
||||||
|
self._set_state(AssistSatelliteState.IDLE)
|
||||||
|
|
||||||
async def async_start_conversation(
|
async def async_start_conversation(
|
||||||
self, start_announcement: AssistSatelliteAnnouncement
|
self, start_announcement: AssistSatelliteAnnouncement
|
||||||
@ -470,7 +486,10 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
return vad.VadSensitivity.to_seconds(vad_sensitivity)
|
return vad.VadSensitivity.to_seconds(vad_sensitivity)
|
||||||
|
|
||||||
async def _resolve_announcement_media_id(
|
async def _resolve_announcement_media_id(
|
||||||
self, message: str, media_id: str | None
|
self,
|
||||||
|
message: str,
|
||||||
|
media_id: str | None,
|
||||||
|
preannounce_media_id: str | None = None,
|
||||||
) -> AssistSatelliteAnnouncement:
|
) -> AssistSatelliteAnnouncement:
|
||||||
"""Resolve the media ID."""
|
"""Resolve the media ID."""
|
||||||
media_id_source: Literal["url", "media_id", "tts"] | None = None
|
media_id_source: Literal["url", "media_id", "tts"] | None = None
|
||||||
@ -478,7 +497,6 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
|
|
||||||
if media_id:
|
if media_id:
|
||||||
original_media_id = media_id
|
original_media_id = media_id
|
||||||
|
|
||||||
else:
|
else:
|
||||||
media_id_source = "tts"
|
media_id_source = "tts"
|
||||||
# Synthesize audio and get URL
|
# Synthesize audio and get URL
|
||||||
@ -530,10 +548,26 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
# Resolve to full URL
|
# Resolve to full URL
|
||||||
media_id = async_process_play_media_url(self.hass, media_id)
|
media_id = async_process_play_media_url(self.hass, media_id)
|
||||||
|
|
||||||
|
# Resolve preannounce media id
|
||||||
|
if preannounce_media_id:
|
||||||
|
if media_source.is_media_source_id(preannounce_media_id):
|
||||||
|
preannounce_media = await media_source.async_resolve_media(
|
||||||
|
self.hass,
|
||||||
|
preannounce_media_id,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
preannounce_media_id = preannounce_media.url
|
||||||
|
|
||||||
|
# Resolve to full URL
|
||||||
|
preannounce_media_id = async_process_play_media_url(
|
||||||
|
self.hass, preannounce_media_id
|
||||||
|
)
|
||||||
|
|
||||||
return AssistSatelliteAnnouncement(
|
return AssistSatelliteAnnouncement(
|
||||||
message=message,
|
message=message,
|
||||||
media_id=media_id,
|
media_id=media_id,
|
||||||
original_media_id=original_media_id,
|
original_media_id=original_media_id,
|
||||||
tts_token=tts_token,
|
tts_token=tts_token,
|
||||||
media_id_source=media_id_source,
|
media_id_source=media_id_source,
|
||||||
|
preannounce_media_id=preannounce_media_id,
|
||||||
)
|
)
|
||||||
|
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
Binary file not shown.
@ -8,12 +8,17 @@ announce:
|
|||||||
message:
|
message:
|
||||||
required: false
|
required: false
|
||||||
example: "Time to wake up!"
|
example: "Time to wake up!"
|
||||||
|
default: ""
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
media_id:
|
media_id:
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
preannounce_media_id:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
start_conversation:
|
start_conversation:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
@ -24,6 +29,7 @@ start_conversation:
|
|||||||
start_message:
|
start_message:
|
||||||
required: false
|
required: false
|
||||||
example: "You left the lights on in the living room. Turn them off?"
|
example: "You left the lights on in the living room. Turn them off?"
|
||||||
|
default: ""
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
start_media_id:
|
start_media_id:
|
||||||
@ -34,3 +40,7 @@ start_conversation:
|
|||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
preannounce_media_id:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
@ -23,6 +23,10 @@
|
|||||||
"media_id": {
|
"media_id": {
|
||||||
"name": "Media ID",
|
"name": "Media ID",
|
||||||
"description": "The media ID to announce instead of using text-to-speech."
|
"description": "The media ID to announce instead of using text-to-speech."
|
||||||
|
},
|
||||||
|
"preannounce_media_id": {
|
||||||
|
"name": "Preannounce Media ID",
|
||||||
|
"description": "The media ID to play before the announcement."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -41,6 +45,10 @@
|
|||||||
"extra_system_prompt": {
|
"extra_system_prompt": {
|
||||||
"name": "Extra system prompt",
|
"name": "Extra system prompt",
|
||||||
"description": "Provide background information to the AI about the request."
|
"description": "Provide background information to the AI about the request."
|
||||||
|
},
|
||||||
|
"preannounce_media_id": {
|
||||||
|
"name": "Preannounce Media ID",
|
||||||
|
"description": "The media ID to play before the start message or media."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,8 @@ async def websocket_test_connection(
|
|||||||
|
|
||||||
hass.async_create_background_task(
|
hass.async_create_background_task(
|
||||||
satellite.async_internal_announce(
|
satellite.async_internal_announce(
|
||||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}"
|
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
|
||||||
|
preannounce_media_id=None,
|
||||||
),
|
),
|
||||||
f"assist_satellite_connection_test_{msg['entity_id']}",
|
f"assist_satellite_connection_test_{msg['entity_id']}",
|
||||||
)
|
)
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiobotocore", "botocore"],
|
"loggers": ["aiobotocore", "botocore"],
|
||||||
"quality_scale": "legacy",
|
"quality_scale": "legacy",
|
||||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
"requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"""The Backup integration."""
|
"""The Backup integration."""
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_SYSTEM
|
||||||
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||||
from homeassistant.helpers.backup import DATA_BACKUP
|
from homeassistant.helpers.backup import DATA_BACKUP
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -18,10 +20,12 @@ from .agent import (
|
|||||||
)
|
)
|
||||||
from .config import BackupConfig, CreateBackupParametersDict
|
from .config import BackupConfig, CreateBackupParametersDict
|
||||||
from .const import DATA_MANAGER, DOMAIN
|
from .const import DATA_MANAGER, DOMAIN
|
||||||
|
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
|
||||||
from .http import async_register_http_views
|
from .http import async_register_http_views
|
||||||
from .manager import (
|
from .manager import (
|
||||||
BackupManager,
|
BackupManager,
|
||||||
BackupManagerError,
|
BackupManagerError,
|
||||||
|
BackupPlatformEvent,
|
||||||
BackupPlatformProtocol,
|
BackupPlatformProtocol,
|
||||||
BackupReaderWriter,
|
BackupReaderWriter,
|
||||||
BackupReaderWriterError,
|
BackupReaderWriterError,
|
||||||
@ -52,6 +56,7 @@ __all__ = [
|
|||||||
"BackupConfig",
|
"BackupConfig",
|
||||||
"BackupManagerError",
|
"BackupManagerError",
|
||||||
"BackupNotFound",
|
"BackupNotFound",
|
||||||
|
"BackupPlatformEvent",
|
||||||
"BackupPlatformProtocol",
|
"BackupPlatformProtocol",
|
||||||
"BackupReaderWriter",
|
"BackupReaderWriter",
|
||||||
"BackupReaderWriterError",
|
"BackupReaderWriterError",
|
||||||
@ -74,6 +79,8 @@ __all__ = [
|
|||||||
"suggested_filename_from_name_date",
|
"suggested_filename_from_name_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
@ -128,4 +135,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async_register_http_views(hass)
|
async_register_http_views(hass)
|
||||||
|
|
||||||
|
discovery_flow.async_create_flow(
|
||||||
|
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
backup_manager: BackupManager = hass.data[DATA_MANAGER]
|
||||||
|
coordinator = BackupDataUpdateCoordinator(hass, entry, backup_manager)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_unsubscribe)
|
||||||
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
21
homeassistant/components/backup/config_flow.py
Normal file
21
homeassistant/components/backup/config_flow.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
"""Config flow for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class BackupConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Home Assistant Backup."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_system(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
return self.async_create_entry(title="Backup", data={})
|
@ -16,8 +16,8 @@ DATA_MANAGER: HassKey[BackupManager] = HassKey(DOMAIN)
|
|||||||
LOGGER = getLogger(__package__)
|
LOGGER = getLogger(__package__)
|
||||||
|
|
||||||
EXCLUDE_FROM_BACKUP = [
|
EXCLUDE_FROM_BACKUP = [
|
||||||
"__pycache__/*",
|
"**/__pycache__/*",
|
||||||
".DS_Store",
|
"**/.DS_Store",
|
||||||
".HA_RESTORE",
|
".HA_RESTORE",
|
||||||
"*.db-shm",
|
"*.db-shm",
|
||||||
"*.log.*",
|
"*.log.*",
|
||||||
|
81
homeassistant/components/backup/coordinator.py
Normal file
81
homeassistant/components/backup/coordinator.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""Coordinator for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.backup import (
|
||||||
|
async_subscribe_events,
|
||||||
|
async_subscribe_platform_events,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
from .manager import (
|
||||||
|
BackupManager,
|
||||||
|
BackupManagerState,
|
||||||
|
BackupPlatformEvent,
|
||||||
|
ManagerStateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackupConfigEntry = ConfigEntry[BackupDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BackupCoordinatorData:
|
||||||
|
"""Class to hold backup data."""
|
||||||
|
|
||||||
|
backup_manager_state: BackupManagerState
|
||||||
|
last_successful_automatic_backup: datetime | None
|
||||||
|
next_scheduled_automatic_backup: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
||||||
|
"""Class to retrieve backup status."""
|
||||||
|
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
backup_manager: BackupManager,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=None,
|
||||||
|
)
|
||||||
|
self.unsubscribe: list[Callable[[], None]] = [
|
||||||
|
async_subscribe_events(hass, self._on_event),
|
||||||
|
async_subscribe_platform_events(hass, self._on_event),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.backup_manager = backup_manager
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
|
||||||
|
"""Handle new event."""
|
||||||
|
LOGGER.debug("Received backup event: %s", event)
|
||||||
|
self.config_entry.async_create_task(self.hass, self.async_refresh())
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> BackupCoordinatorData:
|
||||||
|
"""Update backup manager data."""
|
||||||
|
return BackupCoordinatorData(
|
||||||
|
self.backup_manager.state,
|
||||||
|
self.backup_manager.config.data.last_completed_automatic_backup,
|
||||||
|
self.backup_manager.config.data.schedule.next_automatic_backup,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_unsubscribe(self) -> None:
|
||||||
|
"""Unsubscribe from events."""
|
||||||
|
for unsub in self.unsubscribe:
|
||||||
|
unsub()
|
27
homeassistant/components/backup/diagnostics.py
Normal file
27
homeassistant/components/backup/diagnostics.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""Diagnostics support for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import BackupConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: BackupConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
return {
|
||||||
|
"backup_agents": [
|
||||||
|
{"name": agent.name, "agent_id": agent.agent_id}
|
||||||
|
for agent in coordinator.backup_manager.backup_agents.values()
|
||||||
|
],
|
||||||
|
"backup_config": async_redact_data(
|
||||||
|
coordinator.backup_manager.config.data.to_dict(), [CONF_PASSWORD]
|
||||||
|
),
|
||||||
|
}
|
36
homeassistant/components/backup/entity.py
Normal file
36
homeassistant/components/backup/entity.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Base for backup entities."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import __version__ as HA_VERSION
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import BackupDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
|
||||||
|
"""Base entity for backup manager."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BackupDataUpdateCoordinator,
|
||||||
|
entity_description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize base entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = entity_description.key
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, "backup_manager")},
|
||||||
|
manufacturer="Home Assistant",
|
||||||
|
model="Home Assistant Backup",
|
||||||
|
sw_version=HA_VERSION,
|
||||||
|
name="Backup",
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
configuration_url="homeassistant://config/backup",
|
||||||
|
)
|
@ -229,6 +229,13 @@ class RestoreBackupEvent(ManagerStateEvent):
|
|||||||
state: RestoreBackupState
|
state: RestoreBackupState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||||
|
class BackupPlatformEvent:
|
||||||
|
"""Backup platform class."""
|
||||||
|
|
||||||
|
domain: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||||
class BlockedEvent(ManagerStateEvent):
|
class BlockedEvent(ManagerStateEvent):
|
||||||
"""Backup manager blocked, Home Assistant is starting."""
|
"""Backup manager blocked, Home Assistant is starting."""
|
||||||
@ -355,6 +362,9 @@ class BackupManager:
|
|||||||
self._backup_event_subscriptions = hass.data[
|
self._backup_event_subscriptions = hass.data[
|
||||||
DATA_BACKUP
|
DATA_BACKUP
|
||||||
].backup_event_subscriptions
|
].backup_event_subscriptions
|
||||||
|
self._backup_platform_event_subscriptions = hass.data[
|
||||||
|
DATA_BACKUP
|
||||||
|
].backup_platform_event_subscriptions
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Set up the backup manager."""
|
"""Set up the backup manager."""
|
||||||
@ -465,6 +475,9 @@ class BackupManager:
|
|||||||
LOGGER.debug("%s platforms loaded in total", len(self.platforms))
|
LOGGER.debug("%s platforms loaded in total", len(self.platforms))
|
||||||
LOGGER.debug("%s agents loaded in total", len(self.backup_agents))
|
LOGGER.debug("%s agents loaded in total", len(self.backup_agents))
|
||||||
LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents))
|
LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents))
|
||||||
|
event = BackupPlatformEvent(domain=integration_domain)
|
||||||
|
for subscription in self._backup_platform_event_subscriptions:
|
||||||
|
subscription(event)
|
||||||
|
|
||||||
async def async_pre_backup_actions(self) -> None:
|
async def async_pre_backup_actions(self) -> None:
|
||||||
"""Perform pre backup actions."""
|
"""Perform pre backup actions."""
|
||||||
@ -1713,7 +1726,9 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
|||||||
"""Filter to filter excludes."""
|
"""Filter to filter excludes."""
|
||||||
|
|
||||||
for exclude in excludes:
|
for exclude in excludes:
|
||||||
if not path.match(exclude):
|
# The home assistant core configuration directory is added as "data"
|
||||||
|
# in the tar file, so we need to prefix that path to the filters.
|
||||||
|
if not path.full_match(f"data/{exclude}"):
|
||||||
continue
|
continue
|
||||||
LOGGER.debug("Ignoring %s because of %s", path, exclude)
|
LOGGER.debug("Ignoring %s because of %s", path, exclude)
|
||||||
return True
|
return True
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["http", "websocket_api"],
|
"dependencies": ["http", "websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/backup",
|
"documentation": "https://www.home-assistant.io/integrations/backup",
|
||||||
"integration_type": "system",
|
"integration_type": "service",
|
||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["cronsim==2.6", "securetar==2025.2.1"]
|
"requirements": ["cronsim==2.6", "securetar==2025.2.1"],
|
||||||
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
75
homeassistant/components/backup/sensor.py
Normal file
75
homeassistant/components/backup/sensor.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""Sensor platform for Home Assistant Backup integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import BackupConfigEntry, BackupCoordinatorData
|
||||||
|
from .entity import BackupManagerEntity
|
||||||
|
from .manager import BackupManagerState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class BackupSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Description for Home Assistant Backup sensor entities."""
|
||||||
|
|
||||||
|
value_fn: Callable[[BackupCoordinatorData], str | datetime | None]
|
||||||
|
|
||||||
|
|
||||||
|
BACKUP_MANAGER_DESCRIPTIONS = (
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="backup_manager_state",
|
||||||
|
translation_key="backup_manager_state",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[state.value for state in BackupManagerState],
|
||||||
|
value_fn=lambda data: data.backup_manager_state,
|
||||||
|
),
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="next_scheduled_automatic_backup",
|
||||||
|
translation_key="next_scheduled_automatic_backup",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda data: data.next_scheduled_automatic_backup,
|
||||||
|
),
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="last_successful_automatic_backup",
|
||||||
|
translation_key="last_successful_automatic_backup",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda data: data.last_successful_automatic_backup,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BackupConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Sensor set up for backup config entry."""
|
||||||
|
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
BackupManagerSensor(coordinator, description)
|
||||||
|
for description in BACKUP_MANAGER_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BackupManagerSensor(BackupManagerEntity, SensorEntity):
|
||||||
|
"""Sensor to track backup manager state."""
|
||||||
|
|
||||||
|
entity_description: BackupSensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> str | datetime | None:
|
||||||
|
"""Return native value of entity."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
@ -22,5 +22,24 @@
|
|||||||
"name": "Create automatic backup",
|
"name": "Create automatic backup",
|
||||||
"description": "Creates a new backup with automatic backup settings."
|
"description": "Creates a new backup with automatic backup settings."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"backup_manager_state": {
|
||||||
|
"name": "Backup Manager state",
|
||||||
|
"state": {
|
||||||
|
"idle": "[%key:common::state::idle%]",
|
||||||
|
"create_backup": "Creating a backup",
|
||||||
|
"receive_backup": "Receiving a backup",
|
||||||
|
"restore_backup": "Restoring a backup"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"next_scheduled_automatic_backup": {
|
||||||
|
"name": "Next scheduled automatic backup"
|
||||||
|
},
|
||||||
|
"last_successful_automatic_backup": {
|
||||||
|
"name": "Last successful automatic backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
homeassistant/components/balay/__init__.py
Normal file
1
homeassistant/components/balay/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Balay virtual integration."""
|
6
homeassistant/components/balay/manifest.json
Normal file
6
homeassistant/components/balay/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"domain": "balay",
|
||||||
|
"name": "Balay",
|
||||||
|
"integration_type": "virtual",
|
||||||
|
"supported_by": "home_connect"
|
||||||
|
}
|
@ -132,7 +132,7 @@
|
|||||||
"name": "Charging",
|
"name": "Charging",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Not charging",
|
"off": "Not charging",
|
||||||
"on": "Charging"
|
"on": "[%key:common::state::charging%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"carbon_monoxide": {
|
"carbon_monoxide": {
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
"vehicle_status": {
|
"vehicle_status": {
|
||||||
"name": "Vehicle status",
|
"name": "Vehicle status",
|
||||||
"state": {
|
"state": {
|
||||||
"standby": "Standby",
|
"standby": "[%key:common::state::standby%]",
|
||||||
"vehicle_detected": "Detected",
|
"vehicle_detected": "Detected",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"no_power": "No power",
|
"no_power": "No power",
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"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%]",
|
||||||
"region": "ConnectedDrive Region"
|
"region": "ConnectedDrive region"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"username": "The email address of your MyBMW/MINI Connected account.",
|
"username": "The email address of your MyBMW/MINI Connected account.",
|
||||||
@ -113,10 +113,10 @@
|
|||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"ac_limit": {
|
"ac_limit": {
|
||||||
"name": "AC Charging Limit"
|
"name": "AC charging limit"
|
||||||
},
|
},
|
||||||
"charging_mode": {
|
"charging_mode": {
|
||||||
"name": "Charging Mode",
|
"name": "Charging mode",
|
||||||
"state": {
|
"state": {
|
||||||
"immediate_charging": "Immediate charging",
|
"immediate_charging": "Immediate charging",
|
||||||
"delayed_charging": "Delayed charging",
|
"delayed_charging": "Delayed charging",
|
||||||
@ -181,7 +181,7 @@
|
|||||||
"cooling": "Cooling",
|
"cooling": "Cooling",
|
||||||
"heating": "Heating",
|
"heating": "Heating",
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
"standby": "Standby",
|
"standby": "[%key:common::state::standby%]",
|
||||||
"ventilation": "Ventilation"
|
"ventilation": "Ventilation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
62
homeassistant/components/bosch_alarm/__init__.py
Normal file
62
homeassistant/components/bosch_alarm/__init__.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""The Bosch Alarm integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ssl import SSLError
|
||||||
|
|
||||||
|
from bosch_alarm_mode2 import Panel
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL]
|
||||||
|
|
||||||
|
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
|
||||||
|
"""Set up Bosch Alarm from a config entry."""
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
host=entry.data[CONF_HOST],
|
||||||
|
port=entry.data[CONF_PORT],
|
||||||
|
automation_code=entry.data.get(CONF_PASSWORD),
|
||||||
|
installer_or_user_code=entry.data.get(
|
||||||
|
CONF_INSTALLER_CODE, entry.data.get(CONF_USER_CODE)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await panel.connect()
|
||||||
|
except (PermissionError, ValueError) as err:
|
||||||
|
await panel.disconnect()
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
|
||||||
|
await panel.disconnect()
|
||||||
|
raise ConfigEntryNotReady("Connection failed") from err
|
||||||
|
|
||||||
|
entry.runtime_data = panel
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
|
||||||
|
name=f"Bosch {panel.model}",
|
||||||
|
manufacturer="Bosch Security Systems",
|
||||||
|
model=panel.model,
|
||||||
|
sw_version=panel.firmware_version,
|
||||||
|
)
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
await entry.runtime_data.disconnect()
|
||||||
|
return unload_ok
|
109
homeassistant/components/bosch_alarm/alarm_control_panel.py
Normal file
109
homeassistant/components/bosch_alarm/alarm_control_panel.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"""Support for Bosch Alarm Panel."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bosch_alarm_mode2 import Panel
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import (
|
||||||
|
AlarmControlPanelEntity,
|
||||||
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import BoschAlarmConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BoschAlarmConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up control panels for each area."""
|
||||||
|
panel = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AreaAlarmControlPanel(
|
||||||
|
panel,
|
||||||
|
area_id,
|
||||||
|
config_entry.unique_id or config_entry.entry_id,
|
||||||
|
)
|
||||||
|
for area_id in panel.areas
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AreaAlarmControlPanel(AlarmControlPanelEntity):
|
||||||
|
"""An alarm control panel entity for a bosch alarm panel."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_supported_features = (
|
||||||
|
AlarmControlPanelEntityFeature.ARM_HOME
|
||||||
|
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||||
|
)
|
||||||
|
_attr_code_arm_required = False
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
|
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||||
|
"""Initialise a Bosch Alarm control panel entity."""
|
||||||
|
self.panel = panel
|
||||||
|
self._area = panel.areas[area_id]
|
||||||
|
self._area_id = area_id
|
||||||
|
self._attr_unique_id = f"{unique_id}_area_{area_id}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||||
|
name=self._area.name,
|
||||||
|
manufacturer="Bosch Security Systems",
|
||||||
|
via_device=(
|
||||||
|
DOMAIN,
|
||||||
|
unique_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
|
"""Return the state of the alarm."""
|
||||||
|
if self._area.is_triggered():
|
||||||
|
return AlarmControlPanelState.TRIGGERED
|
||||||
|
if self._area.is_disarmed():
|
||||||
|
return AlarmControlPanelState.DISARMED
|
||||||
|
if self._area.is_arming():
|
||||||
|
return AlarmControlPanelState.ARMING
|
||||||
|
if self._area.is_pending():
|
||||||
|
return AlarmControlPanelState.PENDING
|
||||||
|
if self._area.is_part_armed():
|
||||||
|
return AlarmControlPanelState.ARMED_HOME
|
||||||
|
if self._area.is_all_armed():
|
||||||
|
return AlarmControlPanelState.ARMED_AWAY
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||||
|
"""Disarm this panel."""
|
||||||
|
await self.panel.area_disarm(self._area_id)
|
||||||
|
|
||||||
|
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||||
|
"""Send arm home command."""
|
||||||
|
await self.panel.area_arm_part(self._area_id)
|
||||||
|
|
||||||
|
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
|
"""Send arm away command."""
|
||||||
|
await self.panel.area_arm_all(self._area_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.panel.connection_status()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when entity attached to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._area.status_observer.attach(self.schedule_update_ha_state)
|
||||||
|
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Run when entity removed from hass."""
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
self._area.status_observer.detach(self.schedule_update_ha_state)
|
||||||
|
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
|
165
homeassistant/components/bosch_alarm/config_flow.py
Normal file
165
homeassistant/components/bosch_alarm/config_flow.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
"""Config flow for Bosch Alarm integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import ssl
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from bosch_alarm_mode2 import Panel
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_PORT, default=7700): cv.positive_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
STEP_AUTH_DATA_SCHEMA_SOLUTION = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USER_CODE): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
STEP_AUTH_DATA_SCHEMA_AMAX = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_INSTALLER_CODE): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
STEP_AUTH_DATA_SCHEMA_BG = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
STEP_INIT_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_CODE): str})
|
||||||
|
|
||||||
|
|
||||||
|
async def try_connect(
|
||||||
|
data: dict[str, Any], load_selector: int = 0
|
||||||
|
) -> tuple[str, int | None]:
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
panel = Panel(
|
||||||
|
host=data[CONF_HOST],
|
||||||
|
port=data[CONF_PORT],
|
||||||
|
automation_code=data.get(CONF_PASSWORD),
|
||||||
|
installer_or_user_code=data.get(CONF_INSTALLER_CODE, data.get(CONF_USER_CODE)),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await panel.connect(load_selector)
|
||||||
|
finally:
|
||||||
|
await panel.disconnect()
|
||||||
|
|
||||||
|
return (panel.model, panel.serial_number)
|
||||||
|
|
||||||
|
|
||||||
|
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Bosch Alarm."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Init config flow."""
|
||||||
|
|
||||||
|
self._data: dict[str, Any] = {}
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
# Use load_selector = 0 to fetch the panel model without authentication.
|
||||||
|
(model, serial) = await try_connect(user_input, 0)
|
||||||
|
except (
|
||||||
|
OSError,
|
||||||
|
ConnectionRefusedError,
|
||||||
|
ssl.SSLError,
|
||||||
|
asyncio.exceptions.TimeoutError,
|
||||||
|
) as e:
|
||||||
|
_LOGGER.error("Connection Error: %s", e)
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
self._data = user_input
|
||||||
|
self._data[CONF_MODEL] = model
|
||||||
|
return await self.async_step_auth()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
STEP_USER_DATA_SCHEMA, user_input
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_auth(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the auth step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
# Each model variant requires a different authentication flow
|
||||||
|
if "Solution" in self._data[CONF_MODEL]:
|
||||||
|
schema = STEP_AUTH_DATA_SCHEMA_SOLUTION
|
||||||
|
elif "AMAX" in self._data[CONF_MODEL]:
|
||||||
|
schema = STEP_AUTH_DATA_SCHEMA_AMAX
|
||||||
|
else:
|
||||||
|
schema = STEP_AUTH_DATA_SCHEMA_BG
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self._data.update(user_input)
|
||||||
|
try:
|
||||||
|
(model, serial_number) = await try_connect(
|
||||||
|
self._data, Panel.LOAD_EXTENDED_INFO
|
||||||
|
)
|
||||||
|
except (PermissionError, ValueError) as e:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
_LOGGER.error("Authentication Error: %s", e)
|
||||||
|
except (
|
||||||
|
OSError,
|
||||||
|
ConnectionRefusedError,
|
||||||
|
ssl.SSLError,
|
||||||
|
TimeoutError,
|
||||||
|
) as e:
|
||||||
|
_LOGGER.error("Connection Error: %s", e)
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
if serial_number:
|
||||||
|
await self.async_set_unique_id(str(serial_number))
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
else:
|
||||||
|
self._async_abort_entries_match({CONF_HOST: self._data[CONF_HOST]})
|
||||||
|
return self.async_create_entry(title=f"Bosch {model}", data=self._data)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="auth",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
||||||
|
errors=errors,
|
||||||
|
)
|
6
homeassistant/components/bosch_alarm/const.py
Normal file
6
homeassistant/components/bosch_alarm/const.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""Constants for the Bosch Alarm integration."""
|
||||||
|
|
||||||
|
DOMAIN = "bosch_alarm"
|
||||||
|
HISTORY_ATTR = "history"
|
||||||
|
CONF_INSTALLER_CODE = "installer_code"
|
||||||
|
CONF_USER_CODE = "user_code"
|
11
homeassistant/components/bosch_alarm/manifest.json
Normal file
11
homeassistant/components/bosch_alarm/manifest.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"domain": "bosch_alarm",
|
||||||
|
"name": "Bosch Alarm",
|
||||||
|
"codeowners": ["@mag1024", "@sanjay900"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["bosch-alarm-mode2==0.4.3"]
|
||||||
|
}
|
84
homeassistant/components/bosch_alarm/quality_scale.yaml
Normal file
84
homeassistant/components/bosch_alarm/quality_scale.yaml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions defined
|
||||||
|
appropriate-polling:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No polling
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup: done
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: todo
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
entity-unavailable: todo
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: todo
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class: todo
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: todo
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: todo
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No repairs
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Integration does not make any HTTP requests.
|
||||||
|
strict-typing: done
|
36
homeassistant/components/bosch_alarm/strings.json
Normal file
36
homeassistant/components/bosch_alarm/strings.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "The hostname or IP address of your Bosch alarm panel",
|
||||||
|
"port": "The port used to connect to your Bosch alarm panel. This is usually 7700"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"installer_code": "Installer code",
|
||||||
|
"user_code": "User code"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "The Mode 2 automation code from your panel",
|
||||||
|
"installer_code": "The installer code from your panel",
|
||||||
|
"user_code": "The user code from your panel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -170,6 +170,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
|||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
icon="mdi:compass-outline",
|
icon="mdi:compass-outline",
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key="pressure",
|
key="pressure",
|
||||||
|
@ -142,6 +142,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
|||||||
@property
|
@property
|
||||||
def media_artist(self) -> str | None:
|
def media_artist(self) -> str | None:
|
||||||
"""Artist of current playing media, music track only."""
|
"""Artist of current playing media, music track only."""
|
||||||
|
if (
|
||||||
|
not self.client.play_state.metadata.artist
|
||||||
|
and self.client.state.source == "IR"
|
||||||
|
):
|
||||||
|
# Return channel instead of artist when playing internet radio
|
||||||
|
return self.client.play_state.metadata.station
|
||||||
return self.client.play_state.metadata.artist
|
return self.client.play_state.metadata.artist
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -169,6 +175,11 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
|||||||
"""Last time the media position was updated."""
|
"""Last time the media position was updated."""
|
||||||
return self.client.position_last_updated
|
return self.client.position_last_updated
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_channel(self) -> str | None:
|
||||||
|
"""Channel currently playing."""
|
||||||
|
return self.client.play_state.metadata.station
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_volume_muted(self) -> bool | None:
|
def is_volume_muted(self) -> bool | None:
|
||||||
"""Volume mute status."""
|
"""Volume mute status."""
|
||||||
|
@ -81,7 +81,7 @@ class ChromecastInfo:
|
|||||||
"+label%3A%22integration%3A+cast%22"
|
"+label%3A%22integration%3A+cast%22"
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.info(
|
||||||
(
|
(
|
||||||
"Fetched cast details for unknown model '%s' manufacturer:"
|
"Fetched cast details for unknown model '%s' manufacturer:"
|
||||||
" '%s', type: '%s'. Please %s"
|
" '%s', type: '%s'. Please %s"
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["casttube", "pychromecast"],
|
"loggers": ["casttube", "pychromecast"],
|
||||||
"requirements": ["PyChromecast==14.0.6"],
|
"requirements": ["PyChromecast==14.0.7"],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"zeroconf": ["_googlecast._tcp.local."]
|
"zeroconf": ["_googlecast._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except DIOChaconInvalidAuthError:
|
except DIOChaconInvalidAuthError:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@
|
|||||||
"selector": {
|
"selector": {
|
||||||
"hvac_mode": {
|
"hvac_mode": {
|
||||||
"options": {
|
"options": {
|
||||||
"off": "Off",
|
"off": "[%key:common::state::off%]",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cool": "Cool",
|
"cool": "Cool",
|
||||||
"dry": "Dry",
|
"dry": "Dry",
|
||||||
|
@ -4,13 +4,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||||
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
from hass_nabucasa import Cloud, CloudError
|
from hass_nabucasa import Cloud, CloudError
|
||||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
|
||||||
from hass_nabucasa.cloud_api import (
|
from hass_nabucasa.cloud_api import (
|
||||||
FilesHandlerListEntry,
|
FilesHandlerListEntry,
|
||||||
async_files_delete_file,
|
async_files_delete_file,
|
||||||
@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
"""
|
"""
|
||||||
if not backup.protected:
|
if not backup.protected:
|
||||||
raise BackupAgentError("Cloud backups must be protected")
|
raise BackupAgentError("Cloud backups must be protected")
|
||||||
|
if self._cloud.subscription_expired:
|
||||||
|
raise BackupAgentError("Cloud subscription has expired")
|
||||||
|
|
||||||
size = backup.size
|
size = backup.size
|
||||||
try:
|
try:
|
||||||
@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
) from err
|
) from err
|
||||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||||
except CloudError as err:
|
except CloudError as err:
|
||||||
|
if (
|
||||||
|
isinstance(err, CloudApiError)
|
||||||
|
and isinstance(err.orig_exc, ClientResponseError)
|
||||||
|
and err.orig_exc.status == HTTPStatus.FORBIDDEN
|
||||||
|
and self._cloud.subscription_expired
|
||||||
|
):
|
||||||
|
raise BackupAgentError("Cloud subscription has expired") from err
|
||||||
if tries == _RETRY_LIMIT:
|
if tries == _RETRY_LIMIT:
|
||||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||||
tries += 1
|
tries += 1
|
||||||
|
@ -9,7 +9,6 @@ from typing import Any
|
|||||||
import pycfdns
|
import pycfdns
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
|
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -118,8 +117,6 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle a flow initiated by the user."""
|
"""Handle a flow initiated by the user."""
|
||||||
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
|
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
@ -4,19 +4,19 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Connect to Cloudflare",
|
"title": "Connect to Cloudflare",
|
||||||
"description": "This integration requires an API Token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.",
|
"description": "This integration requires an API token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zone": {
|
"zone": {
|
||||||
"title": "Choose the Zone to Update",
|
"title": "Choose the zone to update",
|
||||||
"data": {
|
"data": {
|
||||||
"zone": "Zone"
|
"zone": "Zone"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"records": {
|
"records": {
|
||||||
"title": "Choose the Records to Update",
|
"title": "Choose the records to update",
|
||||||
"data": {
|
"data": {
|
||||||
"records": "Records"
|
"records": "Records"
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"update_records": {
|
"update_records": {
|
||||||
"name": "Update records",
|
"name": "Update records",
|
||||||
"description": "Manually trigger update to Cloudflare records."
|
"description": "Manually triggers an update of Cloudflare records."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
|
|||||||
|
|
||||||
|
|
||||||
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
|
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
|
||||||
|
DISABLE: 0,
|
||||||
HOME_P1: 1,
|
HOME_P1: 1,
|
||||||
HOME_P2: 2,
|
HOME_P2: 2,
|
||||||
NIGHT: 3,
|
NIGHT: 3,
|
||||||
@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
|
|||||||
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
||||||
}.get(self._area.human_status)
|
}.get(self._area.human_status)
|
||||||
|
|
||||||
|
async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None:
|
||||||
|
"""Update state after action."""
|
||||||
|
self._area.human_status = area_state
|
||||||
|
self._area.armed = armed
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if code != str(self._api.device_pin):
|
if code != str(self._api.device_pin):
|
||||||
return
|
return
|
||||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
|
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
|
||||||
|
await self._async_update_state(
|
||||||
|
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||||
|
)
|
||||||
|
|
||||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
|
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
|
||||||
|
await self._async_update_state(
|
||||||
|
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||||
|
)
|
||||||
|
|
||||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
|
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
|
||||||
|
await self._async_update_state(
|
||||||
|
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||||
|
)
|
||||||
|
|
||||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||||
"""Send arm night command."""
|
"""Send arm night command."""
|
||||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
|
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
|
||||||
|
await self._async_update_state(
|
||||||
|
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||||
|
)
|
||||||
|
@ -9,3 +9,5 @@ _LOGGER = logging.getLogger(__package__)
|
|||||||
DOMAIN = "comelit"
|
DOMAIN = "comelit"
|
||||||
DEFAULT_PORT = 80
|
DEFAULT_PORT = 80
|
||||||
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
|
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
|
||||||
|
|
||||||
|
SCAN_INTERVAL = 5
|
||||||
|
@ -22,7 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
|
||||||
|
|
||||||
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
|
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
|||||||
logger=_LOGGER,
|
logger=_LOGGER,
|
||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
name=f"{DOMAIN}-{host}-coordinator",
|
name=f"{DOMAIN}-{host}-coordinator",
|
||||||
update_interval=timedelta(seconds=5),
|
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
device_registry = dr.async_get(self.hass)
|
device_registry = dr.async_get(self.hass)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
|
@ -8,7 +8,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
|||||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
@ -98,13 +98,20 @@ class ComelitCoverEntity(
|
|||||||
"""Return if the cover is opening."""
|
"""Return if the cover is opening."""
|
||||||
return self._current_action("opening")
|
return self._current_action("opening")
|
||||||
|
|
||||||
|
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||||
|
"""Set desired cover state."""
|
||||||
|
self._last_state = self.state
|
||||||
|
await self._api.set_device_status(COVER, self._device.index, action)
|
||||||
|
self.coordinator.data[COVER][self._device.index].status = state
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||||
"""Close cover."""
|
"""Close cover."""
|
||||||
await self._api.set_device_status(COVER, self._device.index, STATE_OFF)
|
await self._cover_set_state(STATE_OFF, 2)
|
||||||
|
|
||||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||||
"""Open cover."""
|
"""Open cover."""
|
||||||
await self._api.set_device_status(COVER, self._device.index, STATE_ON)
|
await self._cover_set_state(STATE_ON, 1)
|
||||||
|
|
||||||
async def async_stop_cover(self, **_kwargs: Any) -> None:
|
async def async_stop_cover(self, **_kwargs: Any) -> None:
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
@ -112,13 +119,7 @@ class ComelitCoverEntity(
|
|||||||
return
|
return
|
||||||
|
|
||||||
action = STATE_ON if self.is_closing else STATE_OFF
|
action = STATE_ON if self.is_closing else STATE_OFF
|
||||||
await self._api.set_device_status(COVER, self._device.index, action)
|
await self._cover_set_state(action, 0)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
|
||||||
"""Handle device update."""
|
|
||||||
self._last_state = self.state
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle entity which will be added."""
|
"""Handle entity which will be added."""
|
||||||
|
@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
|
|||||||
async def _light_set_state(self, state: int) -> None:
|
async def _light_set_state(self, state: int) -> None:
|
||||||
"""Set desired light state."""
|
"""Set desired light state."""
|
||||||
await self.coordinator.api.set_device_status(LIGHT, self._device.index, state)
|
await self.coordinator.api.set_device_status(LIGHT, self._device.index, state)
|
||||||
await self.coordinator.async_request_refresh()
|
self.coordinator.data[LIGHT][self._device.index].status = state
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the light on."""
|
"""Turn the light on."""
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiocomelit"],
|
"loggers": ["aiocomelit"],
|
||||||
"requirements": ["aiocomelit==0.11.2"]
|
"requirements": ["aiocomelit==0.11.3"]
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
|
|||||||
await self.coordinator.api.set_device_status(
|
await self.coordinator.api.set_device_status(
|
||||||
self._device.type, self._device.index, state
|
self._device.type, self._device.index, state
|
||||||
)
|
)
|
||||||
await self.coordinator.async_request_refresh()
|
self.coordinator.data[self._device.type][self._device.index].status = state
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
|
1
homeassistant/components/constructa/__init__.py
Normal file
1
homeassistant/components/constructa/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Constructa virtual integration."""
|
6
homeassistant/components/constructa/manifest.json
Normal file
6
homeassistant/components/constructa/manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"domain": "constructa",
|
||||||
|
"name": "Constructa",
|
||||||
|
"integration_type": "virtual",
|
||||||
|
"supported_by": "home_connect"
|
||||||
|
}
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from hassil.recognize import RecognizeResult
|
from hassil.recognize import RecognizeResult
|
||||||
@ -91,8 +90,6 @@ __all__ = [
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REGEX_TYPE = type(re.compile(""))
|
|
||||||
|
|
||||||
SERVICE_PROCESS_SCHEMA = vol.Schema(
|
SERVICE_PROCESS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_TEXT): cv.string,
|
vol.Required(ATTR_TEXT): cv.string,
|
||||||
|
@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(maybe_result is None) # first result
|
(maybe_result is None) # first result
|
||||||
or (num_matched_entities > best_num_matched_entities)
|
or (
|
||||||
|
# More literal text matched
|
||||||
|
result.text_chunks_matched > maybe_result.text_chunks_matched
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
# More entities matched
|
||||||
|
num_matched_entities > best_num_matched_entities
|
||||||
|
)
|
||||||
or (
|
or (
|
||||||
# Fewer unmatched entities
|
# Fewer unmatched entities
|
||||||
(num_matched_entities == best_num_matched_entities)
|
(num_matched_entities == best_num_matched_entities)
|
||||||
@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity):
|
|||||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||||
)
|
)
|
||||||
or (
|
|
||||||
# More literal text matched
|
|
||||||
(num_matched_entities == best_num_matched_entities)
|
|
||||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
|
||||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
|
||||||
and (
|
|
||||||
result.text_chunks_matched
|
|
||||||
> maybe_result.text_chunks_matched
|
|
||||||
)
|
|
||||||
)
|
|
||||||
or (
|
or (
|
||||||
# Prefer match failures with entities
|
# Prefer match failures with entities
|
||||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
from dataclasses import asdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from hassil.recognize import MISSING_ENTITY, RecognizeResult
|
from hassil.recognize import MISSING_ENTITY, RecognizeResult
|
||||||
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
||||||
|
from home_assistant_intents import get_language_scores
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import http, websocket_api
|
from homeassistant.components import http, websocket_api
|
||||||
@ -38,6 +40,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||||||
websocket_api.async_register_command(hass, websocket_list_agents)
|
websocket_api.async_register_command(hass, websocket_list_agents)
|
||||||
websocket_api.async_register_command(hass, websocket_list_sentences)
|
websocket_api.async_register_command(hass, websocket_list_sentences)
|
||||||
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
|
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
|
||||||
|
websocket_api.async_register_command(hass, websocket_hass_agent_language_scores)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
@ -336,6 +339,36 @@ def _get_unmatched_slots(
|
|||||||
return unmatched_slots
|
return unmatched_slots
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "conversation/agent/homeassistant/language_scores",
|
||||||
|
vol.Optional("language"): str,
|
||||||
|
vol.Optional("country"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def websocket_hass_agent_language_scores(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Get support scores per language."""
|
||||||
|
language = msg.get("language", hass.config.language)
|
||||||
|
country = msg.get("country", hass.config.country)
|
||||||
|
|
||||||
|
scores = await hass.async_add_executor_job(get_language_scores)
|
||||||
|
matching_langs = language_util.matches(language, scores.keys(), country=country)
|
||||||
|
preferred_lang = matching_langs[0] if matching_langs else language
|
||||||
|
result = {
|
||||||
|
"languages": {
|
||||||
|
lang_key: asdict(lang_scores) for lang_key, lang_scores in scores.items()
|
||||||
|
},
|
||||||
|
"preferred_language": preferred_lang,
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.send_result(msg["id"], result)
|
||||||
|
|
||||||
|
|
||||||
class ConversationProcessView(http.HomeAssistantView):
|
class ConversationProcessView(http.HomeAssistantView):
|
||||||
"""View to process text."""
|
"""View to process text."""
|
||||||
|
|
||||||
|
@ -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.3.23"]
|
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
"""Util for Conversation."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def create_matcher(utterance: str) -> re.Pattern[str]:
|
|
||||||
"""Create a regex that matches the utterance."""
|
|
||||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
|
||||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
|
||||||
parts = re.split(r"({\w+}|\[[\w\s]+\] *)", utterance)
|
|
||||||
# Pattern to extract name from GROUP part. Matches {name}
|
|
||||||
group_matcher = re.compile(r"{(\w+)}")
|
|
||||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
|
||||||
optional_matcher = re.compile(r"\[([\w ]+)\] *")
|
|
||||||
|
|
||||||
pattern = ["^"]
|
|
||||||
for part in parts:
|
|
||||||
group_match = group_matcher.match(part)
|
|
||||||
optional_match = optional_matcher.match(part)
|
|
||||||
|
|
||||||
# Normal part
|
|
||||||
if group_match is None and optional_match is None:
|
|
||||||
pattern.append(part)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Group part
|
|
||||||
if group_match is not None:
|
|
||||||
pattern.append(rf"(?P<{group_match.groups()[0]}>[\w ]+?)\s*")
|
|
||||||
|
|
||||||
# Optional part
|
|
||||||
elif optional_match is not None:
|
|
||||||
pattern.append(rf"(?:{optional_match.groups()[0]} *)?")
|
|
||||||
|
|
||||||
pattern.append("$")
|
|
||||||
return re.compile("".join(pattern), re.IGNORECASE)
|
|
@ -6,7 +6,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"country": "Country"
|
"country": "[%key:common::config_flow::data::country%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"email": "Email used to access your {cookidoo} account.",
|
"email": "Email used to access your {cookidoo} account.",
|
||||||
|
@ -38,10 +38,10 @@
|
|||||||
"name": "[%key:component::cover::title%]",
|
"name": "[%key:component::cover::title%]",
|
||||||
"state": {
|
"state": {
|
||||||
"open": "[%key:common::state::open%]",
|
"open": "[%key:common::state::open%]",
|
||||||
"opening": "Opening",
|
"opening": "[%key:common::state::opening%]",
|
||||||
"closed": "[%key:common::state::closed%]",
|
"closed": "[%key:common::state::closed%]",
|
||||||
"closing": "Closing",
|
"closing": "[%key:common::state::closing%]",
|
||||||
"stopped": "Stopped"
|
"stopped": "[%key:common::state::stopped%]"
|
||||||
},
|
},
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"current_position": {
|
"current_position": {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
import logging
|
||||||
from ssl import SSLError
|
from ssl import SSLError
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
|
class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Deluge."""
|
"""Handle a config flow for Deluge."""
|
||||||
@ -86,7 +89,8 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
await self.hass.async_add_executor_job(api.connect)
|
await self.hass.async_add_executor_job(api.connect)
|
||||||
except (ConnectionRefusedError, TimeoutError, SSLError):
|
except (ConnectionRefusedError, TimeoutError, SSLError):
|
||||||
return "cannot_connect"
|
return "cannot_connect"
|
||||||
except Exception as ex: # noqa: BLE001
|
except Exception as ex:
|
||||||
|
_LOGGER.exception("Unexpected error")
|
||||||
if type(ex).__name__ == "BadLoginError":
|
if type(ex).__name__ == "BadLoginError":
|
||||||
return "invalid_auth"
|
return "invalid_auth"
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydexcom import AccountError, Dexcom, SessionError
|
from pydexcom import AccountError, Dexcom, SessionError
|
||||||
@ -12,6 +13,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
|||||||
|
|
||||||
from .const import CONF_SERVER, DOMAIN, SERVER_OUS, SERVER_US
|
from .const import CONF_SERVER, DOMAIN, SERVER_OUS, SERVER_US
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required(CONF_USERNAME): str,
|
||||||
@ -43,7 +46,8 @@ class DexcomConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except AccountError:
|
except AccountError:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except Exception: # noqa: BLE001
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected error")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
if "base" not in errors:
|
if "base" not in errors:
|
||||||
|
@ -9,7 +9,7 @@ import pydiscovergy.error as discovergyError
|
|||||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.httpx_client import create_async_httpx_client
|
||||||
|
|
||||||
from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator
|
from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -
|
|||||||
client = Discovergy(
|
client = Discovergy(
|
||||||
email=entry.data[CONF_EMAIL],
|
email=entry.data[CONF_EMAIL],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
httpx_client=get_async_client(hass),
|
httpx_client=create_async_httpx_client(hass),
|
||||||
authentication=BasicAuth(),
|
authentication=BasicAuth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
username = auth["cdp_internal_user_id"].lower()
|
username = auth["internalUserID"].lower()
|
||||||
await self.async_set_unique_id(username)
|
await self.async_set_unique_id(username)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
email = auth["email"].lower()
|
email = auth["loginEmailAddress"].lower()
|
||||||
data = {
|
data = {
|
||||||
CONF_EMAIL: email,
|
CONF_EMAIL: email,
|
||||||
CONF_USERNAME: username,
|
CONF_USERNAME: username,
|
||||||
|
@ -8,7 +8,11 @@ from aiodukeenergy import DukeEnergy
|
|||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
|
||||||
from homeassistant.components.recorder import get_instance
|
from homeassistant.components.recorder import get_instance
|
||||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
from homeassistant.components.recorder.models import (
|
||||||
|
StatisticData,
|
||||||
|
StatisticMeanType,
|
||||||
|
StatisticMetaData,
|
||||||
|
)
|
||||||
from homeassistant.components.recorder.statistics import (
|
from homeassistant.components.recorder.statistics import (
|
||||||
async_add_external_statistics,
|
async_add_external_statistics,
|
||||||
get_last_statistics,
|
get_last_statistics,
|
||||||
@ -137,7 +141,7 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
|
|||||||
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
|
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
|
||||||
)
|
)
|
||||||
consumption_metadata = StatisticMetaData(
|
consumption_metadata = StatisticMetaData(
|
||||||
has_mean=False,
|
mean_type=StatisticMeanType.NONE,
|
||||||
has_sum=True,
|
has_sum=True,
|
||||||
name=f"{name_prefix} Consumption",
|
name=f"{name_prefix} Consumption",
|
||||||
source=DOMAIN,
|
source=DOMAIN,
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["recorder"],
|
"dependencies": ["recorder"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["aiodukeenergy==0.2.2"]
|
"requirements": ["aiodukeenergy==0.3.0"]
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ SENSOR_TYPES: tuple[EcoforestSensorEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
EcoforestSensorEntityDescription(
|
EcoforestSensorEntityDescription(
|
||||||
key="convecto_air_flow",
|
key="convecto_air_flow",
|
||||||
translation_key="convecto_air_flow",
|
translation_key="convector_air_flow",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda data: data.convecto_air_flow,
|
value_fn=lambda data: data.convecto_air_flow,
|
||||||
|
@ -78,8 +78,8 @@
|
|||||||
"extractor": {
|
"extractor": {
|
||||||
"name": "Extractor"
|
"name": "Extractor"
|
||||||
},
|
},
|
||||||
"convecto_air_flow": {
|
"convector_air_flow": {
|
||||||
"name": "Convecto air flow"
|
"name": "Convector air flow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
|
@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
|
|||||||
def operation_list(self) -> list[str]:
|
def operation_list(self) -> list[str]:
|
||||||
"""List of available operation modes."""
|
"""List of available operation modes."""
|
||||||
econet_modes = self.water_heater.modes
|
econet_modes = self.water_heater.modes
|
||||||
op_list = []
|
operation_modes = set()
|
||||||
for mode in econet_modes:
|
for mode in econet_modes:
|
||||||
if (
|
if (
|
||||||
mode is not WaterHeaterOperationMode.UNKNOWN
|
mode is not WaterHeaterOperationMode.UNKNOWN
|
||||||
and mode is not WaterHeaterOperationMode.VACATION
|
and mode is not WaterHeaterOperationMode.VACATION
|
||||||
):
|
):
|
||||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||||
op_list.append(ha_mode)
|
operation_modes.add(ha_mode)
|
||||||
return op_list
|
return list(operation_modes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||||
|
@ -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==12.3.1"]
|
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"data": {
|
"data": {
|
||||||
"country": "Country",
|
"country": "[%key:common::config_flow::data::country%]",
|
||||||
"override_rest_url": "REST URL",
|
"override_rest_url": "REST URL",
|
||||||
"override_mqtt_url": "MQTT URL",
|
"override_mqtt_url": "MQTT URL",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
@ -68,6 +68,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
|
|||||||
key="DEGREE",
|
key="DEGREE",
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||||
),
|
),
|
||||||
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
|
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
|
||||||
key="WATT_METERS_SQUARED",
|
key="WATT_METERS_SQUARED",
|
||||||
|
@ -62,6 +62,7 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except (ClientError, TimeoutError):
|
except (ClientError, TimeoutError):
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unknown exception occurred")
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
await self.async_set_unique_id(hub.main.mac_address)
|
await self.async_set_unique_id(hub.main.mac_address)
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||||
|
@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, cast
|
|||||||
|
|
||||||
from elvia import Elvia, error as ElviaError
|
from elvia import Elvia, error as ElviaError
|
||||||
|
|
||||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
from homeassistant.components.recorder.models import (
|
||||||
|
StatisticData,
|
||||||
|
StatisticMeanType,
|
||||||
|
StatisticMetaData,
|
||||||
|
)
|
||||||
from homeassistant.components.recorder.statistics import (
|
from homeassistant.components.recorder.statistics import (
|
||||||
async_add_external_statistics,
|
async_add_external_statistics,
|
||||||
get_last_statistics,
|
get_last_statistics,
|
||||||
@ -144,7 +148,7 @@ class ElviaImporter:
|
|||||||
async_add_external_statistics(
|
async_add_external_statistics(
|
||||||
hass=self.hass,
|
hass=self.hass,
|
||||||
metadata=StatisticMetaData(
|
metadata=StatisticMetaData(
|
||||||
has_mean=False,
|
mean_type=StatisticMeanType.NONE,
|
||||||
has_sum=True,
|
has_sum=True,
|
||||||
name=f"{self.metering_point_id} Consumption",
|
name=f"{self.metering_point_id} Consumption",
|
||||||
source=DOMAIN,
|
source=DOMAIN,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user