Compare commits

..

62 Commits

Author SHA1 Message Date
Bram Kragten
c830320730 Bump version to 2026.4.0b4 2026-03-27 22:46:53 +01:00
Paul Bottein
336aa0f5df Update frontend to 20260325.2 (#166717) 2026-03-27 22:46:49 +01:00
Artur Pragacz
754291b34f Use legacy naming for entities (#166696) 2026-03-27 22:46:49 +01:00
Åke Strandberg
bbae0862b0 Add missing miele oven codes (#166690) 2026-03-27 22:46:48 +01:00
Åke Strandberg
6b7693b2fd Add missing miele program_id code (#166685) 2026-03-27 22:46:47 +01:00
Simone Chemelli
954926a05c Bump aioamazondevices to 13.3.1 (#166658) 2026-03-27 22:46:46 +01:00
Abílio Costa
71981f66ec Update idasen-ha to 2.6.5 (#166645) 2026-03-27 22:46:45 +01:00
Artur Pragacz
7f94f95ac9 Wait for device registry in entity registry loading (#166636) 2026-03-27 22:46:44 +01:00
Erik Montnemery
4ee3177c5d Add select conditions (#166612) 2026-03-27 22:46:43 +01:00
Erik Montnemery
9c1f9ca5c6 Add weather support to humidity conditions (#166599) 2026-03-27 22:46:42 +01:00
Franck Nijhof
cff4cf4d2c Bump version to 2026.4.0b3 2026-03-26 19:51:36 +00:00
Erik Montnemery
ee9d9781ee Add climate.is_hvac_mode condition (#166570) 2026-03-26 19:51:07 +00:00
Jamie Magee
1b972d4adc Remove tplink_lte integration (#166615) 2026-03-26 19:49:52 +00:00
Bram Kragten
72598479d5 Update frontend to 20260325.1 (#166614) 2026-03-26 19:49:50 +00:00
Erik Montnemery
02599a4a6e Add condition humidifier.is_mode (#166610) 2026-03-26 19:49:49 +00:00
Erik Montnemery
af9f351fce Restore support for number entities as limits in moisture conditions and triggers (#166608) 2026-03-26 19:49:47 +00:00
Erik Montnemery
ff79943776 Restore support for number entities as limits in battery conditions and triggers (#166607) 2026-03-26 19:49:46 +00:00
Erik Montnemery
e60048ef30 Add input_boolean support to switch conditions (#166602) 2026-03-26 19:49:45 +00:00
Erik Montnemery
24c0b22038 Add light.is_brightness condition (#166601) 2026-03-26 19:49:43 +00:00
Norbert Rittel
6f32a53742 Make siren conditions consistent with new wording (#166600) 2026-03-26 19:49:42 +00:00
Erik Montnemery
da9d1080d9 Remove number entity support from power triggers and conditions (#166597) 2026-03-26 19:49:41 +00:00
Erik Montnemery
2ea4d7913e Remove number entity support from moisture triggers and conditions (#166596) 2026-03-26 19:49:40 +00:00
Erik Montnemery
16999e3707 Remove number entity support from illuminance triggers and conditions (#166595) 2026-03-26 19:49:38 +00:00
Erik Montnemery
5c53b847dc Remove number entity support from humidity triggers and conditions (#166594) 2026-03-26 19:49:37 +00:00
Erik Montnemery
3afd763d16 Remove number entity support from battery triggers and conditions (#166593) 2026-03-26 19:49:35 +00:00
Abílio Costa
75a15ed24e Add todo to experimental triggers (#166591) 2026-03-26 19:49:34 +00:00
Ronald van der Meer
6d56597a2a Bump pooldose 0.9.0 (#166589) 2026-03-26 19:49:32 +00:00
Erik Montnemery
5872222213 Remove class NumericalDomainSpec (#166588) 2026-03-26 19:49:31 +00:00
reneboer
bd5c73fd7b Bump renault-api to 0.5.7 (#166586) 2026-03-26 19:49:30 +00:00
hanwg
d8a32dcf69 Add missing translations for Telegram bot (#166581)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-26 19:49:29 +00:00
Devin Slick
87cd90ab5d Bump lojack-api to 0.7.2 (#166560)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 19:45:06 +00:00
Tom
cb5b0c5b5e Verify Proxmox permissions when creating snapshots (#166547) 2026-03-26 19:45:04 +00:00
John Meyers
2fa16101f4 Update rainmachine solar radiation to reflect it is per day, not per … (#166040) 2026-03-26 19:45:03 +00:00
Franck Nijhof
6dd5c30b49 Bump version to 2026.4.0b2 2026-03-26 10:59:11 +00:00
AlCalzone
72f5a572eb Revert: Create repair issue for legacy Z-Wave Door state sensors that are still in use (#166583) 2026-03-26 10:58:55 +00:00
Erik Montnemery
d501d8cb28 Adjust some trigger and condition schemas (#166568) 2026-03-26 10:58:54 +00:00
Keilin Bickar
35c4b4ff5b Bump asyncsleepiq to 1.7.1 (#166552) 2026-03-26 10:58:53 +00:00
Keilin Bickar
f3e8ac5b8e Bump sense-energy to 0.14.0 (#166550) 2026-03-26 10:58:51 +00:00
tronikos
ab2bcd84c6 Add Google Drive backup upload progress (#166549) 2026-03-26 10:58:50 +00:00
Ariel Ebersberger
cdf7b013a9 Add battery triggers (#166258) 2026-03-26 10:58:48 +00:00
Erik Montnemery
eeba0467a1 Add trigger humidifier.mode_changed (#166241)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-26 10:58:47 +00:00
Franck Nijhof
43ca72bf7e Bump version to 2026.4.0b1 2026-03-26 00:01:26 +00:00
Franck Nijhof
aa9e279026 Improve conversation action naming consistency (#166542) 2026-03-26 00:01:16 +00:00
Franck Nijhof
9f3917830d Improve weather action naming consistency (#166540) 2026-03-26 00:01:15 +00:00
Franck Nijhof
c458bc2ee3 Improve dashboard action naming consistency (#166539) 2026-03-26 00:01:14 +00:00
Franck Nijhof
e0455629d7 Improve logger action naming consistency (#166538) 2026-03-26 00:01:12 +00:00
Franck Nijhof
b802dcba8d Improve group action naming consistency (#166537) 2026-03-26 00:01:11 +00:00
Franck Nijhof
7ff868e94c Improve water heater action naming consistency (#166535)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 00:01:10 +00:00
Franck Nijhof
44bd3e3d74 Improve device tracker action naming consistency (#166534) 2026-03-26 00:01:09 +00:00
Jordan Harvey
9d793ce1df Bump pyanglianwater to 3.1.2 (#166531) 2026-03-26 00:01:07 +00:00
Franck Nijhof
d8dee8fc91 Improve image action naming consistency (#166527) 2026-03-26 00:01:06 +00:00
Franck Nijhof
3c52acb825 Improve counter action naming consistency (#166526) 2026-03-26 00:01:04 +00:00
Franck Nijhof
cb195be6ad Improve automation action naming consistency (#166525) 2026-03-26 00:01:03 +00:00
Franck Nijhof
08f7bed679 Improve humidifier action naming consistency (#166524) 2026-03-26 00:01:02 +00:00
Erik Montnemery
744563c7a7 Speed up trigger tests (#166522) 2026-03-26 00:01:01 +00:00
Franck Nijhof
5d48801645 Improve valve action naming consistency (#166521) 2026-03-26 00:00:59 +00:00
Franck Nijhof
4211686c07 Improve script action naming consistency (#166517) 2026-03-26 00:00:58 +00:00
Franck Nijhof
98379c9642 Improve cloud action naming consistency (#166516) 2026-03-26 00:00:57 +00:00
Erik Montnemery
a3c9d35a13 Use NumericThresholdSelector in numeric conditions (#166507) 2026-03-26 00:00:56 +00:00
Erik Montnemery
5a7abc0a92 Add trigger water_heater.operation_mode_changed (#166450) 2026-03-26 00:00:54 +00:00
johanzander
ade73ec159 growatt_server: use human-readable labels in exception messages (#166024)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-26 00:00:53 +00:00
Franck Nijhof
6f7a5d9320 Bump version to 2026.4.0b0 2026-03-25 18:48:08 +00:00
111 changed files with 1051 additions and 1788 deletions

View File

@@ -112,7 +112,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -123,7 +123,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.5"
HA_SHORT_VERSION: "2026.4"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
@@ -280,7 +280,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -301,7 +301,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
with:
extra-args: --all-files zizmor
@@ -364,7 +364,7 @@ jobs:
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
key: >-
@@ -372,7 +372,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -384,7 +384,7 @@ jobs:
env.HA_SHORT_VERSION }}-
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
@@ -430,7 +430,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -484,7 +484,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -515,7 +515,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -552,7 +552,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -643,7 +643,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -694,7 +694,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -747,7 +747,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -804,7 +804,7 @@ jobs:
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -812,7 +812,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: .mypy_cache
key: >-
@@ -854,7 +854,7 @@ jobs:
- base
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -887,7 +887,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -930,7 +930,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -964,7 +964,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -1080,7 +1080,7 @@ jobs:
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1115,7 +1115,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -1238,7 +1238,7 @@ jobs:
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1275,7 +1275,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -1392,7 +1392,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
flags: full-suite
@@ -1421,7 +1421,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1455,7 +1455,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: venv
fail-on-cache-miss: true
@@ -1563,7 +1563,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
@@ -1591,7 +1591,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results
fail_ci_if_error: true

View File

@@ -468,6 +468,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
translation.async_setup(hass)
recovery = hass.config.recovery_mode
device_registry.async_setup(hass)
try:
await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),

View File

@@ -13,9 +13,6 @@ from homeassistant.helpers import (
config_entry_oauth2_flow,
device_registry as dr,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import api
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
@@ -28,17 +25,11 @@ async def async_setup_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Set up Aladdin Connect Genie from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)

View File

@@ -37,9 +37,6 @@
"close_door_failed": {
"message": "Failed to close the garage door"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"open_door_failed": {
"message": "Failed to open the garage door"
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.3.0"]
"requirements": ["aioamazondevices==13.3.1"]
}

View File

@@ -2,8 +2,8 @@
import asyncio
from asyncio import timeout
from contextlib import AsyncExitStack
import logging
from typing import Any
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
@@ -54,31 +54,36 @@ async def _run_client(
client = runtime_data.client
coordinators = runtime_data.coordinators
def _listen(_: Any) -> None:
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
while True:
try:
async with AsyncExitStack() as stack:
async with timeout(interval):
await client.start()
stack.push_async_callback(client.stop)
async with timeout(interval):
await client.start()
_LOGGER.debug("Client connected %s", client.host)
_LOGGER.debug("Client connected %s", client.host)
try:
try:
for coordinator in coordinators.values():
await coordinator.state.start()
with client.listen(_listen):
for coordinator in coordinators.values():
await stack.enter_async_context(
coordinator.async_monitor_client()
)
coordinator.async_notify_connected()
await client.process()
finally:
_LOGGER.debug("Client disconnected %s", client.host)
finally:
await client.stop()
_LOGGER.debug("Client disconnected %s", client.host)
for coordinator in coordinators.values():
coordinator.async_notify_disconnected()
except ConnectionFailed:
pass
await asyncio.sleep(interval)
except TimeoutError:
continue
except Exception:
_LOGGER.exception("Unexpected exception, aborting arcam client")
return
await asyncio.sleep(interval)

View File

@@ -2,13 +2,11 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from dataclasses import dataclass
import logging
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import AmxDuetResponse, Client, ResponsePacket
from arcam.fmj.client import Client
from arcam.fmj.state import State
from homeassistant.config_entries import ConfigEntry
@@ -53,7 +51,7 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
)
self.client = client
self.state = State(client, zone)
self.update_in_progress = False
self.last_update_success = False
name = config_entry.title
unique_id = config_entry.unique_id or config_entry.entry_id
@@ -76,34 +74,24 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None:
"""Fetch data for manual refresh."""
try:
self.update_in_progress = True
await self.state.update()
except ConnectionFailed as err:
raise UpdateFailed(
f"Connection failed during update for zone {self.state.zn}"
) from err
finally:
self.update_in_progress = False
@callback
def _async_notify_packet(self, packet: ResponsePacket | AmxDuetResponse) -> None:
"""Packet callback to detect changes to state."""
if (
not isinstance(packet, ResponsePacket)
or packet.zn != self.state.zn
or self.update_in_progress
):
return
def async_notify_data_updated(self) -> None:
"""Notify that new data has been received from the device."""
self.async_set_updated_data(None)
@callback
def async_notify_connected(self) -> None:
"""Handle client connected."""
self.hass.async_create_task(self.async_refresh())
@callback
def async_notify_disconnected(self) -> None:
"""Handle client disconnected."""
self.last_update_success = False
self.async_update_listeners()
@asynccontextmanager
async def async_monitor_client(self) -> AsyncGenerator[None]:
"""Monitor a client and state for changes while connected."""
async with self.state:
self.hass.async_create_task(self.async_refresh())
try:
with self.client.listen(self._async_notify_packet):
yield
finally:
self.hass.async_create_task(self.async_refresh())

View File

@@ -26,8 +26,3 @@ class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
if description is not None:
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
self.entity_description = description
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.client.connected

View File

@@ -142,6 +142,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"person",
"power",
"schedule",
"select",
"siren",
"switch",
"temperature",

View File

@@ -578,13 +578,13 @@ class CalendarEntity(Entity):
return STATE_OFF
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the state machine.
This sets up listeners to handle state transitions for start or end of
the current or upcoming event.
"""
super()._async_write_ha_state()
super().async_write_ha_state()
if self._alarm_unsubs is None:
self._alarm_unsubs = []
_LOGGER.debug(

View File

@@ -760,12 +760,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return CameraCapabilities(frontend_stream_types)
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the state machine.
Schedules async_refresh_providers if support of streams have changed.
"""
super()._async_write_ha_state()
super().async_write_ha_state()
if self.__supports_stream != (
supports_stream := self.supported_features & CameraEntityFeature.STREAM
):

View File

@@ -44,18 +44,18 @@ class DemoRemote(RemoteEntity):
return {"last_command_sent": self._last_command_sent}
return None
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the remote on."""
self._attr_is_on = True
self.async_write_ha_state()
self.schedule_update_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
def turn_off(self, **kwargs: Any) -> None:
"""Turn the remote off."""
self._attr_is_on = False
self.async_write_ha_state()
self.schedule_update_ha_state()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to a device."""
for com in command:
self._last_command_sent = com
self.async_write_ha_state()
self.schedule_update_ha_state()

View File

@@ -61,12 +61,12 @@ class DemoSwitch(SwitchEntity):
name=device_name,
)
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._attr_is_on = True
self.async_write_ha_state()
self.schedule_update_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
self._attr_is_on = False
self.async_write_ha_state()
self.schedule_update_ha_state()

View File

@@ -51,7 +51,6 @@ def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
return a.name not in (
"_cache",
"compat_aliases",
"compat_name",
"original_name_unprefixed",
)

View File

@@ -353,10 +353,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
# Device was de/re-connected, state might have changed
self.async_write_ha_state()
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state."""
self._attr_supported_features = self._supported_features()
super()._async_write_ha_state()
super().async_write_ha_state()
async def _device_connect(self, location: str) -> None:
"""Connect to the device now that it's available."""

View File

@@ -4,12 +4,9 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import api
from .const import DOMAIN, FitbitScope
from .const import FitbitScope
from .coordinator import FitbitConfigEntry, FitbitData, FitbitDeviceCoordinator
from .exceptions import FitbitApiException, FitbitAuthException
from .model import config_from_entry_data
@@ -19,17 +16,11 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: FitbitConfigEntry) -> bool:
"""Set up fitbit from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
fitbit_api = api.OAuthFitbitApi(
hass, session, unit_system=entry.data.get("unit_system")

View File

@@ -121,10 +121,5 @@
"name": "Water"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -3,7 +3,7 @@
import asyncio
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from .coordinator import (
FreshrConfigEntry,
@@ -21,10 +21,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: FreshrConfigEntry) -> bo
await devices_coordinator.async_config_entry_first_refresh()
readings: dict[str, FreshrReadingsCoordinator] = {
device_id: FreshrReadingsCoordinator(
device.id: FreshrReadingsCoordinator(
hass, entry, device, devices_coordinator.client
)
for device_id, device in devices_coordinator.data.items()
for device in devices_coordinator.data
}
await asyncio.gather(
*(
@@ -38,35 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FreshrConfigEntry) -> bo
readings=readings,
)
known_devices: set[str] = set(readings)
@callback
def _handle_coordinator_update() -> None:
current = set(devices_coordinator.data)
removed_ids = known_devices - current
if removed_ids:
known_devices.difference_update(removed_ids)
for device_id in removed_ids:
entry.runtime_data.readings.pop(device_id, None)
new_ids = current - known_devices
if not new_ids:
return
known_devices.update(new_ids)
for device_id in new_ids:
device = devices_coordinator.data[device_id]
readings_coordinator = FreshrReadingsCoordinator(
hass, entry, device, devices_coordinator.client
)
entry.runtime_data.readings[device_id] = readings_coordinator
hass.async_create_task(
readings_coordinator.async_refresh(),
name=f"freshr_readings_refresh_{device_id}",
)
entry.async_on_unload(
devices_coordinator.async_add_listener(_handle_coordinator_update)
)
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True

View File

@@ -12,7 +12,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -33,7 +32,7 @@ class FreshrData:
type FreshrConfigEntry = ConfigEntry[FreshrData]
class FreshrDevicesCoordinator(DataUpdateCoordinator[dict[str, DeviceSummary]]):
class FreshrDevicesCoordinator(DataUpdateCoordinator[list[DeviceSummary]]):
"""Coordinator that refreshes the device list once an hour."""
config_entry: FreshrConfigEntry
@@ -49,7 +48,7 @@ class FreshrDevicesCoordinator(DataUpdateCoordinator[dict[str, DeviceSummary]]):
)
self.client = FreshrClient(session=async_create_clientsession(hass))
async def _async_update_data(self) -> dict[str, DeviceSummary]:
async def _async_update_data(self) -> list[DeviceSummary]:
"""Fetch the list of devices from the Fresh-r API."""
username = self.config_entry.data[CONF_USERNAME]
password = self.config_entry.data[CONF_PASSWORD]
@@ -69,23 +68,8 @@ class FreshrDevicesCoordinator(DataUpdateCoordinator[dict[str, DeviceSummary]]):
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
current = {device.id: device for device in devices}
if self.data is not None:
stale_ids = set(self.data) - set(current)
if stale_ids:
device_registry = dr.async_get(self.hass)
for device_id in stale_ids:
if device := device_registry.async_get_device(
identifiers={(DOMAIN, device_id)}
):
device_registry.async_update_device(
device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
return current
else:
return devices
class FreshrReadingsCoordinator(DataUpdateCoordinator[DeviceReadings]):

View File

@@ -45,9 +45,7 @@ rules:
discovery-update-info:
status: exempt
comment: Integration connects to a cloud service; no local network discovery is possible.
discovery:
status: exempt
comment: No local network discovery of devices is possible (no zeroconf, mdns or other discovery mechanisms).
discovery: todo
docs-data-update: done
docs-examples: done
docs-known-limitations: done
@@ -55,7 +53,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -66,7 +64,7 @@ rules:
repair-issues:
status: exempt
comment: No actionable repair scenarios exist; authentication failures are handled via the reauthentication flow.
stale-devices: done
stale-devices: todo
# Platinum
async-dependency: done

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
UnitOfTemperature,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -112,43 +112,26 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fresh-r sensors from a config entry."""
coordinator = config_entry.runtime_data.devices
known_devices: set[str] = set()
@callback
def _check_devices() -> None:
current = set(coordinator.data)
removed_ids = known_devices - current
if removed_ids:
known_devices.difference_update(removed_ids)
new_ids = current - known_devices
if not new_ids:
return
known_devices.update(new_ids)
entities: list[FreshrSensor] = []
for device_id in new_ids:
device = coordinator.data[device_id]
descriptions = SENSOR_TYPES.get(
device.device_type, SENSOR_TYPES[DeviceType.FRESH_R]
entities: list[FreshrSensor] = []
for device in config_entry.runtime_data.devices.data:
descriptions = SENSOR_TYPES.get(
device.device_type, SENSOR_TYPES[DeviceType.FRESH_R]
)
device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
name=_DEVICE_TYPE_NAMES.get(device.device_type, "Fresh-r"),
serial_number=device.id,
manufacturer="Fresh-r",
)
entities.extend(
FreshrSensor(
config_entry.runtime_data.readings[device.id],
description,
device_info,
)
device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=_DEVICE_TYPE_NAMES.get(device.device_type, "Fresh-r"),
serial_number=device_id,
manufacturer="Fresh-r",
)
entities.extend(
FreshrSensor(
config_entry.runtime_data.readings[device_id],
description,
device_info,
)
for description in descriptions
)
async_add_entities(entities)
_check_devices()
config_entry.async_on_unload(coordinator.async_add_listener(_check_devices))
for description in descriptions
)
async_add_entities(entities)
class FreshrSensor(CoordinatorEntity[FreshrReadingsCoordinator], SensorEntity):

View File

@@ -97,7 +97,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
super().__init__(coordinator, ain)
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the HASS state machine."""
if self.data.holiday_active:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
@@ -109,7 +109,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
self._attr_supported_features = SUPPORTED_FEATURES
self._attr_hvac_modes = HVAC_MODES
self._attr_preset_modes = PRESET_MODES
return super()._async_write_ha_state()
return super().async_write_ha_state()
@property
def current_temperature(self) -> float:

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260325.1"]
"requirements": ["home-assistant-frontend==20260325.2"]
}

View File

@@ -2,19 +2,14 @@
from __future__ import annotations
from aiohttp import ClientError
import aiohttp
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
from homeassistant.components import conversation
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery, intent
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
@@ -56,11 +51,13 @@ async def async_setup_entry(
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except (OAuth2TokenRequestError, ClientError) as err:
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
mem_storage = InMemoryStorage(hass)

View File

@@ -8,6 +8,7 @@ import logging
from typing import Any
import uuid
import aiohttp
from aiohttp import web
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
@@ -25,11 +26,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
ServiceValidationError,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later
@@ -82,8 +79,9 @@ async def async_send_text_commands(
session = entry.runtime_data.session
try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError:
entry.async_start_reauth(hass)
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
entry.async_start_reauth(hass)
raise
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]

View File

@@ -33,18 +33,11 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GooglePhotosConfigEntry
) -> bool:
"""Set up Google Photos from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
web_session = async_get_clientsession(hass)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(web_session, oauth_session)

View File

@@ -68,9 +68,6 @@
"no_access_to_path": {
"message": "Cannot read {filename}, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"upload_error": {
"message": "Failed to upload content: {message}"
}

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from homematicip.base.enums import LockState, SmokeDetectorAlarmType, WindowState
from homematicip.base.enums import SmokeDetectorAlarmType, WindowState
from homematicip.base.functionalChannels import MultiModeInputChannel
from homematicip.device import (
AccelerationSensor,
@@ -74,30 +74,6 @@ SAM_DEVICE_ATTRIBUTES = {
}
def _is_full_flush_lock_controller(device: object) -> bool:
"""Return whether the device is an HmIP-FLC."""
return getattr(device, "modelType", None) == "HmIP-FLC" and hasattr(
device, "functionalChannels"
)
def _get_channel_by_role(
device: object,
functional_channel_type: str,
channel_role: str,
) -> object | None:
"""Return the matching functional channel for the device."""
for channel in getattr(device, "functionalChannels", []):
channel_type = getattr(channel, "functionalChannelType", None)
channel_type_name = getattr(channel_type, "name", channel_type)
if channel_type_name != functional_channel_type:
continue
if getattr(channel, "channelRole", None) != channel_role:
continue
return channel
return None
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
@@ -146,9 +122,6 @@ async def async_setup_entry(
entities.append(
HomematicipPluggableMainsFailureSurveillanceSensor(hap, device)
)
if _is_full_flush_lock_controller(device):
entities.append(HomematicipFullFlushLockControllerLocked(hap, device))
entities.append(HomematicipFullFlushLockControllerGlassBreak(hap, device))
if isinstance(device, PresenceDetectorIndoor):
entities.append(HomematicipPresenceDetector(hap, device))
if isinstance(device, SmokeDetector):
@@ -325,55 +298,6 @@ class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity):
return self._device.motionDetected
class HomematicipFullFlushLockControllerLocked(
HomematicipGenericEntity, BinarySensorEntity
):
"""Representation of the HomematicIP full flush lock controller lock state."""
_attr_device_class = BinarySensorDeviceClass.LOCK
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the full flush lock controller lock sensor."""
super().__init__(hap, device, post="Locked")
@property
def is_on(self) -> bool:
"""Return true if the controlled lock is locked."""
channel = _get_channel_by_role(
self._device,
"MULTI_MODE_LOCK_INPUT_CHANNEL",
"DOOR_LOCK_SENSOR",
)
if channel is None:
return False
lock_state = getattr(channel, "lockState", None)
return getattr(lock_state, "name", lock_state) == LockState.LOCKED.name
class HomematicipFullFlushLockControllerGlassBreak(
HomematicipGenericEntity, BinarySensorEntity
):
"""Representation of the HomematicIP full flush lock controller glass state."""
_attr_device_class = BinarySensorDeviceClass.PROBLEM
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the full flush lock controller glass break sensor."""
super().__init__(hap, device, post="Glass break")
@property
def is_on(self) -> bool:
"""Return true if glass break has been detected."""
channel = _get_channel_by_role(
self._device,
"MULTI_MODE_LOCK_INPUT_CHANNEL",
"DOOR_LOCK_SENSOR",
)
if channel is None:
return False
return bool(getattr(channel, "glassBroken", False))
class HomematicipPresenceDetector(HomematicipGenericEntity, BinarySensorEntity):
"""Representation of the HomematicIP presence detector."""

View File

@@ -12,13 +12,6 @@ from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
def _is_full_flush_lock_controller(device: object) -> bool:
"""Return whether the device is an HmIP-FLC."""
return getattr(device, "modelType", None) == "HmIP-FLC" and hasattr(
device, "send_start_impulse_async"
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
@@ -27,17 +20,11 @@ async def async_setup_entry(
"""Set up the HomematicIP button from a config entry."""
hap = config_entry.runtime_data
entities: list[ButtonEntity] = [
async_add_entities(
HomematicipGarageDoorControllerButton(hap, device)
for device in hap.home.devices
if isinstance(device, WallMountedGarageDoorController)
]
entities.extend(
HomematicipFullFlushLockControllerButton(hap, device)
for device in hap.home.devices
if _is_full_flush_lock_controller(device)
)
async_add_entities(entities)
class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEntity):
@@ -51,16 +38,3 @@ class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEnti
async def async_press(self) -> None:
"""Handle the button press."""
await self._device.send_start_impulse_async()
class HomematicipFullFlushLockControllerButton(HomematicipGenericEntity, ButtonEntity):
"""Representation of the HomematicIP full flush lock controller opener."""
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the full flush lock controller opener button."""
super().__init__(hap, device, post="Door opener")
self._attr_icon = "mdi:door-open"
async def async_press(self) -> None:
"""Handle the button press."""
await self._device.send_start_impulse_async()

View File

@@ -11,6 +11,10 @@ from homeassistant.components.humidifier import (
DOMAIN as HUMIDIFIER_DOMAIN,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.components.weather import (
ATTR_WEATHER_HUMIDITY,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
@@ -24,6 +28,9 @@ HUMIDITY_DOMAIN_SPECS = {
value_source=HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
),
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.HUMIDITY),
WEATHER_DOMAIN: DomainSpec(
value_source=ATTR_WEATHER_HUMIDITY,
),
}
CONDITIONS: dict[str, type[Condition]] = {

View File

@@ -19,6 +19,7 @@ is_value:
device_class: humidity
- domain: climate
- domain: humidifier
- domain: weather
fields:
behavior:
required: true

View File

@@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["idasen-ha==2.6.4"]
"requirements": ["idasen-ha==2.6.5"]
}

View File

@@ -1,7 +1,6 @@
"""Constants for the Matter integration."""
import logging
from typing import Final
from chip.clusters import Objects as clusters
@@ -115,5 +114,3 @@ SERVICE_CREDENTIAL_TYPES = [
CRED_TYPE_FINGER_VEIN,
CRED_TYPE_FACE,
]
CONCENTRATION_BECQUERELS_PER_CUBIC_METER: Final = "Bq/m³"

View File

@@ -140,9 +140,6 @@
"pump_status": {
"default": "mdi:pump"
},
"radon_concentration": {
"default": "mdi:radioactive"
},
"tank_percentage": {
"default": "mdi:water-boiler"
},

View File

@@ -48,7 +48,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util, slugify
from .const import CONCENTRATION_BECQUERELS_PER_CUBIC_METER
from .entity import MatterEntity, MatterEntityDescription
from .helpers import get_matter
from .models import MatterDiscoverySchema
@@ -745,19 +744,6 @@ DISCOVERY_SCHEMAS = [
clusters.OzoneConcentrationMeasurement.Attributes.MeasuredValue,
),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="RadonSensor",
native_unit_of_measurement=CONCENTRATION_BECQUERELS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
translation_key="radon_concentration",
),
entity_class=MatterSensor,
required_attributes=(
clusters.RadonConcentrationMeasurement.Attributes.MeasuredValue,
),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(

View File

@@ -549,9 +549,6 @@
"pump_speed": {
"name": "Rotation speed"
},
"radon_concentration": {
"name": "Radon concentration"
},
"reactive_current": {
"name": "Reactive current"
},

View File

@@ -7,7 +7,6 @@ from typing import cast
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, llm
from .application_credentials import authorization_server_context
@@ -43,14 +42,7 @@ async def _create_token_manager(
hass: HomeAssistant, entry: ModelContextProtocolConfigEntry
) -> TokenManager | None:
"""Create a OAuth token manager for the config entry if the server requires authentication."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
if not implementation:
if not (implementation := await async_get_config_entry_implementation(hass, entry)):
return None
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)

View File

@@ -56,10 +56,5 @@
}
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -270,6 +270,7 @@ class ProgramPhaseOven(MieleEnum, missing_to_none=True):
process_finished = 3078
searing = 3080
roasting = 3081
cooling_down = 3083
energy_save = 3084
pre_heating = 3099
@@ -452,6 +453,7 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
proofing = 27, 10057
sportswear = 29, 10052
automatic_plus = 31
table_linen = 33
outerwear = 37
pillows = 39
cool_air = 45 # washer-dryer
@@ -586,6 +588,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
microwave_fan_grill = 23
conventional_heat = 24
top_heat = 25
booster = 27
fan_grill = 29
bottom_heat = 31
moisture_plus_auto_roast = 35, 48
@@ -594,6 +597,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
moisture_plus_conventional_heat = 51, 76
popcorn = 53
quick_microwave = 54
airfry = 95
custom_program_1 = 97
custom_program_2 = 98
custom_program_3 = 99

View File

@@ -273,6 +273,7 @@
"program_id": {
"name": "Program",
"state": {
"airfry": "AirFry",
"almond_macaroons_1_tray": "Almond macaroons (1 tray)",
"almond_macaroons_2_trays": "Almond macaroons (2 trays)",
"amaranth": "Amaranth",
@@ -334,6 +335,7 @@
"blanching": "Blanching",
"blueberry_muffins": "Blueberry muffins",
"bologna_sausage": "Bologna sausage",
"booster": "Booster",
"bottling": "Bottling",
"bottling_hard": "Bottling (hard)",
"bottling_medium": "Bottling (medium)",
@@ -881,6 +883,7 @@
"swiss_roll": "Swiss roll",
"swiss_toffee_cream_100_ml": "Swiss toffee cream (100 ml)",
"swiss_toffee_cream_150_ml": "Swiss toffee cream (150 ml)",
"table_linen": "Table linen",
"tagliatelli_fresh": "Tagliatelli (fresh)",
"tall_items": "Tall items",
"tart_flambe": "Tart flambè",

View File

@@ -10,6 +10,6 @@
"iot_class": "local_push",
"loggers": ["music_assistant"],
"quality_scale": "bronze",
"requirements": ["music-assistant-client==1.3.4"],
"requirements": ["music-assistant-client==1.3.3"],
"zeroconf": ["_mass._tcp.local."]
}

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
from http import HTTPStatus
import logging
import secrets
from typing import Any
from aiohttp import ClientError
import aiohttp
import pyatmo
from homeassistant.components import cloud
@@ -18,12 +19,7 @@ from homeassistant.components.webhook import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -93,9 +89,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError as ex:
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
except (OAuth2TokenRequestError, ClientError) as ex:
except aiohttp.ClientResponseError as ex:
_LOGGER.warning("API error: %s (%s)", ex.status, ex.message)
if ex.status in (
HTTPStatus.BAD_REQUEST,
HTTPStatus.UNAUTHORIZED,
HTTPStatus.FORBIDDEN,
):
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
raise ConfigEntryNotReady from ex
required_scopes = api.get_api_scopes(entry.data["auth_implementation"])

View File

@@ -56,7 +56,7 @@ class RadarrCalendarEntity(RadarrEntity, CalendarEntity):
return await self.coordinator.async_get_events(start_date, end_date)
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the state machine."""
if self.coordinator.event:
self._attr_extra_state_attributes = {
@@ -64,4 +64,4 @@ class RadarrCalendarEntity(RadarrEntity, CalendarEntity):
}
else:
self._attr_extra_state_attributes = {}
super()._async_write_ha_state()
super().async_write_ha_state()

View File

@@ -0,0 +1,55 @@
"""Provides conditions for selects."""
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.input_select import DOMAIN as INPUT_SELECT_DOMAIN
from homeassistant.const import CONF_OPTIONS
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityStateConditionBase,
)
from .const import CONF_OPTION, DOMAIN
IS_OPTION_SELECTED_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_OPTION): vol.All(
cv.ensure_list, vol.Length(min=1), [str]
),
},
}
)
SELECT_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_SELECT_DOMAIN: DomainSpec()}
class IsOptionSelectedCondition(EntityStateConditionBase):
"""Condition for select option."""
_domain_specs = SELECT_DOMAIN_SPECS
_schema = IS_OPTION_SELECTED_SCHEMA
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize the option selected condition."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._states = set(config.options[CONF_OPTION])
CONDITIONS: dict[str, type[Condition]] = {
"is_option_selected": IsOptionSelectedCondition,
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the select conditions."""
return CONDITIONS

View File

@@ -0,0 +1,26 @@
is_option_selected:
target:
entity:
- domain: select
- domain: input_select
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
option:
context:
filter_target: target
required: true
selector:
state:
attribute: options
hide_states:
- unavailable
- unknown
multiple: true

View File

@@ -1,4 +1,9 @@
{
"conditions": {
"is_option_selected": {
"condition": "mdi:format-list-bulleted"
}
},
"entity_component": {
"_": {
"default": "mdi:format-list-bulleted"

View File

@@ -1,4 +1,20 @@
{
"conditions": {
"is_option_selected": {
"description": "Tests if one or more dropdowns have a specific option selected.",
"fields": {
"behavior": {
"description": "Whether the condition should pass when any or all targeted entities match.",
"name": "Behavior"
},
"option": {
"description": "The options to check for.",
"name": "Option"
}
},
"name": "Option is selected"
}
},
"device_automation": {
"action_type": {
"select_first": "Change {entity_name} to first option",
@@ -36,6 +52,14 @@
"message": "Option {option} is not valid for entity {entity_id}, valid options are: {options}."
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
}
},
"services": {
"select_first": {
"description": "Selects the first option of a select.",

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["sfrbox-api==0.1.1"]
"requirements": ["sfrbox-api==0.1.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["ttn_client==1.3.0"]
"requirements": ["ttn_client==1.2.3"]
}

View File

@@ -138,13 +138,6 @@ async def async_setup_entry(
return True
async def async_unload_entry(
hass: HomeAssistant, entry: WaterFurnaceConfigEntry
) -> bool:
"""Unload a WaterFurnace config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, entry: WaterFurnaceConfigEntry
) -> bool:

View File

@@ -29,7 +29,7 @@ rules:
action-exceptions:
status: exempt
comment: This integration does not have custom service actions.
config-entry-unloading: done
config-entry-unloading: todo
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done

View File

@@ -16,8 +16,8 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0.dev0"
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0b4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)

View File

@@ -868,16 +868,11 @@ def url(
) -> str:
"""Validate an URL."""
url_in = str(value)
parsed = urlparse(url_in)
if parsed.scheme not in _schema_list:
raise vol.Invalid("invalid url")
if urlparse(url_in).scheme in _schema_list:
return cast(str, vol.Schema(vol.Url())(url_in))
try:
_port = parsed.port
except ValueError as err:
raise vol.Invalid("invalid url") from err
return cast(str, vol.Schema(vol.Url())(url_in))
raise vol.Invalid("invalid url")
def configuration_url(value: Any) -> str:

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections import defaultdict
from collections.abc import Iterable, Mapping
from datetime import datetime
@@ -771,6 +772,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
devices: ActiveDeviceRegistryItems
deleted_devices: DeviceRegistryItems[DeletedDeviceEntry]
_device_data: dict[str, DeviceEntry]
_loaded_event: asyncio.Event | None = None
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the device registry."""
@@ -784,6 +786,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
serialize_in_event_loop=False,
)
@callback
def async_setup(self) -> None:
"""Set up the registry."""
self._loaded_event = asyncio.Event()
@callback
def async_get(self, device_id: str) -> DeviceEntry | None:
"""Get device.
@@ -1463,6 +1470,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
async def _async_load(self) -> None:
"""Load the device registry."""
assert self._loaded_event is not None
assert not self._loaded_event.is_set()
async_setup_cleanup(self.hass, self)
data = await self._store.async_load()
@@ -1560,6 +1570,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self.deleted_devices = deleted_devices
self._device_data = devices.data
self._loaded_event.set()
async def async_wait_loaded(self) -> None:
"""Wait until the device registry is fully loaded.
Will only wait if the registry had already been set up.
"""
if self._loaded_event is not None:
await self._loaded_event.wait()
@callback
def _data_to_save(self) -> dict[str, Any]:
"""Return data of device registry to store in a file."""
@@ -1706,9 +1726,14 @@ def async_get(hass: HomeAssistant) -> DeviceRegistry:
return DeviceRegistry(hass)
def async_setup(hass: HomeAssistant) -> None:
"""Set up device registry."""
assert DATA_REGISTRY not in hass.data
async_get(hass).async_setup()
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
"""Load device registry."""
assert DATA_REGISTRY not in hass.data
await async_get(hass).async_load(load_empty=load_empty)

View File

@@ -1040,14 +1040,9 @@ class Entity(
self._async_verify_state_writable()
self._async_write_ha_state()
@final
@callback
def async_write_ha_state(self) -> None:
"""Write the state to the state machine.
Note: Integrations which need to customize state write should
override _async_write_ha_state, not this method.
"""
"""Write the state to the state machine."""
if not self.hass or not self._verified_state_writable:
self._async_verify_state_writable()
if self.hass.loop_thread_id != threading.get_ident():

View File

@@ -80,7 +80,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 21
STORAGE_VERSION_MINOR = 22
STORAGE_KEY = "core.entity_registry"
CLEANUP_INTERVAL = 3600 * 24
@@ -240,7 +240,6 @@ class RegistryEntry:
# For backwards compatibility, should be removed in the future
compat_aliases: list[str] = attr.ib(factory=list, eq=False)
compat_name: str | None = attr.ib(default=None, eq=False)
# original_name_unprefixed is used to store the result of stripping
# the device name prefix from the original_name, if possible.
@@ -413,8 +412,7 @@ class RegistryEntry:
"has_entity_name": self.has_entity_name,
"labels": list(self.labels),
"modified_at": self.modified_at,
"name": self.compat_name,
"name_v2": self.name,
"name": self.name,
"object_id_base": self.object_id_base,
"options": self.options,
"original_device_class": self.original_device_class,
@@ -471,6 +469,7 @@ def _async_get_full_entity_name(
original_name: str | None,
original_name_unprefixed: str | None | UndefinedType = UNDEFINED,
overridden_name: str | None = None,
use_legacy_naming: bool = False,
) -> str:
"""Get full name for an entity.
@@ -480,7 +479,7 @@ def _async_get_full_entity_name(
if name is None and overridden_name is not None:
name = overridden_name
else:
elif not use_legacy_naming or name is None:
device_name: str | None = None
if (
device_id is not None
@@ -533,6 +532,7 @@ def async_get_full_entity_name(
name=entry.name,
original_name=original_name,
original_name_unprefixed=original_name_unprefixed,
use_legacy_naming=True,
)
@@ -660,7 +660,6 @@ class DeletedRegistryEntry:
# For backwards compatibility, should be removed in the future
compat_aliases: list[str] = attr.ib(factory=list, eq=False)
compat_name: str | None = attr.ib(default=None, eq=False)
_cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
@@ -696,8 +695,7 @@ class DeletedRegistryEntry:
"id": self.id,
"labels": list(self.labels),
"modified_at": self.modified_at,
"name": self.compat_name,
"name_v2": self.name,
"name": self.name,
"options": self.options if self.options is not UNDEFINED else {},
"options_undefined": self.options is UNDEFINED,
"orphaned_timestamp": self.orphaned_timestamp,
@@ -850,46 +848,37 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entity in data["entities"]:
entity["object_id_base"] = entity["original_name"]
if old_minor_version < 21:
# Version 1.21 migrates the full name to include device name,
# even if entity name is overwritten by user.
# It also adds support for COMPUTED_NAME in aliases and starts preserving their order.
# To avoid a major version bump, we keep the old name and aliases as-is
# and use new name_v2 and aliases_v2 fields instead.
if old_minor_version == 21:
# Version 1.21 has been reverted.
# It migrated entity names to the new format stored in `name_v2`
# field, automatically stripping any device name prefix present.
# The old name was stored in `name` field for backwards compatibility.
# For users who already migrated to v1.21, we restore old names
# but try to preserve any user renames made since that migration.
device_registry = dr.async_get(self.hass)
for entity in data["entities"]:
alias_to_add: str | None = None
old_name = entity["name"]
name = entity.pop("name_v2")
if (
(name := entity["name"])
(name != old_name)
and (device_id := entity["device_id"]) is not None
and (device := device_registry.async_get(device_id)) is not None
and (device_name := device.name_by_user or device.name)
):
# Strip the device name prefix from the entity name if present,
# and add the full generated name as an alias.
# If the name doesn't have the device name prefix and the
# entity is exposed to a voice assistant, add the previous
# name as an alias instead to preserve backwards compatibility.
if (
new_name := _async_strip_prefix_from_entity_name(
name, device_name
)
) is not None:
name = new_name
elif any(
entity.get("options", {}).get(key, {}).get("should_expose")
for key in ("conversation", "cloud.google_assistant")
):
alias_to_add = name
name = f"{device_name} {name}"
entity["name_v2"] = name
entity["aliases_v2"] = [alias_to_add, *entity["aliases"]]
entity["name"] = name
if old_minor_version < 22:
# Version 1.22 adds support for COMPUTED_NAME in aliases and starts preserving
# their order.
# To avoid a major version bump, we keep the old aliases as-is and use aliases_v2
# field instead.
for entity in data["entities"]:
entity["aliases_v2"] = [None, *entity["aliases"]]
for entity in data["deleted_entities"]:
# We don't know what the device name was, so the only thing we can do
# is to clear the overwritten name to not mislead users.
entity["name_v2"] = None
entity["aliases_v2"] = [None, *entity["aliases"]]
if old_major_version > 1:
@@ -1363,7 +1352,6 @@ class EntityRegistry(BaseRegistry):
area_id = deleted_entity.area_id
categories = deleted_entity.categories
compat_aliases = deleted_entity.compat_aliases
compat_name = deleted_entity.compat_name
created_at = deleted_entity.created_at
device_class = deleted_entity.device_class
if deleted_entity.disabled_by is not UNDEFINED:
@@ -1395,7 +1383,6 @@ class EntityRegistry(BaseRegistry):
area_id = None
categories = {}
compat_aliases = []
compat_name = None
device_class = None
icon = None
labels = set()
@@ -1443,7 +1430,6 @@ class EntityRegistry(BaseRegistry):
categories=categories,
capabilities=none_if_undefined(capabilities),
compat_aliases=compat_aliases,
compat_name=compat_name,
config_entry_id=none_if_undefined(config_entry_id),
config_subentry_id=none_if_undefined(config_subentry_id),
created_at=created_at,
@@ -1506,7 +1492,6 @@ class EntityRegistry(BaseRegistry):
area_id=entity.area_id,
categories=entity.categories,
compat_aliases=entity.compat_aliases,
compat_name=entity.compat_name,
config_entry_id=config_entry_id,
config_subentry_id=entity.config_subentry_id,
created_at=entity.created_at,
@@ -1620,14 +1605,27 @@ class EntityRegistry(BaseRegistry):
for entity in entities:
if entity.has_entity_name:
continue
name = (
entity.original_name_unprefixed
if by_user and entity.name is None
else UNDEFINED
)
# When a user renames a device, update entity names to reflect
# the new device name.
# An empty name_unprefixed means the entity name equals
# the device name (e.g. a main sensor); a non-empty one
# is appended as a suffix.
name: str | None | UndefinedType = UNDEFINED
if (
by_user
and entity.name is None
and (name_unprefixed := entity.original_name_unprefixed) is not None
):
if not name_unprefixed:
name = device_name
elif device_name:
name = f"{device_name} {name_unprefixed}"
original_name_unprefixed = _async_strip_prefix_from_entity_name(
entity.original_name, device_name
)
self._async_update_entity(
entity.entity_id,
name=name,
@@ -1944,6 +1942,10 @@ class EntityRegistry(BaseRegistry):
async def _async_load(self) -> None:
"""Load the entity registry."""
# Device registry must be loaded before entity registry because
# migration and entity processing reference device names.
await dr.async_get(self.hass).async_wait_loaded()
_async_setup_cleanup(self.hass, self)
_async_setup_entity_restore(self.hass, self)
@@ -1991,7 +1993,6 @@ class EntityRegistry(BaseRegistry):
categories=entity["categories"],
capabilities=entity["capabilities"],
compat_aliases=entity["aliases"],
compat_name=entity["name"],
config_entry_id=entity["config_entry_id"],
config_subentry_id=entity["config_subentry_id"],
created_at=datetime.fromisoformat(entity["created_at"]),
@@ -2012,7 +2013,7 @@ class EntityRegistry(BaseRegistry):
has_entity_name=entity["has_entity_name"],
labels=set(entity["labels"]),
modified_at=datetime.fromisoformat(entity["modified_at"]),
name=entity["name_v2"],
name=entity["name"],
object_id_base=entity.get("object_id_base"),
options=entity["options"],
original_device_class=entity["original_device_class"],
@@ -2063,7 +2064,6 @@ class EntityRegistry(BaseRegistry):
area_id=entity["area_id"],
categories=entity["categories"],
compat_aliases=entity["aliases"],
compat_name=entity["name"],
config_entry_id=entity["config_entry_id"],
config_subentry_id=entity["config_subentry_id"],
created_at=datetime.fromisoformat(entity["created_at"]),
@@ -2083,7 +2083,7 @@ class EntityRegistry(BaseRegistry):
id=entity["id"],
labels=set(entity["labels"]),
modified_at=datetime.fromisoformat(entity["modified_at"]),
name=entity["name_v2"],
name=entity["name"],
options=entity["options"]
if not entity["options_undefined"]
else UNDEFINED,

View File

@@ -39,7 +39,7 @@ habluetooth==5.11.1
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260325.1
home-assistant-frontend==20260325.2
home-assistant-intents==2026.3.24
httpx==0.28.1
ifaddr==0.2.0

View File

@@ -55,6 +55,7 @@ def run(args: Sequence[str] | None) -> None:
async def run_command(args: argparse.Namespace) -> None:
"""Run the command."""
hass = HomeAssistant(os.path.join(os.getcwd(), args.config))
dr.async_setup(hass)
await asyncio.gather(dr.async_load(hass), er.async_load(hass))
hass.auth = await auth_manager_from_config(hass, [{"type": "homeassistant"}], [])
provider = hass.auth.auth_providers[0]

View File

@@ -302,6 +302,7 @@ async def async_check_config(config_dir):
hass = core.HomeAssistant(config_dir)
loader.async_setup(hass)
hass.config_entries = ConfigEntries(hass, {})
dr.async_setup(hass)
await ar.async_load(hass)
await dr.async_load(hass)
await er.async_load(hass)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.5.0.dev0"
version = "2026.4.0b4"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

12
requirements_all.txt generated
View File

@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.3.0
aioamazondevices==13.3.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -1229,7 +1229,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260325.1
home-assistant-frontend==20260325.2
# homeassistant.components.conversation
home-assistant-intents==2026.3.24
@@ -1286,7 +1286,7 @@ icalendar==6.3.1
icmplib==3.0
# homeassistant.components.idasen_desk
idasen-ha==2.6.4
idasen-ha==2.6.5
# homeassistant.components.idrive_e2
idrive-e2-client==0.1.1
@@ -1564,7 +1564,7 @@ mozart-api==5.3.1.108.2
mullvad-api==1.0.0
# homeassistant.components.music_assistant
music-assistant-client==1.3.4
music-assistant-client==1.3.3
# homeassistant.components.tts
mutagen==1.47.0
@@ -2933,7 +2933,7 @@ sentry-sdk==2.48.0
serialx==0.6.2
# homeassistant.components.sfr_box
sfrbox-api==0.1.1
sfrbox-api==0.1.0
# homeassistant.components.sharkiq
sharkiq==1.5.0
@@ -3154,7 +3154,7 @@ trmnl==0.1.1
ttls==1.8.3
# homeassistant.components.thethingsnetwork
ttn_client==1.3.0
ttn_client==1.2.3
# homeassistant.components.tuya
tuya-device-handlers==0.0.15

View File

@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.3.0
aioamazondevices==13.3.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -1093,7 +1093,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260325.1
home-assistant-frontend==20260325.2
# homeassistant.components.conversation
home-assistant-intents==2026.3.24
@@ -1144,7 +1144,7 @@ icalendar==6.3.1
icmplib==3.0
# homeassistant.components.idasen_desk
idasen-ha==2.6.4
idasen-ha==2.6.5
# homeassistant.components.idrive_e2
idrive-e2-client==0.1.1
@@ -1377,7 +1377,7 @@ mozart-api==5.3.1.108.2
mullvad-api==1.0.0
# homeassistant.components.music_assistant
music-assistant-client==1.3.4
music-assistant-client==1.3.3
# homeassistant.components.tts
mutagen==1.47.0
@@ -2490,7 +2490,7 @@ sentry-sdk==2.48.0
serialx==0.6.2
# homeassistant.components.sfr_box
sfrbox-api==0.1.1
sfrbox-api==0.1.0
# homeassistant.components.sharkiq
sharkiq==1.5.0
@@ -2666,7 +2666,7 @@ trmnl==0.1.1
ttls==1.8.3
# homeassistant.components.thethingsnetwork
ttn_client==1.3.0
ttn_client==1.2.3
# homeassistant.components.tuya
tuya-device-handlers==0.0.15

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from .model import Config, Integration, IntegrationType
from .model import Config, Integration
BASE = """
# This file is generated by script/hassfest/codeowners.py
@@ -65,7 +65,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config)
for domain in sorted(integrations):
integration = integrations[domain]
if integration.integration_type == IntegrationType.VIRTUAL:
if integration.integration_type == "virtual":
continue
codeowners = integration.manifest["codeowners"]

View File

@@ -6,7 +6,7 @@ import json
from typing import Any
from .brand import validate as validate_brands
from .model import Brand, Config, Integration, IntegrationType
from .model import Brand, Config, Integration
from .serializer import format_python_namespace
UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard"}
@@ -75,7 +75,7 @@ def _generate_and_validate(integrations: dict[str, Integration], config: Config)
_validate_integration(config, integration)
if integration.integration_type == IntegrationType.HELPER:
if integration.integration_type == "helper":
domains["helper"].append(domain)
else:
domains["integration"].append(domain)
@@ -94,8 +94,8 @@ def _populate_brand_integrations(
for domain in sub_integrations:
integration = integrations.get(domain)
if not integration or integration.integration_type in (
IntegrationType.ENTITY,
IntegrationType.SYSTEM,
"entity",
"system",
):
continue
metadata: dict[str, Any] = {
@@ -170,10 +170,7 @@ def _generate_integrations(
result["integration"][domain] = metadata
else: # integration
integration = integrations[domain]
if integration.integration_type in (
IntegrationType.ENTITY,
IntegrationType.SYSTEM,
):
if integration.integration_type in ("entity", "system"):
continue
if integration.translated_name:
@@ -183,7 +180,7 @@ def _generate_integrations(
metadata["integration_type"] = integration.integration_type
if integration.integration_type == IntegrationType.VIRTUAL:
if integration.integration_type == "virtual":
if integration.supported_by:
metadata["supported_by"] = integration.supported_by
if integration.iot_standards:
@@ -198,7 +195,7 @@ def _generate_integrations(
):
metadata["single_config_entry"] = single_config_entry
if integration.integration_type == IntegrationType.HELPER:
if integration.integration_type == "helper":
result["helper"][domain] = metadata
else:
result["integration"][domain] = metadata

View File

@@ -4,7 +4,7 @@ import re
from homeassistant.util.yaml import load_yaml_dict
from .model import Config, Integration, IntegrationType
from .model import Config, Integration
# Non-entity-platform components that belong in base_platforms
EXTRA_BASE_PLATFORMS = {"diagnostics"}
@@ -29,7 +29,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
entity_platforms = {
integration.domain
for integration in integrations.values()
if integration.integration_type == IntegrationType.ENTITY
if integration.manifest.get("integration_type") == "entity"
and integration.domain != "tag"
}

View File

@@ -11,7 +11,7 @@ from voluptuous.humanize import humanize_error
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.icon import convert_shorthand_service_icon
from .model import Config, Integration, IntegrationType
from .model import Config, Integration
from .translations import translation_key_validator
@@ -141,7 +141,7 @@ TRIGGER_ICONS_SCHEMA = cv.schema_with_slug_keys(
def icon_schema(
core_integration: bool, integration_type: IntegrationType, no_entity_platform: bool
core_integration: bool, integration_type: str, no_entity_platform: bool
) -> vol.Schema:
"""Create an icon schema."""
@@ -189,12 +189,8 @@ def icon_schema(
}
)
if integration_type in (
IntegrationType.ENTITY,
IntegrationType.HELPER,
IntegrationType.SYSTEM,
):
if integration_type != IntegrationType.ENTITY or no_entity_platform:
if integration_type in ("entity", "helper", "system"):
if integration_type != "entity" or no_entity_platform:
field = vol.Optional("entity_component")
else:
field = vol.Required("entity_component")
@@ -211,7 +207,7 @@ def icon_schema(
)
}
)
if integration_type not in (IntegrationType.ENTITY, IntegrationType.SYSTEM):
if integration_type not in ("entity", "system"):
schema = schema.extend(
{
vol.Optional("entity"): vol.All(

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from .model import Config, Integration, IntegrationType
from .model import Config, Integration
from .serializer import format_python
@@ -12,12 +12,12 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
if config.specific_integrations:
return
int_type = IntegrationType.ENTITY
int_type = "entity"
domains = [
integration.domain
for integration in integrations.values()
if integration.integration_type == int_type
if integration.manifest.get("integration_type") == int_type
# Tag is type "entity" but has no entity platform
and integration.domain != "tag"
]
@@ -36,7 +36,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
def generate(integrations: dict[str, Integration], config: Config) -> None:
"""Generate integration file."""
int_type = IntegrationType.ENTITY
int_type = "entity"
filename = "entity_platforms"
platform_path = config.root / f"homeassistant/generated/{filename}.py"
platform_path.write_text(config.cache[f"integrations_{int_type}"])

View File

@@ -21,7 +21,7 @@ from homeassistant.const import Platform
from homeassistant.helpers import config_validation as cv
from script.util import sort_manifest as util_sort_manifest
from .model import Config, Integration, IntegrationType, ScaledQualityScaleTiers
from .model import Config, Integration, ScaledQualityScaleTiers
DOCUMENTATION_URL_SCHEMA = "https"
DOCUMENTATION_URL_HOST = "www.home-assistant.io"
@@ -206,7 +206,15 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
vol.Required("domain"): str,
vol.Required("name"): str,
vol.Optional("integration_type", default="hub"): vol.In(
[t.value for t in IntegrationType if t != IntegrationType.VIRTUAL]
[
"device",
"entity",
"hardware",
"helper",
"hub",
"service",
"system",
]
),
vol.Optional("config_flow"): bool,
vol.Optional("mqtt"): [str],
@@ -303,7 +311,7 @@ VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
{
vol.Required("domain"): str,
vol.Required("name"): str,
vol.Required("integration_type"): IntegrationType.VIRTUAL.value,
vol.Required("integration_type"): "virtual",
vol.Exclusive("iot_standards", "virtual_integration"): [
vol.Any("homekit", "zigbee", "zwave")
],
@@ -314,7 +322,7 @@ VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
def manifest_schema(value: dict[str, Any]) -> vol.Schema:
"""Validate integration manifest."""
if value.get("integration_type") == IntegrationType.VIRTUAL:
if value.get("integration_type") == "virtual":
return VIRTUAL_INTEGRATION_MANIFEST_SCHEMA(value)
return INTEGRATION_MANIFEST_SCHEMA(value)
@@ -365,12 +373,12 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
if (
domain not in NO_IOT_CLASS
and "iot_class" not in integration.manifest
and integration.integration_type != IntegrationType.VIRTUAL
and integration.manifest.get("integration_type") != "virtual"
):
integration.add_error("manifest", "Domain is missing an IoT Class")
if (
integration.integration_type == IntegrationType.VIRTUAL
integration.manifest.get("integration_type") == "virtual"
and (supported_by := integration.manifest.get("supported_by"))
and not (core_components_dir / supported_by).exists()
):

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from enum import IntEnum, StrEnum
from enum import IntEnum
import json
import pathlib
from typing import Any, Literal
@@ -200,15 +200,9 @@ class Integration:
return self.manifest.get("supported_by", {})
@property
def integration_type(self) -> IntegrationType:
def integration_type(self) -> str:
"""Get integration_type."""
integration_type = self.manifest.get("integration_type", "hub")
try:
return IntegrationType(integration_type)
except ValueError:
# The manifest validation will catch this as an error, so we can default to
# a valid value here to avoid ValueErrors in other plugins
return IntegrationType.HUB
return self.manifest.get("integration_type", "hub")
@property
def iot_class(self) -> str | None:
@@ -254,19 +248,6 @@ class Integration:
self.manifest_path = manifest_path
class IntegrationType(StrEnum):
"""Supported integration types."""
DEVICE = "device"
ENTITY = "entity"
HARDWARE = "hardware"
HELPER = "helper"
HUB = "hub"
SERVICE = "service"
SYSTEM = "system"
VIRTUAL = "virtual"
class ScaledQualityScaleTiers(IntEnum):
"""Supported manifest quality scales."""

View File

@@ -11,7 +11,7 @@ from homeassistant.const import Platform
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.yaml import load_yaml_dict
from .model import Config, Integration, IntegrationType, ScaledQualityScaleTiers
from .model import Config, Integration, ScaledQualityScaleTiers
from .quality_scale_validation import (
RuleValidationProtocol,
action_setup,
@@ -2200,7 +2200,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
if (
integration.domain not in INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE
and integration.domain not in NO_QUALITY_SCALE
and integration.integration_type != IntegrationType.VIRTUAL
and integration.integration_type != "virtual"
):
integration.add_error(
"quality_scale",
@@ -2218,7 +2218,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
)
return
return
if integration.integration_type == IntegrationType.VIRTUAL:
if integration.integration_type == "virtual":
integration.add_error(
"quality_scale",
"Virtual integrations are not allowed to have a quality scale file.",

View File

@@ -14,7 +14,7 @@ from voluptuous.humanize import humanize_error
import homeassistant.helpers.config_validation as cv
from script.translations import upload
from .model import Config, Integration, IntegrationType
from .model import Config, Integration
UNDEFINED = 0
REQUIRED = 1
@@ -345,9 +345,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
flow_title=REMOVED,
require_step_title=False,
mandatory_description=(
"user"
if integration.integration_type == IntegrationType.HELPER
else None
"user" if integration.integration_type == "helper" else None
),
),
vol.Optional("config_subentries"): cv.schema_with_slug_keys(

View File

@@ -305,6 +305,8 @@ async def async_test_home_assistant(
hass
)
if load_registries:
dr.async_setup(hass)
with (
patch.object(StoreWithoutWriteLoad, "async_load", return_value=None),
patch(

View File

@@ -12,32 +12,12 @@ from homeassistant.components.aladdin_connect import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import init_integration
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:

View File

@@ -1,17 +1,15 @@
"""Tests for the arcam_fmj component."""
from asyncio import CancelledError, Queue
from collections.abc import AsyncGenerator, Generator
from contextlib import contextmanager
from unittest.mock import AsyncMock, Mock, patch
from collections.abc import AsyncGenerator
from unittest.mock import Mock, patch
from arcam.fmj.client import Client, ResponsePacket
from arcam.fmj.client import Client
from arcam.fmj.state import State
import pytest
from homeassistant.components.arcam_fmj.const import DEFAULT_NAME
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -30,50 +28,12 @@ MOCK_CONFIG_ENTRY = {CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}
@pytest.fixture(name="client")
def client_fixture() -> Generator[Mock]:
def client_fixture() -> Mock:
"""Get a mocked client."""
client = Mock(Client)
client.host = MOCK_HOST
client.port = MOCK_PORT
queue = Queue[BaseException | None]()
listeners = set()
async def _start():
client.connected = True
async def _process():
result = await queue.get()
client.connected = False
if isinstance(result, BaseException):
raise result
@contextmanager
def _listen(listener):
listeners.add(listener)
yield client
listeners.remove(listener)
@callback
def _notify_data_updated(zn=1):
packet = Mock(ResponsePacket)
packet.zn = zn
for listener in listeners:
listener(packet)
@callback
def _notify_connection(exception: Exception | None = None):
queue.put_nowait(exception)
client.start.side_effect = _start
client.process.side_effect = _process
client.listen.side_effect = _listen
client.notify_data_updated = _notify_data_updated
client.notify_connection = _notify_connection
yield client
queue.put_nowait(CancelledError())
return client
@pytest.fixture(name="state_1")
@@ -92,8 +52,6 @@ def state_1_fixture(client: Mock) -> State:
state.get_mute.return_value = None
state.get_decode_modes.return_value = []
state.get_decode_mode.return_value = None
state.__aenter__ = AsyncMock()
state.__aexit__ = AsyncMock()
return state
@@ -113,8 +71,6 @@ def state_2_fixture(client: Mock) -> State:
state.get_mute.return_value = None
state.get_decode_modes.return_value = []
state.get_decode_mode.return_value = None
state.__aenter__ = AsyncMock()
state.__aexit__ = AsyncMock()
return state
@@ -148,6 +104,18 @@ async def player_setup_fixture(
return state_2
raise ValueError(f"Unknown player zone: {zone}")
async def _mock_run_client(hass: HomeAssistant, runtime_data, interval):
coordinators = runtime_data.coordinators
def _notify_data_updated() -> None:
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
client.notify_data_updated = _notify_data_updated
for coordinator in coordinators.values():
coordinator.async_notify_connected()
await async_setup_component(hass, "homeassistant", {})
with (
@@ -156,6 +124,10 @@ async def player_setup_fixture(
"homeassistant.components.arcam_fmj.coordinator.State",
side_effect=state_mock,
),
patch(
"homeassistant.components.arcam_fmj._run_client",
side_effect=_mock_run_client,
),
):
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -33,7 +33,7 @@ from homeassistant.components.media_player import (
SERVICE_VOLUME_UP,
MediaType,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant, State as CoreState
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -62,21 +62,6 @@ async def test_setup(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("player_setup")
async def test_disconnect(hass: HomeAssistant, client: Mock) -> None:
"""Test a disconnection is detected."""
data = hass.states.get(MOCK_ENTITY_ID)
assert data
assert data.state != STATE_UNAVAILABLE
client.notify_connection(ConnectionFailed())
await hass.async_block_till_done()
data = hass.states.get(MOCK_ENTITY_ID)
assert data
assert data.state == STATE_UNAVAILABLE
async def update(hass: HomeAssistant, client: Mock, entity_id: str) -> CoreState:
"""Force a update of player and return current state data."""
client.notify_data_updated()

View File

@@ -2,7 +2,6 @@
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from unittest.mock import patch
import pytest
@@ -13,9 +12,6 @@ from homeassistant.components.fitbit.const import (
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from .conftest import (
CLIENT_ID,
@@ -30,23 +26,6 @@ from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup(
hass: HomeAssistant,
integration_setup: Callable[[], Awaitable[bool]],

View File

@@ -40,7 +40,6 @@ def mock_config_entry() -> MockConfigEntry:
domain=DOMAIN,
data={CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass"},
unique_id="test-user",
entry_id="01JKRA6QKPBE00ZZ9BKWDB3CTB",
)

View File

@@ -1,22 +1,14 @@
"""Test the Fresh-r initialization."""
from aiohttp import ClientError
from freezegun.api import FrozenDateTimeFactory
from pyfreshr.exceptions import ApiResponseError, LoginError
import pytest
from homeassistant.components.freshr.const import DOMAIN
from homeassistant.components.freshr.coordinator import (
DEVICES_SCAN_INTERVAL,
READINGS_SCAN_INTERVAL,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import entity_registry as er
from .conftest import DEVICE_ID, MagicMock, MockConfigEntry
from tests.common import async_fire_time_changed
from .conftest import MagicMock, MockConfigEntry
@pytest.mark.usefixtures("init_integration")
@@ -72,47 +64,3 @@ async def test_setup_no_devices(
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
== []
)
@pytest.mark.usefixtures("init_integration")
async def test_stale_device_removed(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_freshr_client: MagicMock,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that a device absent from a successful poll is removed from the registry."""
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})
mock_freshr_client.fetch_devices.return_value = []
freezer.tick(DEVICES_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) is None
call_count = mock_freshr_client.fetch_device_current.call_count
freezer.tick(READINGS_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_freshr_client.fetch_device_current.call_count == call_count
@pytest.mark.usefixtures("init_integration")
async def test_stale_device_not_removed_on_poll_error(
hass: HomeAssistant,
mock_freshr_client: MagicMock,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that a device is not removed when the devices poll fails."""
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})
mock_freshr_client.fetch_devices.side_effect = ApiResponseError("cloud error")
freezer.tick(DEVICES_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})

View File

@@ -5,19 +5,16 @@ from unittest.mock import MagicMock
from aiohttp import ClientError
from freezegun.api import FrozenDateTimeFactory
from pyfreshr.exceptions import ApiResponseError
from pyfreshr.models import DeviceReadings, DeviceSummary
from pyfreshr.models import DeviceReadings
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.freshr.const import DOMAIN
from homeassistant.components.freshr.coordinator import (
DEVICES_SCAN_INTERVAL,
READINGS_SCAN_INTERVAL,
)
from homeassistant.components.freshr.coordinator import READINGS_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import DEVICE_ID, MOCK_DEVICE_CURRENT
from .conftest import DEVICE_ID
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -85,71 +82,3 @@ async def test_readings_connection_error_makes_unavailable(
state = hass.states.get("sensor.fresh_r_inside_temperature")
assert state is not None
assert state.state == "unavailable"
DEVICE_ID_2 = "SN002"
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_device_reappears_after_removal(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_freshr_client: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that entities are re-created when a previously removed device reappears."""
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})
# Device disappears from the account
mock_freshr_client.fetch_devices.return_value = []
freezer.tick(DEVICES_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) is None
# Device reappears
mock_freshr_client.fetch_devices.return_value = [DeviceSummary(id=DEVICE_ID)]
mock_freshr_client.fetch_device_current.return_value = MOCK_DEVICE_CURRENT
freezer.tick(DEVICES_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})
t1_entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{DEVICE_ID}_t1"
)
assert t1_entity_id
assert hass.states.get(t1_entity_id).state != "unavailable"
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_dynamic_device_added(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_freshr_client: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that sensors are created for a device that appears after initial setup."""
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID_2)}) is None
mock_freshr_client.fetch_devices.return_value = [
DeviceSummary(id=DEVICE_ID),
DeviceSummary(id=DEVICE_ID_2),
]
mock_freshr_client.fetch_device_current.return_value = MOCK_DEVICE_CURRENT
freezer.tick(DEVICES_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID_2)})
t1_entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{DEVICE_ID_2}_t1"
)
assert t1_entity_id
assert entity_registry.async_get_entity_id("sensor", DOMAIN, f"{DEVICE_ID_2}_co2")
assert hass.states.get(t1_entity_id).state != "unavailable"

View File

@@ -2,7 +2,6 @@
import http
import time
from unittest.mock import patch
from aiohttp import ClientError
from google_photos_library_api.exceptions import GooglePhotosApiError
@@ -11,7 +10,6 @@ import pytest
from homeassistant.components.google_photos.const import OAUTH2_TOKEN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -120,19 +118,3 @@ async def test_coordinator_init_failure(
) -> None:
"""Test init failure to load albums."""
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_entry_implementation_unavailable(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test setup entry when implementation is unavailable."""
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=config_entry_oauth2_flow.ImplementationUnavailableError,
):
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -1,6 +1,5 @@
"""Initializer helpers for HomematicIP fake server."""
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from homematicip.async_home import AsyncHome
@@ -70,124 +69,6 @@ async def default_mock_hap_factory_fixture(
return HomeFactory(hass, mock_connection, hmip_config_entry)
@pytest.fixture(name="full_flush_lock_controller_device_data")
def full_flush_lock_controller_device_data_fixture() -> dict[str, Any]:
"""Return fixture data for an HmIP-FLC device."""
return {
"availableFirmwareVersion": "0.0.0",
"connectionType": "HMIP_RF",
"deviceArchetype": "HMIP",
"firmwareVersion": "1.0.10",
"firmwareVersionInteger": 65546,
"functionalChannels": {
"0": {
"configPending": False,
"deviceId": "3014F7110000000000000026",
"dutyCycle": False,
"functionalChannelType": "DEVICE_BASE",
"groupIndex": 0,
"groups": [],
"index": 0,
"label": "",
"lowBat": None,
"routerModuleEnabled": False,
"routerModuleSupported": False,
"rssiDeviceValue": -82,
"rssiPeerValue": -97,
"supportedOptionalFeatures": {
"IFeatureRssiValue": True,
"IOptionalFeatureDutyCycle": True,
"IOptionalFeatureLowBat": False,
},
"unreach": False,
},
"1": {
"actionParameter": "NOT_CUSTOMISABLE",
"binaryBehaviorType": "NORMALLY_OPEN",
"channelRole": "DOOR_LOCK_SENSOR",
"corrosionPreventionActive": False,
"deviceId": "3014F7110000000000000026",
"doorBellSensorEventTimestamp": None,
"eventDelay": 0,
"functionalChannelType": "MULTI_MODE_LOCK_INPUT_CHANNEL",
"glassBroken": True,
"groupIndex": 1,
"groups": [],
"index": 1,
"label": "",
"lockState": "LOCKED",
"multiModeInputMode": "BINARY_BEHAVIOR",
"supportedOptionalFeatures": {},
"windowState": "OPEN",
},
"3": {
"channelRole": "DOOR_LOCK_ACTUATOR",
"deviceId": "3014F7110000000000000026",
"doorLockActive": False,
"functionalChannelType": "DOOR_SWITCH_CHANNEL",
"groupIndex": 3,
"groups": [],
"impulseDuration": 111600.0,
"index": 3,
"internalLinkConfiguration": {
"firstInputAction": "TOGGLE",
"internalLinkConfigurationType": "SINGLE_INPUT_DOOR_SWITCH",
},
"label": "",
"multiModeInputMode": "KEY_BEHAVIOR",
"processing": False,
"profileMode": "AUTOMATIC",
"supportedOptionalFeatures": {},
"userDesiredProfileMode": "AUTOMATIC",
},
"4": {
"channelRole": "DOOR_OPENER_ACTUATOR",
"deviceId": "3014F7110000000000000026",
"doorLockActive": False,
"functionalChannelType": "DOOR_SWITCH_CHANNEL",
"groupIndex": 4,
"groups": [],
"impulseDuration": 0.9,
"index": 4,
"internalLinkConfiguration": {
"firstInputAction": "LOCK_OPEN",
"internalLinkConfigurationType": "SINGLE_INPUT_DOOR_SWITCH",
},
"label": "",
"multiModeInputMode": "SWITCH_BEHAVIOR",
"processing": False,
"profileMode": "AUTOMATIC",
"supportedOptionalFeatures": {},
"userDesiredProfileMode": "AUTOMATIC",
},
"5": {
"authorized": True,
"channelRole": "DOOR_LOCK_ACTUATOR",
"deviceId": "3014F7110000000000000026",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 3,
"groups": [],
"index": 5,
"label": "",
"supportedOptionalFeatures": {},
},
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000026",
"label": "Universal Motorschloss Controller",
"lastStatusUpdate": 1760619002144,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
"modelId": 546,
"modelType": "HmIP-FLC",
"oem": "eQ-3",
"permanentlyReachable": True,
"serializedGlobalTradeItemNumber": "3014F7110000000000000026",
"type": "FULL_FLUSH_LOCK_CONTROLLER",
"updateState": "UP_TO_DATE",
}
@pytest.fixture(name="hmip_config")
def hmip_config_fixture() -> ConfigType:
"""Create a config for homematic ip cloud."""

View File

@@ -109,10 +109,7 @@ class HomeFactory:
self.hmip_config_entry = hmip_config_entry
async def async_get_mock_hap(
self,
test_devices=None,
test_groups=None,
extra_devices: list[dict[str, Any]] | None = None,
self, test_devices=None, test_groups=None
) -> HomematicipHAP:
"""Create a mocked homematic access point."""
home_name = self.hmip_config_entry.data["name"]
@@ -122,7 +119,6 @@ class HomeFactory:
home_name=home_name,
test_devices=test_devices,
test_groups=test_groups,
extra_devices=extra_devices,
)
.init_home()
.get_async_home_mock()
@@ -160,12 +156,7 @@ class HomeTemplate(Home):
_typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP
def __init__(
self,
connection=None,
home_name="",
test_devices=None,
test_groups=None,
extra_devices: list[dict[str, Any]] | None = None,
self, connection=None, home_name="", test_devices=None, test_groups=None
) -> None:
"""Init template with connection."""
super().__init__(connection=connection)
@@ -175,12 +166,8 @@ class HomeTemplate(Home):
self.init_json_state = None
self.test_devices = test_devices
self.test_groups = test_groups
self.extra_devices = extra_devices or []
def _cleanup_json(self, json):
for extra_device in self.extra_devices:
json["devices"][extra_device["id"]] = extra_device
if self.test_devices is not None:
new_devices = {}
for json_device in json["devices"].items():

View File

@@ -1,7 +1,5 @@
"""Tests for HomematicIP Cloud binary sensor."""
from typing import Any
from homematicip.base.enums import SmokeDetectorAlarmType, WindowState
from homeassistant.components.homematicip_cloud.binary_sensor import (
@@ -29,49 +27,6 @@ from homeassistant.core import HomeAssistant
from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics
async def test_hmip_full_flush_lock_controller_binary_sensors(
hass: HomeAssistant,
default_mock_hap_factory: HomeFactory,
full_flush_lock_controller_device_data: dict[str, Any],
) -> None:
"""Test HomematicIP full flush lock controller binary sensors."""
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Universal Motorschloss Controller"],
extra_devices=[full_flush_lock_controller_device_data],
)
lock_entity_id = "binary_sensor.universal_motorschloss_controller_locked"
lock_state, hmip_device = get_and_check_entity_basics(
hass,
mock_hap,
lock_entity_id,
"Universal Motorschloss Controller Locked",
"HmIP-FLC",
)
assert lock_state.state == STATE_ON
glass_entity_id = "binary_sensor.universal_motorschloss_controller_glass_break"
glass_state, _ = get_and_check_entity_basics(
hass,
mock_hap,
glass_entity_id,
"Universal Motorschloss Controller Glass break",
"HmIP-FLC",
)
assert glass_state.state == STATE_ON
assert hmip_device is not None
await async_manipulate_test_data(hass, hmip_device, "lockState", "UNLOCKED")
lock_state = hass.states.get(lock_entity_id)
assert lock_state
assert lock_state.state == STATE_OFF
await async_manipulate_test_data(hass, hmip_device, "glassBroken", False)
glass_state = hass.states.get(glass_entity_id)
assert glass_state
assert glass_state.state == STATE_OFF
async def test_hmip_home_cloud_connection_sensor(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:

View File

@@ -1,7 +1,5 @@
"""Tests for HomematicIP Cloud button."""
from typing import Any
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
@@ -43,41 +41,3 @@ async def test_hmip_garage_door_controller_button(
state = hass.states.get(entity_id)
assert state
assert state.state == now.isoformat()
async def test_hmip_full_flush_lock_controller_button(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
default_mock_hap_factory: HomeFactory,
full_flush_lock_controller_device_data: dict[str, Any],
) -> None:
"""Test HomematicIP full flush lock controller opener button."""
entity_id = "button.universal_motorschloss_controller_door_opener"
entity_name = "Universal Motorschloss Controller Door opener"
device_model = "HmIP-FLC"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Universal Motorschloss Controller"],
extra_devices=[full_flush_lock_controller_device_data],
)
get_and_check_entity_basics(hass, mock_hap, entity_id, entity_name, device_model)
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNKNOWN
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
freezer.move_to(now)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
hmip_device = mock_hap.hmip_device_by_entity_id[entity_id]
assert hmip_device.mock_calls[-1][0] == "send_start_impulse_async"
state = hass.states.get(entity_id)
assert state
assert state.state == now.isoformat()

View File

@@ -11,6 +11,7 @@ from homeassistant.components.climate import (
from homeassistant.components.humidifier import (
ATTR_CURRENT_HUMIDITY as HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
)
from homeassistant.components.weather import ATTR_WEATHER_HUMIDITY
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON
from homeassistant.core import HomeAssistant
@@ -48,6 +49,12 @@ async def target_humidifiers(hass: HomeAssistant) -> dict[str, list[str]]:
return await target_entities(hass, "humidifier")
@pytest.fixture
async def target_weathers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple weather entities associated with different targets."""
return await target_entities(hass, "weather")
@pytest.mark.parametrize(
"condition",
[
@@ -275,3 +282,75 @@ async def test_humidity_humidifier_condition_behavior_all(
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("weather"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
parametrize_numerical_attribute_condition_above_below_any(
"humidity.is_value",
"sunny",
ATTR_WEATHER_HUMIDITY,
),
)
async def test_humidity_weather_condition_behavior_any(
hass: HomeAssistant,
target_weathers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test the humidity weather condition with 'any' behavior."""
await assert_condition_behavior_any(
hass,
target_entities=target_weathers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("weather"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
parametrize_numerical_attribute_condition_above_below_all(
"humidity.is_value",
"sunny",
ATTR_WEATHER_HUMIDITY,
),
)
async def test_humidity_weather_condition_behavior_all(
hass: HomeAssistant,
target_weathers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test the humidity weather condition with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_weathers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)

View File

@@ -1,10 +1,9 @@
"""Test the KNX config flow."""
from contextlib import contextmanager
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from unittest.mock import MagicMock, Mock, patch
import pytest
from xknx.exceptions import XKNXException
from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.io.gateway_scanner import GatewayDescriptor
@@ -61,12 +60,6 @@ FIXTURE_UPLOAD_UUID = "0123456789abcdef0123456789abcdef"
GATEWAY_INDIVIDUAL_ADDRESS = IndividualAddress("1.0.0")
async def _mock_validate_ip_for_invalid_local(ip_address: str) -> str:
if ip_address in {"no_local_ip", "asdf"}:
raise XKNXException
return ip_address
@pytest.fixture(name="knx_setup")
def fixture_knx_setup():
"""Mock KNX entry setup."""
@@ -245,19 +238,15 @@ async def test_routing_setup_advanced(
assert result["errors"] == {"base": "no_router_discovered"}
# invalid user input
with patch(
"homeassistant.components.knx.config_flow.xknx_validate_ip",
new=AsyncMock(side_effect=_mock_validate_ip_for_invalid_local),
):
result_invalid_input = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_MCAST_GRP: "10.1.2.3", # no valid multicast group
CONF_KNX_MCAST_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "not_a_valid_address",
CONF_KNX_LOCAL_IP: "no_local_ip",
},
)
result_invalid_input = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_MCAST_GRP: "10.1.2.3", # no valid multicast group
CONF_KNX_MCAST_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "not_a_valid_address",
CONF_KNX_LOCAL_IP: "no_local_ip",
},
)
assert result_invalid_input["type"] is FlowResultType.FORM
assert result_invalid_input["step_id"] == "routing"
assert result_invalid_input["errors"] == {
@@ -762,19 +751,15 @@ async def test_tunneling_setup_for_local_ip(
"base": "no_tunnel_discovered",
}
# invalid local ip address
with patch(
"homeassistant.components.knx.config_flow.xknx_validate_ip",
new=AsyncMock(side_effect=_mock_validate_ip_for_invalid_local),
):
result_invalid_local = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.0.2",
CONF_PORT: 3675,
CONF_KNX_LOCAL_IP: "asdf",
},
)
result_invalid_local = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.0.2",
CONF_PORT: 3675,
CONF_KNX_LOCAL_IP: "asdf",
},
)
assert result_invalid_local["type"] is FlowResultType.FORM
assert result_invalid_local["step_id"] == "manual_tunnel"
assert result_invalid_local["errors"] == {

View File

@@ -197,7 +197,7 @@
"1": 1
}
],
"1/29/1": [3, 29, 91, 1026, 1029, 1037, 1043, 1066, 1068, 1069, 1070, 1071],
"1/29/1": [3, 29, 91, 1026, 1029, 1037, 1043, 1066, 1068, 1069, 1070],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
@@ -274,15 +274,7 @@
"1/1070/65533": 3,
"1/1070/65528": [],
"1/1070/65529": [],
"1/1070/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"1/1071/0": 60.0,
"1/1071/1": 0.0,
"1/1071/2": 500.0,
"1/1071/65532": 1,
"1/1071/65533": 3,
"1/1071/65528": [],
"1/1071/65529": [],
"1/1071/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533]
"1/1070/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533]
},
"attribute_subscriptions": [
[1, 1037, 0],
@@ -291,7 +283,6 @@
[1, 1068, 0],
[1, 1069, 0],
[1, 1026, 0],
[1, 1029, 0],
[1, 1071, 0]
[1, 1029, 0]
]
}

View File

@@ -397,60 +397,6 @@
'state': '3.0',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_radon_concentration-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_radon_concentration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Radon concentration',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Radon concentration',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'radon_concentration',
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-RadonSensor-1071-0',
'unit_of_measurement': 'Bq/m³',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_radon_concentration-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'lightfi-aq1-air-quality-sensor Radon concentration',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'Bq/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_radon_concentration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60.0',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -8436,60 +8382,6 @@
'state': '2.0',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_radon_concentration-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_air_purifier_radon_concentration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Radon concentration',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Radon concentration',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'radon_concentration',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-RadonSensor-1071-0',
'unit_of_measurement': 'Bq/m³',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_radon_concentration-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Air Purifier Radon concentration',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'Bq/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_air_purifier_radon_concentration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2.0',
})
# ---
# name: test_sensors[mock_air_purifier][sensor.mock_air_purifier_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -359,18 +359,6 @@ async def test_air_quality_sensor(
assert state
assert state.state == "50.0"
# Radon
state = hass.states.get("sensor.lightfi_aq1_air_quality_sensor_radon_concentration")
assert state
assert state.state == "60.0"
set_node_attribute(matter_node, 1, 1071, 0, 50)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.lightfi_aq1_air_quality_sensor_radon_concentration")
assert state
assert state.state == "50.0"
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_tvoc_level_sensor(

View File

@@ -13,9 +13,6 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import llm
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from .conftest import TEST_API_NAME
@@ -345,19 +342,3 @@ async def test_convert_tool_schema_fails(
):
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_implementation_not_available(
hass: HomeAssistant,
config_entry_with_auth: MockConfigEntry,
mock_mcp_client: AsyncMock,
) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
with patch(
"homeassistant.components.mcp.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry_with_auth.entry_id)
await hass.async_block_till_done()
assert config_entry_with_auth.state is ConfigEntryState.SETUP_RETRY

View File

@@ -5243,6 +5243,7 @@
'area_id': None,
'capabilities': dict({
'options': list([
'airfry',
'almond_macaroons_1_tray',
'almond_macaroons_2_trays',
'amaranth',
@@ -5294,6 +5295,7 @@
'blanching',
'blueberry_muffins',
'bologna_sausage',
'booster',
'bottling',
'bottling_hard',
'bottling_medium',
@@ -5856,6 +5858,7 @@
'device_class': 'enum',
'friendly_name': 'Oven Program',
'options': list([
'airfry',
'almond_macaroons_1_tray',
'almond_macaroons_2_trays',
'amaranth',
@@ -5907,6 +5910,7 @@
'blanching',
'blueberry_muffins',
'bologna_sausage',
'booster',
'bottling',
'bottling_hard',
'bottling_medium',
@@ -6449,6 +6453,7 @@
'area_id': None,
'capabilities': dict({
'options': list([
'cooling_down',
'energy_save',
'heating_up',
'not_running',
@@ -6495,6 +6500,7 @@
'device_class': 'enum',
'friendly_name': 'Oven Program phase',
'options': list([
'cooling_down',
'energy_save',
'heating_up',
'not_running',
@@ -7624,6 +7630,7 @@
'starch',
'steam_care',
'stuffed_toys',
'table_linen',
'trainers',
'trainers_refresh',
'warm_air',
@@ -7707,6 +7714,7 @@
'starch',
'steam_care',
'stuffed_toys',
'table_linen',
'trainers',
'trainers_refresh',
'warm_air',
@@ -9041,6 +9049,7 @@
'area_id': None,
'capabilities': dict({
'options': list([
'airfry',
'almond_macaroons_1_tray',
'almond_macaroons_2_trays',
'amaranth',
@@ -9092,6 +9101,7 @@
'blanching',
'blueberry_muffins',
'bologna_sausage',
'booster',
'bottling',
'bottling_hard',
'bottling_medium',
@@ -9654,6 +9664,7 @@
'device_class': 'enum',
'friendly_name': 'Oven Program',
'options': list([
'airfry',
'almond_macaroons_1_tray',
'almond_macaroons_2_trays',
'amaranth',
@@ -9705,6 +9716,7 @@
'blanching',
'blueberry_muffins',
'bologna_sausage',
'booster',
'bottling',
'bottling_hard',
'bottling_medium',
@@ -10247,6 +10259,7 @@
'area_id': None,
'capabilities': dict({
'options': list([
'cooling_down',
'energy_save',
'heating_up',
'not_running',
@@ -10293,6 +10306,7 @@
'device_class': 'enum',
'friendly_name': 'Oven Program phase',
'options': list([
'cooling_down',
'energy_save',
'heating_up',
'not_running',
@@ -11422,6 +11436,7 @@
'starch',
'steam_care',
'stuffed_toys',
'table_linen',
'trainers',
'trainers_refresh',
'warm_air',
@@ -11505,6 +11520,7 @@
'starch',
'steam_care',
'stuffed_toys',
'table_linen',
'trainers',
'trainers_refresh',
'warm_air',

View File

@@ -160,7 +160,6 @@ async def test_config_tcp(hass: HomeAssistant) -> None:
"homeassistant.components.mysensors.config_flow.try_connect",
return_value=True,
),
patch("homeassistant.components.mysensors.gateway.socket.getaddrinfo"),
patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
@@ -199,7 +198,6 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None:
"homeassistant.components.mysensors.config_flow.try_connect",
return_value=False,
),
patch("homeassistant.components.mysensors.gateway.socket.getaddrinfo"),
patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
@@ -679,7 +677,6 @@ async def test_duplicate(
"homeassistant.components.mysensors.config_flow.try_connect",
return_value=True,
),
patch("homeassistant.components.mysensors.gateway.socket.getaddrinfo"),
patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,

View File

@@ -15,7 +15,6 @@ from homeassistant.components.netatmo import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_WEBHOOK_ID, Platform
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import OAuth2TokenRequestReauthError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -447,7 +446,7 @@ async def test_setup_component_invalid_token(
"""Test handling of invalid token."""
async def fake_ensure_valid_token(*args, **kwargs):
raise OAuth2TokenRequestReauthError(
raise aiohttp.ClientResponseError(
request_info=aiohttp.client.RequestInfo(
url="http://example.com",
method="GET",
@@ -456,7 +455,6 @@ async def test_setup_component_invalid_token(
),
status=400,
history=(),
domain="netatmo",
)
with (

View File

@@ -27,9 +27,3 @@ async def test_binary_sensors(
entry = await init_integration(hass, mock_nuki_requests)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
# Unload the config entry after taking a snapshot is required because the integration may cache
# DNS results or keep references to the original gethostbyname, so unloading ensures the patch
# is effective for subsequent tests and avoids DNS lookups
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -25,9 +25,3 @@ async def test_locks(
entry = await init_integration(hass, mock_nuki_requests)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
# Unload the config entry after taking a snapshot is required because the integration may cache
# DNS results or keep references to the original gethostbyname, so unloading ensures the patch
# is effective for subsequent tests and avoids DNS lookups
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -25,9 +25,3 @@ async def test_sensors(
entry = await init_integration(hass, mock_nuki_requests)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
# Unload the config entry after taking a snapshot is required because the integration may cache
# DNS results or keep references to the original gethostbyname, so unloading ensures the patch
# is effective for subsequent tests and avoids DNS lookups
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -0,0 +1,284 @@
"""Test select conditions."""
from contextlib import AbstractContextManager, nullcontext as does_not_raise
from typing import Any
import pytest
import voluptuous as vol
from homeassistant.components.select.condition import CONF_OPTION
from homeassistant.const import CONF_ENTITY_ID, CONF_OPTIONS, CONF_TARGET
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import async_validate_condition_config
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
target_entities,
)
@pytest.fixture
async def target_selects(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple select entities associated with different targets."""
return await target_entities(hass, "select")
@pytest.fixture
async def target_input_selects(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple input_select entities associated with different targets."""
return await target_entities(hass, "input_select")
@pytest.mark.parametrize(
"condition",
["select.is_option_selected"],
)
async def test_select_conditions_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
) -> None:
"""Test the select conditions are gated by the labs flag."""
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("select"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
parametrize_condition_states_any(
condition="select.is_option_selected",
condition_options={CONF_OPTION: ["option_a", "option_b"]},
target_states=["option_a", "option_b"],
other_states=["option_c"],
),
)
async def test_select_condition_behavior_any(
hass: HomeAssistant,
target_selects: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test the select condition with 'any' behavior."""
await assert_condition_behavior_any(
hass,
target_entities=target_selects,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("select"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
parametrize_condition_states_all(
condition="select.is_option_selected",
condition_options={CONF_OPTION: ["option_a", "option_b"]},
target_states=["option_a", "option_b"],
other_states=["option_c"],
),
)
async def test_select_condition_behavior_all(
hass: HomeAssistant,
target_selects: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test the select condition with 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_selects,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("input_select"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
parametrize_condition_states_any(
condition="select.is_option_selected",
condition_options={CONF_OPTION: ["option_a", "option_b"]},
target_states=["option_a", "option_b"],
other_states=["option_c"],
),
)
async def test_input_select_condition_behavior_any(
hass: HomeAssistant,
target_input_selects: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test the select condition with input_select entities and 'any' behavior."""
await assert_condition_behavior_any(
hass,
target_entities=target_input_selects,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("input_select"),
)
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
parametrize_condition_states_all(
condition="select.is_option_selected",
condition_options={CONF_OPTION: ["option_a", "option_b"]},
target_states=["option_a", "option_b"],
other_states=["option_c"],
),
)
async def test_input_select_condition_behavior_all(
hass: HomeAssistant,
target_input_selects: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test the select condition with input_select entities and 'all' behavior."""
await assert_condition_behavior_all(
hass,
target_entities=target_input_selects,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
)
# --- Cross-domain test ---
@pytest.mark.usefixtures("enable_labs_preview_features")
async def test_select_condition_evaluates_both_domains(
hass: HomeAssistant,
) -> None:
"""Test that the select condition evaluates both select and input_select entities."""
entity_id_select = "select.test_select"
entity_id_input_select = "input_select.test_input_select"
hass.states.async_set(entity_id_select, "option_a")
hass.states.async_set(entity_id_input_select, "option_a")
await hass.async_block_till_done()
cond = await create_target_condition(
hass,
condition="select.is_option_selected",
target={CONF_ENTITY_ID: [entity_id_select, entity_id_input_select]},
behavior="any",
condition_options={CONF_OPTION: ["option_a", "option_b"]},
)
assert cond(hass) is True
# Set one to a non-matching option - "any" behavior should still pass
hass.states.async_set(entity_id_select, "option_c")
await hass.async_block_till_done()
assert cond(hass) is True
# Set both to non-matching options
hass.states.async_set(entity_id_input_select, "option_c")
await hass.async_block_till_done()
assert cond(hass) is False
# --- Schema validation tests ---
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("condition", "condition_options", "expected_result"),
[
# Valid configurations
(
"select.is_option_selected",
{CONF_OPTION: ["option_a", "option_b"]},
does_not_raise(),
),
(
"select.is_option_selected",
{CONF_OPTION: "option_a"},
does_not_raise(),
),
# Invalid configurations
(
"select.is_option_selected",
# Empty option list
{CONF_OPTION: []},
pytest.raises(vol.Invalid),
),
(
"select.is_option_selected",
# Missing CONF_OPTION
{},
pytest.raises(vol.Invalid),
),
],
)
async def test_select_is_option_selected_condition_validation(
hass: HomeAssistant,
condition: str,
condition_options: dict[str, Any],
expected_result: AbstractContextManager,
) -> None:
"""Test select is_option_selected condition config validation."""
with expected_result:
await async_validate_condition_config(
hass,
{
"condition": condition,
CONF_TARGET: {CONF_ENTITY_ID: "select.test"},
CONF_OPTIONS: condition_options,
},
)

View File

@@ -62,13 +62,11 @@ async def test_light_service_calls(hass: HomeAssistant) -> None:
assert hass.states.get("light.light_switch").state == "on"
await common.async_toggle(hass, "light.light_switch")
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == "off"
assert hass.states.get("light.light_switch").state == "off"
await common.async_turn_on(hass, "light.light_switch")
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == "on"
assert hass.states.get("light.light_switch").state == "on"

View File

@@ -77,7 +77,6 @@ async def test_service_calls(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "cover.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("cover.decorative_lights").state == CoverState.CLOSED
@@ -88,7 +87,6 @@ async def test_service_calls(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "cover.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_ON
assert hass.states.get("cover.decorative_lights").state == CoverState.OPEN
@@ -99,7 +97,6 @@ async def test_service_calls(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "cover.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("cover.decorative_lights").state == CoverState.CLOSED
@@ -110,7 +107,6 @@ async def test_service_calls(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "switch.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_ON
assert hass.states.get("cover.decorative_lights").state == CoverState.OPEN
@@ -121,7 +117,6 @@ async def test_service_calls(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "switch.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("cover.decorative_lights").state == CoverState.CLOSED
@@ -132,7 +127,6 @@ async def test_service_calls(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "switch.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_ON
assert hass.states.get("cover.decorative_lights").state == CoverState.OPEN
@@ -166,7 +160,6 @@ async def test_service_calls_inverted(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "cover.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("cover.decorative_lights").state == CoverState.OPEN
@@ -177,7 +170,6 @@ async def test_service_calls_inverted(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "cover.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("cover.decorative_lights").state == CoverState.OPEN
@@ -188,7 +180,6 @@ async def test_service_calls_inverted(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "cover.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_ON
assert hass.states.get("cover.decorative_lights").state == CoverState.CLOSED
@@ -199,7 +190,6 @@ async def test_service_calls_inverted(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "switch.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_ON
assert hass.states.get("cover.decorative_lights").state == CoverState.CLOSED
@@ -210,7 +200,6 @@ async def test_service_calls_inverted(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "switch.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("cover.decorative_lights").state == CoverState.OPEN
@@ -221,7 +210,6 @@ async def test_service_calls_inverted(hass: HomeAssistant) -> None:
{CONF_ENTITY_ID: "switch.decorative_lights"},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get("switch.decorative_lights").state == STATE_ON
assert hass.states.get("cover.decorative_lights").state == CoverState.CLOSED

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