Merge branch 'dev' into mill

This commit is contained in:
Daniel Hjelseth Høyer 2025-04-07 21:19:56 +02:00 committed by GitHub
commit 5700b96f53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1365 changed files with 66867 additions and 16073 deletions

View File

@ -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 }}
@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2025.02.0 uses: home-assistant/builder@2025.03.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -263,7 +263,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2025.02.0 uses: home-assistant/builder@2025.03.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -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 }}

View File

@ -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
@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Dependency review - name: Dependency review
uses: actions/dependency-review-action@v4.5.0 uses: actions/dependency-review-action@v4.6.0
with: with:
license-check: false # We use our own license audit checks license-check: false # We use our own license audit checks
@ -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

View File

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

View File

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

View File

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

View File

@ -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.*
@ -363,6 +364,7 @@ homeassistant.components.notify.*
homeassistant.components.notion.* homeassistant.components.notion.*
homeassistant.components.number.* homeassistant.components.number.*
homeassistant.components.nut.* homeassistant.components.nut.*
homeassistant.components.ohme.*
homeassistant.components.onboarding.* homeassistant.components.onboarding.*
homeassistant.components.oncue.* homeassistant.components.oncue.*
homeassistant.components.onedrive.* homeassistant.components.onedrive.*

2
.vscode/tasks.json vendored
View File

@ -4,7 +4,7 @@
{ {
"label": "Run Home Assistant Core", "label": "Run Home Assistant Core",
"type": "shell", "type": "shell",
"command": "hass -c ./config", "command": "${command:python.interpreterPath} -m homeassistant -c ./config",
"group": "test", "group": "test",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",

6
CODEOWNERS generated
View File

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

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

View File

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

View File

@ -178,6 +178,15 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
strict_core=False, strict_core=False,
skip_for_tests=True, skip_for_tests=True,
), ),
BlockingCall(
original_func=SSLContext.set_default_verify_paths,
object=SSLContext,
function="set_default_verify_paths",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall( BlockingCall(
original_func=Path.open, original_func=Path.open,
object=Path, object=Path,

View File

@ -859,8 +859,14 @@ async def _async_set_up_integrations(
integrations, all_integrations = await _async_resolve_domains_and_preload( integrations, all_integrations = await _async_resolve_domains_and_preload(
hass, config hass, config
) )
all_domains = set(all_integrations) # Detect all cycles
domains = set(integrations) integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, all_integrations.values(), set(all_integrations)
)
)
all_domains = set(integrations_after_dependencies)
domains = set(integrations) & all_domains
_LOGGER.info( _LOGGER.info(
"Domains to be set up: %s | %s", "Domains to be set up: %s | %s",
@ -868,6 +874,8 @@ async def _async_set_up_integrations(
all_domains - domains, all_domains - domains,
) )
async_set_domains_to_be_loaded(hass, all_domains)
# Initialize recorder # Initialize recorder
if "recorder" in all_domains: if "recorder" in all_domains:
recorder.async_initialize_recorder(hass) recorder.async_initialize_recorder(hass)
@ -900,24 +908,12 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered = { stage_dep_domains_unfiltered = {
dep dep
for domain in stage_domains for domain in stage_domains
for dep in all_integrations[domain].all_dependencies for dep in integrations_after_dependencies[domain]
if dep not in stage_domains if dep not in stage_domains
} }
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
stage_all_domains = stage_domains | stage_dep_domains stage_all_domains = stage_domains | stage_dep_domains
stage_all_integrations = {
domain: all_integrations[domain] for domain in stage_all_domains
}
# Detect all cycles
stage_integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, stage_all_integrations.values(), stage_all_domains
)
)
stage_all_domains = set(stage_integrations_after_dependencies)
stage_domains &= stage_all_domains
stage_dep_domains &= stage_all_domains
_LOGGER.info( _LOGGER.info(
"Setting up stage %s: %s | %s\nDependencies: %s | %s", "Setting up stage %s: %s | %s\nDependencies: %s | %s",
@ -928,8 +924,6 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered - stage_dep_domains, stage_dep_domains_unfiltered - stage_dep_domains,
) )
async_set_domains_to_be_loaded(hass, stage_all_domains)
if timeout is None: if timeout is None:
await _async_setup_multi_components(hass, stage_all_domains, config) await _async_setup_multi_components(hass, stage_all_domains, config)
continue continue

View File

@ -0,0 +1,5 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}

View File

@ -0,0 +1,5 @@
{
"domain": "eve",
"name": "Eve",
"iot_standards": ["matter"]
}

View File

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

View File

@ -72,10 +72,10 @@
"level": { "level": {
"name": "Level", "name": "Level",
"state": { "state": {
"high": "High", "high": "[%key:common::state::high%]",
"low": "Low", "low": "[%key:common::state::low%]",
"moderate": "Moderate", "moderate": "Moderate",
"very_high": "Very high" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -89,10 +89,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -123,10 +123,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -167,10 +167,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -181,10 +181,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -195,10 +195,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }

View File

@ -5,14 +5,14 @@
"data": { "data": {
"connection_type": "Select connection type" "connection_type": "Select connection type"
}, },
"description": "Select connection type. Local requires heaters with bluetooth" "description": "Select connection type. Local requires heaters with Bluetooth"
}, },
"local": { "local": {
"data": { "data": {
"wifi_ssid": "Wi-Fi SSID", "wifi_ssid": "Wi-Fi SSID",
"wifi_pswd": "Wi-Fi Password" "wifi_pswd": "Wi-Fi password"
}, },
"description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue led starts blinking before pressing Submit. Configuring heater might take some minutes." "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue LED starts blinking before pressing Submit. Configuring heater might take some minutes."
}, },
"cloud": { "cloud": {
"data": { "data": {

View File

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

View File

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

View File

@ -7,7 +7,7 @@
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]", "latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]", "longitude": "[%key:common::config_flow::data::longitude%]",
"radius": "Station Radius (miles; optional)" "radius": "Station radius (miles; optional)"
} }
} }
}, },
@ -25,7 +25,7 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"radius": "Station Radius (miles)" "radius": "Station radius (miles)"
} }
} }
} }

View File

@ -91,7 +91,7 @@
"name": "Hydrogen fluoride" "name": "Hydrogen fluoride"
}, },
"health_index": { "health_index": {
"name": "Health Index" "name": "Health index"
}, },
"absolute_humidity": { "absolute_humidity": {
"name": "Absolute humidity" "name": "Absolute humidity"
@ -112,10 +112,10 @@
"name": "Oxygen" "name": "Oxygen"
}, },
"performance_index": { "performance_index": {
"name": "Performance Index" "name": "Performance index"
}, },
"hydrogen_phosphide": { "hydrogen_phosphide": {
"name": "Hydrogen Phosphide" "name": "Hydrogen phosphide"
}, },
"relative_pressure": { "relative_pressure": {
"name": "Relative pressure" "name": "Relative pressure"
@ -127,22 +127,22 @@
"name": "Refrigerant" "name": "Refrigerant"
}, },
"silicon_hydride": { "silicon_hydride": {
"name": "Silicon Hydride" "name": "Silicon hydride"
}, },
"noise": { "noise": {
"name": "Noise" "name": "Noise"
}, },
"maximum_noise": { "maximum_noise": {
"name": "Noise (Maximum)" "name": "Noise (maximum)"
}, },
"radon": { "radon": {
"name": "Radon" "name": "Radon"
}, },
"industrial_volatile_organic_compounds": { "industrial_volatile_organic_compounds": {
"name": "VOCs (Industrial)" "name": "VOCs (industrial)"
}, },
"virus_index": { "virus_index": {
"name": "Virus Index" "name": "Virus index"
} }
} }
} }

View File

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

View File

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

View File

@ -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%]",
@ -16,8 +16,8 @@
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
"city": "City", "city": "City",
"country": "Country", "state": "State",
"state": "State" "country": "[%key:common::config_flow::data::country%]"
} }
}, },
"reauth_confirm": { "reauth_confirm": {
@ -56,12 +56,12 @@
"sensor": { "sensor": {
"pollutant_label": { "pollutant_label": {
"state": { "state": {
"co": "Carbon Monoxide", "co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"n2": "Nitrogen Dioxide", "n2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"o3": "Ozone", "o3": "[%key:component::sensor::entity_component::ozone::name%]",
"p1": "PM10", "p1": "[%key:component::sensor::entity_component::pm10::name%]",
"p2": "PM2.5", "p2": "[%key:component::sensor::entity_component::pm25::name%]",
"s2": "Sulfur Dioxide" "s2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
} }
}, },
"pollutant_level": { "pollutant_level": {

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.9"] "requirements": ["aioairzone==1.0.0"]
} }

View File

@ -9,6 +9,8 @@ from aioairzone.const import (
AZD_HUMIDITY, AZD_HUMIDITY,
AZD_TEMP, AZD_TEMP,
AZD_TEMP_UNIT, AZD_TEMP_UNIT,
AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_SIGNAL,
AZD_WEBSERVER, AZD_WEBSERVER,
AZD_WIFI_RSSI, AZD_WIFI_RSSI,
AZD_ZONES, AZD_ZONES,
@ -73,6 +75,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription(
device_class=SensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_THERMOSTAT_SIGNAL,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="thermostat_signal",
),
) )

View File

@ -76,6 +76,9 @@
"sensor": { "sensor": {
"rssi": { "rssi": {
"name": "RSSI" "name": "RSSI"
},
"thermostat_signal": {
"name": "Signal strength"
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@
"name": "Status date" "name": "Status date"
}, },
"dip_switch_settings": { "dip_switch_settings": {
"name": "Dip switch settings" "name": "DIP switch settings"
}, },
"low_battery_signal": { "low_battery_signal": {
"name": "Low battery signal" "name": "Low battery signal"

View File

@ -20,6 +20,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_IGNORE, SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_ZEROCONF, SOURCE_ZEROCONF,
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_IDENTIFIERS: list(combined_identifiers), CONF_IDENTIFIERS: list(combined_identifiers),
}, },
) )
if entry.source != SOURCE_IGNORE: # Don't reload ignored entries or in the middle of reauth,
# e.g. if the user is entering a new PIN
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
self.hass.config_entries.async_schedule_reload(entry.entry_id) self.hass.config_entries.async_schedule_reload(entry.entry_id)
if not allow_exist: if not allow_exist:
raise DeviceAlreadyConfigured raise DeviceAlreadyConfigured

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,9 +36,9 @@
"wi_fi_strength": { "wi_fi_strength": {
"name": "Wi-Fi strength", "name": "Wi-Fi strength",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
} }
} }

View File

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

View File

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

View File

@ -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,8 @@ 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"): bool,
vol.Optional("preannounce_media_id"): str,
} }
), ),
cv.has_at_least_one_key("message", "media_id"), cv.has_at_least_one_key("message", "media_id"),
@ -70,6 +76,8 @@ 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"): bool,
vol.Optional("preannounce_media_id"): str,
vol.Optional("extra_system_prompt"): str, vol.Optional("extra_system_prompt"): str,
} }
), ),
@ -82,6 +90,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

View File

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

View File

@ -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,8 @@ 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: bool = True,
preannounce_media_id: str = PREANNOUNCE_URL,
) -> None: ) -> None:
"""Play and show an announcement on the satellite. """Play and show an announcement on the satellite.
@ -186,6 +191,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 is True, a sound is played before the announcement.
If preannounce_media_id is provided, it overrides the default sound.
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 +201,11 @@ 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=preannounce_media_id if preannounce else None,
)
if self._is_announcing: if self._is_announcing:
raise SatelliteBusyError raise SatelliteBusyError
@ -220,6 +232,8 @@ 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: bool = True,
preannounce_media_id: str = PREANNOUNCE_URL,
) -> None: ) -> None:
"""Start a conversation from the satellite. """Start a conversation from the satellite.
@ -229,6 +243,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 is True, a sound is played before the start message or media.
If preannounce_media_id is provided, it overrides the default sound.
Calls async_start_conversation. Calls async_start_conversation.
""" """
await self._cancel_running_pipeline() await self._cancel_running_pipeline()
@ -244,13 +261,17 @@ 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=preannounce_media_id if preannounce else None,
) )
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 +301,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 +492,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 +503,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 +554,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,
) )

View File

@ -8,12 +8,22 @@ 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:
required: false
default: true
selector:
boolean:
preannounce_media_id:
required: false
selector:
text:
start_conversation: start_conversation:
target: target:
entity: entity:
@ -24,6 +34,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 +45,12 @@ start_conversation:
required: false required: false
selector: selector:
text: text:
preannounce:
required: false
default: true
selector:
boolean:
preannounce_media_id:
required: false
selector:
text:

View File

@ -23,6 +23,14 @@
"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": {
"name": "Preannounce",
"description": "Play a sound before the announcement."
},
"preannounce_media_id": {
"name": "Preannounce media ID",
"description": "Custom media ID to play before the announcement."
} }
} }
}, },
@ -41,6 +49,14 @@
"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": {
"name": "Preannounce",
"description": "Play a sound before the start message or media."
},
"preannounce_media_id": {
"name": "Preannounce media ID",
"description": "Custom media ID to play before the start message or media."
} }
} }
} }

View File

@ -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=False,
), ),
f"assist_satellite_connection_test_{msg['entity_id']}", f"assist_satellite_connection_test_{msg['entity_id']}",
) )

View File

@ -66,28 +66,28 @@
"name": "Upload" "name": "Upload"
}, },
"load_avg_1m": { "load_avg_1m": {
"name": "Average load (1m)" "name": "Average load (1 min)"
}, },
"load_avg_5m": { "load_avg_5m": {
"name": "Average load (5m)" "name": "Average load (5 min)"
}, },
"load_avg_15m": { "load_avg_15m": {
"name": "Average load (15m)" "name": "Average load (15 min)"
}, },
"24ghz_temperature": { "24ghz_temperature": {
"name": "2.4GHz Temperature" "name": "2.4GHz temperature"
}, },
"5ghz_temperature": { "5ghz_temperature": {
"name": "5GHz Temperature" "name": "5GHz temperature"
}, },
"cpu_temperature": { "cpu_temperature": {
"name": "CPU Temperature" "name": "CPU temperature"
}, },
"5ghz_2_temperature": { "5ghz_2_temperature": {
"name": "5GHz Temperature (Radio 2)" "name": "5GHz temperature (Radio 2)"
}, },
"6ghz_temperature": { "6ghz_temperature": {
"name": "6GHz Temperature" "name": "6GHz temperature"
}, },
"cpu_usage": { "cpu_usage": {
"name": "CPU usage" "name": "CPU usage"

View File

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

View File

@ -14,7 +14,7 @@
"personal_access_token": "Personal Access Token (PAT)" "personal_access_token": "Personal Access Token (PAT)"
}, },
"description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.", "description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.",
"title": "Add Azure DevOps Project" "title": "Add Azure DevOps project"
}, },
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
@ -32,7 +32,7 @@
"entity": { "entity": {
"sensor": { "sensor": {
"build_id": { "build_id": {
"name": "{definition_name} latest build id" "name": "{definition_name} latest build ID"
}, },
"finish_time": { "finish_time": {
"name": "{definition_name} latest build finish time" "name": "{definition_name} latest build finish time"
@ -59,7 +59,7 @@
"name": "{definition_name} latest build start time" "name": "{definition_name} latest build start time"
}, },
"url": { "url": {
"name": "{definition_name} latest build url" "name": "{definition_name} latest build URL"
}, },
"work_item_count": { "work_item_count": {
"name": "{item_type} {item_state} work items" "name": "{item_type} {item_state} work items"
@ -68,7 +68,7 @@
}, },
"exceptions": { "exceptions": {
"authentication_failed": { "authentication_failed": {
"message": "Could not authorize with Azure DevOps for {title}. You will need to update your personal access token." "message": "Could not authorize with Azure DevOps for {title}. You will need to update your Personal Access Token."
} }
} }
} }

View File

@ -175,7 +175,8 @@ class AzureStorageBackupAgent(BackupAgent):
"""Find a blob by backup id.""" """Find a blob by backup id."""
async for blob in self._client.list_blobs(include="metadata"): async for blob in self._client.list_blobs(include="metadata"):
if ( if (
backup_id == blob.metadata.get("backup_id", "") blob.metadata is not None
and backup_id == blob.metadata.get("backup_id", "")
and blob.metadata.get("metadata_version") == METADATA_VERSION and blob.metadata.get("metadata_version") == METADATA_VERSION
): ):
return blob return blob

View File

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

View 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={})

View File

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

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

View 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]
),
}

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

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1 @@
"""Balay virtual integration."""

View File

@ -0,0 +1,6 @@
{
"domain": "balay",
"name": "Balay",
"integration_type": "virtual",
"supported_by": "home_connect"
}

View File

@ -103,8 +103,8 @@
"temperature_range": { "temperature_range": {
"name": "Temperature range", "name": "Temperature range",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
} }
}, },

View File

@ -124,15 +124,15 @@
"battery": { "battery": {
"name": "Battery", "name": "Battery",
"state": { "state": {
"off": "Normal", "off": "[%key:common::state::normal%]",
"on": "Low" "on": "[%key:common::state::low%]"
} }
}, },
"battery_charging": { "battery_charging": {
"name": "Charging", "name": "Charging",
"state": { "state": {
"off": "Not charging", "off": "Not charging",
"on": "Charging" "on": "[%key:common::state::charging%]"
} }
}, },
"carbon_monoxide": { "carbon_monoxide": {
@ -145,7 +145,7 @@
"cold": { "cold": {
"name": "Cold", "name": "Cold",
"state": { "state": {
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]", "off": "[%key:common::state::normal%]",
"on": "Cold" "on": "Cold"
} }
}, },
@ -180,7 +180,7 @@
"heat": { "heat": {
"name": "Heat", "name": "Heat",
"state": { "state": {
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]", "off": "[%key:common::state::normal%]",
"on": "Hot" "on": "Hot"
} }
}, },

View File

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

View File

@ -501,19 +501,17 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
return return
# presets and inputs might have the same name; presets have priority # presets and inputs might have the same name; presets have priority
url: str | None = None
for input_ in self._inputs: for input_ in self._inputs:
if input_.text == source: if input_.text == source:
url = input_.url await self._player.play_url(input_.url)
return
for preset in self._presets: for preset in self._presets:
if preset.name == source: if preset.name == source:
url = preset.url await self._player.load_preset(preset.id)
return
if url is None:
raise ServiceValidationError(f"Source {source} not found") raise ServiceValidationError(f"Source {source} not found")
await self._player.play_url(url)
async def async_clear_playlist(self) -> None: async def async_clear_playlist(self) -> None:
"""Clear players playlist.""" """Clear players playlist."""
if self.is_grouped and not self.is_leader: if self.is_grouped and not self.is_leader:

View File

@ -19,8 +19,8 @@
"bleak-retry-connector==3.9.0", "bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4", "bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.4.5", "bluetooth-auto-recovery==1.4.5",
"bluetooth-data-tools==1.26.1", "bluetooth-data-tools==1.27.0",
"dbus-fast==2.39.6", "dbus-fast==2.43.0",
"habluetooth==3.32.0" "habluetooth==3.37.0"
] ]
} }

View File

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

View File

@ -16,6 +16,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN from .const import DOMAIN
@ -91,11 +92,22 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered[CONF_ACCESS_TOKEN] = token self._discovered[CONF_ACCESS_TOKEN] = token
try: try:
_, hub_name = await _validate_input(self.hass, self._discovered) bond_id, hub_name = await _validate_input(self.hass, self._discovered)
except InputValidationError: except InputValidationError:
return return
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._discovered[CONF_NAME] = hub_name self._discovered[CONF_NAME] = hub_name
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by dhcp discovery."""
host = discovery_info.ip
bond_id = discovery_info.hostname.partition("-")[2].upper()
await self.async_set_unique_id(bond_id)
return await self.async_step_any_discovery(bond_id, host)
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@ -104,11 +116,17 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
host: str = discovery_info.host host: str = discovery_info.host
bond_id = name.partition(".")[0] bond_id = name.partition(".")[0]
await self.async_set_unique_id(bond_id) await self.async_set_unique_id(bond_id)
return await self.async_step_any_discovery(bond_id, host)
async def async_step_any_discovery(
self, bond_id: str, host: str
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
for entry in self._async_current_entries(): for entry in self._async_current_entries():
if entry.unique_id != bond_id: if entry.unique_id != bond_id:
continue continue
updates = {CONF_HOST: host} updates = {CONF_HOST: host}
if entry.state == ConfigEntryState.SETUP_ERROR and ( if entry.state is ConfigEntryState.SETUP_ERROR and (
token := await async_get_token(self.hass, host) token := await async_get_token(self.hass, host)
): ):
updates[CONF_ACCESS_TOKEN] = token updates[CONF_ACCESS_TOKEN] = token
@ -153,10 +171,14 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_HOST: self._discovered[CONF_HOST], CONF_HOST: self._discovered[CONF_HOST],
} }
try: try:
_, hub_name = await _validate_input(self.hass, data) bond_id, hub_name = await _validate_input(self.hass, data)
except InputValidationError as error: except InputValidationError as error:
errors["base"] = error.base errors["base"] = error.base
else: else:
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self._discovered[CONF_HOST]}
)
return self.async_create_entry( return self.async_create_entry(
title=hub_name, title=hub_name,
data=data, data=data,
@ -185,8 +207,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
except InputValidationError as error: except InputValidationError as error:
errors["base"] = error.base errors["base"] = error.base
else: else:
await self.async_set_unique_id(bond_id) await self.async_set_unique_id(bond_id, raise_on_progress=False)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured(
updates={CONF_HOST: user_input[CONF_HOST]}
)
return self.async_create_entry(title=hub_name, data=user_input) return self.async_create_entry(title=hub_name, data=user_input)
return self.async_show_form( return self.async_show_form(

View File

@ -3,6 +3,16 @@
"name": "Bond", "name": "Bond",
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
"config_flow": true, "config_flow": true,
"dhcp": [
{
"hostname": "bond-*",
"macaddress": "3C6A2C1*"
},
{
"hostname": "bond-*",
"macaddress": "F44E38*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bond", "documentation": "https://www.home-assistant.io/integrations/bond",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["bond_async"], "loggers": ["bond_async"],

View File

@ -0,0 +1,67 @@
"""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 ConfigEntryAuthFailed, 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 ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from err
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
await panel.disconnect()
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) 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

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

View File

@ -0,0 +1,218 @@
"""Config flow for Bosch Alarm integration."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
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,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an authentication error."""
self._data = dict(entry_data)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reauth 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:
reauth_entry = self._get_reauth_entry()
self._data.update(user_input)
try:
(_, _) = 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:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)

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

View File

@ -0,0 +1,73 @@
"""Diagnostics for bosch alarm."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from . import BoschAlarmConfigEntry
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: BoschAlarmConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,
"areas": [
{
"id": area_id,
"name": area.name,
"all_ready": area.all_ready,
"part_ready": area.part_ready,
"faults": area.faults,
"alarms": area.alarms,
"disarmed": area.is_disarmed(),
"arming": area.is_arming(),
"pending": area.is_pending(),
"part_armed": area.is_part_armed(),
"all_armed": area.is_all_armed(),
"armed": area.is_armed(),
"triggered": area.is_triggered(),
}
for area_id, area in entry.runtime_data.areas.items()
],
"points": [
{
"id": point_id,
"name": point.name,
"open": point.is_open(),
"normal": point.is_normal(),
}
for point_id, point in entry.runtime_data.points.items()
],
"doors": [
{
"id": door_id,
"name": door.name,
"open": door.is_open(),
"locked": door.is_locked(),
}
for door_id, door in entry.runtime_data.doors.items()
],
"outputs": [
{
"id": output_id,
"name": output.name,
"active": output.is_active(),
}
for output_id, output in entry.runtime_data.outputs.items()
],
"history_events": entry.runtime_data.events,
},
}

View 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"]
}

View 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: done
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

View File

@ -0,0 +1,57 @@
{
"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"
}
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data::installer_code%]",
"user_code": "[%key:component::bosch_alarm::config::step::auth::data::user_code%]"
},
"data_description": {
"password": "[%key:component::bosch_alarm::config::step::auth::data_description::password%]",
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data_description::installer_code%]",
"user_code": "[%key:component::bosch_alarm::config::step::auth::data_description::user_code%]"
}
}
},
"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%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"exceptions": {
"cannot_connect": {
"message": "Could not connect to panel."
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
}
}
}

View File

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

View File

@ -74,7 +74,7 @@
}, },
"get_events": { "get_events": {
"name": "Get events", "name": "Get events",
"description": "Get events on a calendar within a time range.", "description": "Retrieves events on a calendar within a time range.",
"fields": { "fields": {
"start_date_time": { "start_date_time": {
"name": "Start time", "name": "Start time",

View File

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

View File

@ -2,18 +2,11 @@
from __future__ import annotations from __future__ import annotations
from contextlib import suppress
import logging import logging
from typing import TYPE_CHECKING, Literal, cast from typing import TYPE_CHECKING, Literal, cast
with suppress(Exception):
# TurboJPEG imports numpy which may or may not work so
# we have to guard the import here. We still want
# to import it at top level so it gets loaded
# in the import executor and not in the event loop.
from turbojpeg import TurboJPEG from turbojpeg import TurboJPEG
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Image from . import Image

View File

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

View File

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

View File

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

View File

@ -98,13 +98,13 @@
"name": "Preset", "name": "Preset",
"state": { "state": {
"none": "None", "none": "None",
"eco": "Eco", "home": "[%key:common::state::home%]",
"away": "Away", "away": "[%key:common::state::not_home%]",
"activity": "Activity",
"boost": "Boost", "boost": "Boost",
"comfort": "Comfort", "comfort": "Comfort",
"home": "[%key:common::state::home%]", "eco": "Eco",
"sleep": "Sleep", "sleep": "Sleep"
"activity": "Activity"
} }
}, },
"preset_modes": { "preset_modes": {
@ -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",

View File

@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
flow_id=flow_id, user_input=tokens flow_id=flow_id, user_input=tokens
) )
self.hass.async_create_task(await_tokens()) # It's a background task because it should be cancelled on shutdown and there's nothing else
# we can do in such case. There's also no need to wait for this during setup.
self.hass.async_create_background_task(
await_tokens(), name="Awaiting OAuth tokens"
)
return authorize_url return authorize_url

View File

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

View File

@ -245,6 +245,10 @@ class CloudLoginView(HomeAssistantView):
name = "api:cloud:login" name = "api:cloud:login"
@require_admin @require_admin
async def post(self, request: web.Request) -> web.Response:
"""Handle login request."""
return await self._post(request)
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator( @RequestDataValidator(
vol.Schema( vol.Schema(
@ -259,7 +263,7 @@ class CloudLoginView(HomeAssistantView):
) )
) )
) )
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: async def _post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle login request.""" """Handle login request."""
hass = request.app[KEY_HASS] hass = request.app[KEY_HASS]
cloud = hass.data[DATA_CLOUD] cloud = hass.data[DATA_CLOUD]
@ -316,8 +320,12 @@ class CloudLogoutView(HomeAssistantView):
name = "api:cloud:logout" name = "api:cloud:logout"
@require_admin @require_admin
@_handle_cloud_errors
async def post(self, request: web.Request) -> web.Response: async def post(self, request: web.Request) -> web.Response:
"""Handle logout request."""
return await self._post(request)
@_handle_cloud_errors
async def _post(self, request: web.Request) -> web.Response:
"""Handle logout request.""" """Handle logout request."""
hass = request.app[KEY_HASS] hass = request.app[KEY_HASS]
cloud = hass.data[DATA_CLOUD] cloud = hass.data[DATA_CLOUD]
@ -400,9 +408,13 @@ class CloudForgotPasswordView(HomeAssistantView):
name = "api:cloud:forgot_password" name = "api:cloud:forgot_password"
@require_admin @require_admin
async def post(self, request: web.Request) -> web.Response:
"""Handle forgot password request."""
return await self._post(request)
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({vol.Required("email"): str})) @RequestDataValidator(vol.Schema({vol.Required("email"): str}))
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: async def _post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle forgot password request.""" """Handle forgot password request."""
hass = request.app[KEY_HASS] hass = request.app[KEY_HASS]
cloud = hass.data[DATA_CLOUD] cloud = hass.data[DATA_CLOUD]

View File

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

View File

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

View File

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

View File

@ -119,10 +119,10 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
# because no serial number or mac is available # because no serial number or mac is available
self._attr_unique_id = f"{config_entry_entry_id}-{device.index}" self._attr_unique_id = f"{config_entry_entry_id}-{device.index}"
self._attr_device_info = coordinator.platform_device_info(device, device.type) self._attr_device_info = coordinator.platform_device_info(device, device.type)
self._update_attributes()
@callback def _update_attributes(self) -> None:
def _handle_coordinator_update(self) -> None: """Update class attributes."""
"""Handle updated data from the coordinator."""
device = self.coordinator.data[CLIMATE][self._device.index] device = self.coordinator.data[CLIMATE][self._device.index]
if not isinstance(device.val, list): if not isinstance(device.val, list):
raise HomeAssistantError( raise HomeAssistantError(
@ -158,6 +158,12 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
self._attr_target_temperature = values[4] / 10 self._attr_target_temperature = values[4] / 10
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_attributes()
super()._handle_coordinator_update()
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if ( if (
@ -171,6 +177,8 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
await self.coordinator.api.set_clima_status( await self.coordinator.api.set_clima_status(
self._device.index, ClimaComelitCommand.SET, target_temp self._device.index, ClimaComelitCommand.SET, target_temp
) )
self._attr_target_temperature = target_temp
self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode.""" """Set hvac mode."""
@ -182,3 +190,5 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
await self.coordinator.api.set_clima_status( await self.coordinator.api.set_clima_status(
self._device.index, MODE_TO_ACTION[hvac_mode] self._device.index, MODE_TO_ACTION[hvac_mode]
) )
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()

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