mirror of
https://github.com/home-assistant/core.git
synced 2026-03-28 10:30:30 +01:00
Compare commits
62 Commits
entity_mar
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c830320730 | ||
|
|
336aa0f5df | ||
|
|
754291b34f | ||
|
|
bbae0862b0 | ||
|
|
6b7693b2fd | ||
|
|
954926a05c | ||
|
|
71981f66ec | ||
|
|
7f94f95ac9 | ||
|
|
4ee3177c5d | ||
|
|
9c1f9ca5c6 | ||
|
|
cff4cf4d2c | ||
|
|
ee9d9781ee | ||
|
|
1b972d4adc | ||
|
|
72598479d5 | ||
|
|
02599a4a6e | ||
|
|
af9f351fce | ||
|
|
ff79943776 | ||
|
|
e60048ef30 | ||
|
|
24c0b22038 | ||
|
|
6f32a53742 | ||
|
|
da9d1080d9 | ||
|
|
2ea4d7913e | ||
|
|
16999e3707 | ||
|
|
5c53b847dc | ||
|
|
3afd763d16 | ||
|
|
75a15ed24e | ||
|
|
6d56597a2a | ||
|
|
5872222213 | ||
|
|
bd5c73fd7b | ||
|
|
d8a32dcf69 | ||
|
|
87cd90ab5d | ||
|
|
cb5b0c5b5e | ||
|
|
2fa16101f4 | ||
|
|
6dd5c30b49 | ||
|
|
72f5a572eb | ||
|
|
d501d8cb28 | ||
|
|
35c4b4ff5b | ||
|
|
f3e8ac5b8e | ||
|
|
ab2bcd84c6 | ||
|
|
cdf7b013a9 | ||
|
|
eeba0467a1 | ||
|
|
43ca72bf7e | ||
|
|
aa9e279026 | ||
|
|
9f3917830d | ||
|
|
c458bc2ee3 | ||
|
|
e0455629d7 | ||
|
|
b802dcba8d | ||
|
|
7ff868e94c | ||
|
|
44bd3e3d74 | ||
|
|
9d793ce1df | ||
|
|
d8dee8fc91 | ||
|
|
3c52acb825 | ||
|
|
cb195be6ad | ||
|
|
08f7bed679 | ||
|
|
744563c7a7 | ||
|
|
5d48801645 | ||
|
|
4211686c07 | ||
|
|
98379c9642 | ||
|
|
a3c9d35a13 | ||
|
|
5a7abc0a92 | ||
|
|
ade73ec159 | ||
|
|
6f7a5d9320 |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -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
|
||||
|
||||
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.0"]
|
||||
"requirements": ["aioamazondevices==13.3.1"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -142,6 +142,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"person",
|
||||
"power",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -121,10 +121,5 @@
|
||||
"name": "Water"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]] = {
|
||||
|
||||
@@ -19,6 +19,7 @@ is_value:
|
||||
device_class: humidity
|
||||
- domain: climate
|
||||
- domain: humidifier
|
||||
- domain: weather
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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³"
|
||||
|
||||
@@ -140,9 +140,6 @@
|
||||
"pump_status": {
|
||||
"default": "mdi:pump"
|
||||
},
|
||||
"radon_concentration": {
|
||||
"default": "mdi:radioactive"
|
||||
},
|
||||
"tank_percentage": {
|
||||
"default": "mdi:water-boiler"
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -549,9 +549,6 @@
|
||||
"pump_speed": {
|
||||
"name": "Rotation speed"
|
||||
},
|
||||
"radon_concentration": {
|
||||
"name": "Radon concentration"
|
||||
},
|
||||
"reactive_current": {
|
||||
"name": "Reactive current"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -56,10 +56,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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è",
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()
|
||||
|
||||
55
homeassistant/components/select/condition.py
Normal file
55
homeassistant/components/select/condition.py
Normal 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
|
||||
26
homeassistant/components/select/conditions.yaml
Normal file
26
homeassistant/components/select/conditions.yaml
Normal 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
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_option_selected": {
|
||||
"condition": "mdi:format-list-bulleted"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:format-list-bulleted"
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
12
requirements_all.txt
generated
@@ -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
|
||||
|
||||
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}"])
|
||||
|
||||
@@ -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()
|
||||
):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"] == {
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
284
tests/components/select/test_condition.py
Normal file
284
tests/components/select/test_condition.py
Normal 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,
|
||||
},
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user