mirror of
https://github.com/home-assistant/core.git
synced 2025-12-13 03:18:08 +00:00
Compare commits
1 Commits
knx-data-s
...
add-app-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a209b98d1c |
12
.github/workflows/builder.yml
vendored
12
.github/workflows/builder.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
architectures: ${{ env.ARCHITECTURES }}
|
architectures: ${{ env.ARCHITECTURES }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -273,7 +273,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -311,7 +311,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -464,7 +464,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
@@ -509,7 +509,7 @@ jobs:
|
|||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
|
|||||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -41,8 +41,8 @@ env:
|
|||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2026.1"
|
HA_SHORT_VERSION: "2026.1"
|
||||||
DEFAULT_PYTHON: "3.13.9"
|
DEFAULT_PYTHON: "3.13"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13.9', '3.14.0']"
|
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
# 10.6 is the current long-term-support
|
# 10.6 is the current long-term-support
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Check out code from GitHub
|
name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
uses: actions/ai-inference@02c6cc30ae592ce65ee356387748dfc2fd5f7993 # v2.0.3
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
uses: actions/ai-inference@02c6cc30ae592ce65ee356387748dfc2fd5f7993 # v2.0.3
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
|||||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
|
|||||||
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Checkout the repository
|
name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
uses: &home-assistant-wheels home-assistant/wheels@6066c17a2a4aafcf7bdfeae01717f63adfcdba98 # 2025.11.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
|||||||
3
CODEOWNERS
generated
3
CODEOWNERS
generated
@@ -539,8 +539,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
/tests/components/freedompro/ @stefano055415
|
/tests/components/freedompro/ @stefano055415
|
||||||
/homeassistant/components/fressnapf_tracker/ @eifinger
|
|
||||||
/tests/components/fressnapf_tracker/ @eifinger
|
|
||||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||||
@@ -1763,7 +1761,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
/tests/components/vilfo/ @ManneW
|
/tests/components/vilfo/ @ManneW
|
||||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||||
/tests/components/vivotek/ @HarlemSquirrel
|
|
||||||
/homeassistant/components/vizio/ @raman325
|
/homeassistant/components/vizio/ @raman325
|
||||||
/tests/components/vizio/ @raman325
|
/tests/components/vizio/ @raman325
|
||||||
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import Any, Final
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_COMPONENT_LOADED,
|
EVENT_COMPONENT_LOADED,
|
||||||
EVENT_CORE_CONFIG_UPDATE,
|
EVENT_CORE_CONFIG_UPDATE,
|
||||||
EVENT_LABS_UPDATED,
|
|
||||||
EVENT_LOVELACE_UPDATED,
|
EVENT_LOVELACE_UPDATED,
|
||||||
EVENT_PANELS_UPDATED,
|
EVENT_PANELS_UPDATED,
|
||||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||||
@@ -46,7 +45,6 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
|
|||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
EVENT_THEMES_UPDATED,
|
EVENT_THEMES_UPDATED,
|
||||||
EVENT_LABEL_REGISTRY_UPDATED,
|
EVENT_LABEL_REGISTRY_UPDATED,
|
||||||
EVENT_LABS_UPDATED,
|
|
||||||
EVENT_CATEGORY_REGISTRY_UPDATED,
|
EVENT_CATEGORY_REGISTRY_UPDATED,
|
||||||
EVENT_FLOOR_REGISTRY_UPDATED,
|
EVENT_FLOOR_REGISTRY_UPDATED,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -175,56 +174,6 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
|
|||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, entry_data: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication upon an API authentication error."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm reauthentication dialog."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
reauth_entry = self._get_reauth_entry()
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
# Combine existing data with new password
|
|
||||||
data = {
|
|
||||||
CONF_HOST: reauth_entry.data[CONF_HOST],
|
|
||||||
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
|
|
||||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
await validate_input(self.hass, data)
|
|
||||||
except CannotConnect:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except InvalidAuth:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
reauth_entry,
|
|
||||||
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PASSWORD): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
description_placeholders={
|
|
||||||
"username": reauth_entry.data[CONF_USERNAME],
|
|
||||||
"host": reauth_entry.data[CONF_HOST],
|
|
||||||
},
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
class CannotConnect(HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@@ -54,15 +53,7 @@ class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
|
|||||||
try:
|
try:
|
||||||
status = await self.client.get_statuses()
|
status = await self.client.get_statuses()
|
||||||
settings = await self.client.get_settings()
|
settings = await self.client.get_settings()
|
||||||
except AirobotAuthError as err:
|
except (AirobotAuthError, AirobotConnectionError) as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise UpdateFailed(f"Failed to communicate with device: {err}") from err
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="authentication_failed",
|
|
||||||
) from err
|
|
||||||
except AirobotConnectionError as err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="connection_failed",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
return AirobotData(status=status, settings=settings)
|
return AirobotData(status=status, settings=settings)
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyairobotrest"],
|
"loggers": ["pyairobotrest"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyairobotrest==0.1.0"]
|
"requirements": ["pyairobotrest==0.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: done
|
reauthentication-flow: todo
|
||||||
test-coverage: done
|
test-coverage: done
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
@@ -15,24 +14,15 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
"password": "The thermostat password."
|
||||||
},
|
},
|
||||||
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
|
||||||
},
|
|
||||||
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
|
||||||
},
|
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"username": "Device ID"
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of your Airobot thermostat.",
|
"host": "The hostname or IP address of your Airobot thermostat.",
|
||||||
@@ -44,12 +34,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"authentication_failed": {
|
|
||||||
"message": "Authentication failed, please reauthenticate."
|
|
||||||
},
|
|
||||||
"connection_failed": {
|
|
||||||
"message": "Failed to communicate with device."
|
|
||||||
},
|
|
||||||
"set_preset_mode_failed": {
|
"set_preset_mode_failed": {
|
||||||
"message": "Failed to set preset mode to {preset_mode}."
|
"message": "Failed to set preset mode to {preset_mode}."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -159,74 +159,81 @@
|
|||||||
"title": "Alarm control panel",
|
"title": "Alarm control panel",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"armed": {
|
"armed": {
|
||||||
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
"description": "Triggers when an alarm is armed.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::armed::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm armed"
|
"name": "When an alarm is armed"
|
||||||
},
|
},
|
||||||
"armed_away": {
|
"armed_away": {
|
||||||
"description": "Triggers after one or more alarms become armed in away mode.",
|
"description": "Triggers when an alarm is armed away.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_away::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm armed away"
|
"name": "When an alarm is armed away"
|
||||||
},
|
},
|
||||||
"armed_home": {
|
"armed_home": {
|
||||||
"description": "Triggers after one or more alarms become armed in home mode.",
|
"description": "Triggers when an alarm is armed home.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_home::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm armed home"
|
"name": "When an alarm is armed home"
|
||||||
},
|
},
|
||||||
"armed_night": {
|
"armed_night": {
|
||||||
"description": "Triggers after one or more alarms become armed in night mode.",
|
"description": "Triggers when an alarm is armed night.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_night::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm armed night"
|
"name": "When an alarm is armed night"
|
||||||
},
|
},
|
||||||
"armed_vacation": {
|
"armed_vacation": {
|
||||||
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
"description": "Triggers when an alarm is armed vacation.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_vacation::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm armed vacation"
|
"name": "When an alarm is armed vacation"
|
||||||
},
|
},
|
||||||
"disarmed": {
|
"disarmed": {
|
||||||
"description": "Triggers after one or more alarms become disarmed.",
|
"description": "Triggers when an alarm is disarmed.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::disarmed::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm disarmed"
|
"name": "When an alarm is disarmed"
|
||||||
},
|
},
|
||||||
"triggered": {
|
"triggered": {
|
||||||
"description": "Triggers after one or more alarms become triggered.",
|
"description": "Triggers when an alarm is triggered.",
|
||||||
|
"description_configured": "[%key:component::alarm_control_panel::triggers::triggered::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Alarm triggered"
|
"name": "When an alarm is triggered"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"account_number": "Your account number found on your latest bill.",
|
"account_number": "Your account number found on your latest bill.",
|
||||||
"password": "Your password",
|
"password": "Your password",
|
||||||
"username": "Username or email used to log in to the Anglian Water website."
|
"username": "Username or email used to login to the Anglian Water website."
|
||||||
},
|
},
|
||||||
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
|
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["zeroconf"],
|
"dependencies": ["zeroconf"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyatv", "srptools"],
|
"loggers": ["pyatv", "srptools"],
|
||||||
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
|
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||||
"integration_type": "entity",
|
"integration_type": "entity",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==3.5.0"]
|
"requirements": ["hassil==3.4.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,44 +112,48 @@
|
|||||||
"title": "Assist satellite",
|
"title": "Assist satellite",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"idle": {
|
"idle": {
|
||||||
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
"description": "Triggers when an Assist satellite becomes idle.",
|
||||||
|
"description_configured": "[%key:component::assist_satellite::triggers::idle::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Satellite became idle"
|
"name": "When an Assist satellite becomes idle"
|
||||||
},
|
},
|
||||||
"listening": {
|
"listening": {
|
||||||
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
"description": "Triggers when an Assist satellite starts listening.",
|
||||||
|
"description_configured": "[%key:component::assist_satellite::triggers::listening::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Satellite started listening"
|
"name": "When an Assist satellite starts listening"
|
||||||
},
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
"description": "Triggers when an Assist satellite is processing.",
|
||||||
|
"description_configured": "[%key:component::assist_satellite::triggers::processing::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Satellite started processing"
|
"name": "When an Assist satellite is processing"
|
||||||
},
|
},
|
||||||
"responding": {
|
"responding": {
|
||||||
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
"description": "Triggers when an Assist satellite is responding.",
|
||||||
|
"description_configured": "[%key:component::assist_satellite::triggers::responding::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Satellite started responding"
|
"name": "When an Assist satellite is responding"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
|||||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"assist_satellite",
|
"assist_satellite",
|
||||||
"binary_sensor",
|
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
"fan",
|
"fan",
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"preview_features": {
|
"preview_features": {
|
||||||
"new_triggers_conditions": {
|
"new_triggers_conditions": {
|
||||||
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
|
|
||||||
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
|
|
||||||
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
|
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,10 +69,10 @@
|
|||||||
},
|
},
|
||||||
"preview_features": {
|
"preview_features": {
|
||||||
"new_triggers_conditions": {
|
"new_triggers_conditions": {
|
||||||
"description": "Enables new purpose-specific triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own purpose-specific triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
|
"description": "Enables new intuitive triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own intuitive triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
|
||||||
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new purpose-specific triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
|
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new intuitive triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
|
||||||
"enable_confirmation": "This feature is still in development and may change. These new purpose-specific triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
|
"enable_confirmation": "This feature is still in development and may change. These new intuitive triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
|
||||||
"name": "Purpose-specific triggers and conditions"
|
"name": "Intuitive triggers and conditions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|||||||
@@ -174,13 +174,5 @@
|
|||||||
"on": "mdi:window-open"
|
"on": "mdi:window-open"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"triggers": {
|
|
||||||
"occupancy_cleared": {
|
|
||||||
"trigger": "mdi:home-outline"
|
|
||||||
},
|
|
||||||
"occupancy_detected": {
|
|
||||||
"trigger": "mdi:home"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
|
||||||
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior"
|
|
||||||
},
|
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"condition_type": {
|
"condition_type": {
|
||||||
"is_bat_low": "{entity_name} battery is low",
|
"is_bat_low": "{entity_name} battery is low",
|
||||||
@@ -321,36 +317,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
"title": "Binary sensor"
|
||||||
"trigger_behavior": {
|
|
||||||
"options": {
|
|
||||||
"any": "Any",
|
|
||||||
"first": "First",
|
|
||||||
"last": "Last"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Binary sensor",
|
|
||||||
"triggers": {
|
|
||||||
"occupancy_cleared": {
|
|
||||||
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
|
|
||||||
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Occupancy cleared"
|
|
||||||
},
|
|
||||||
"occupancy_detected": {
|
|
||||||
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
|
|
||||||
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Occupancy detected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
"""Provides triggers for binary sensors."""
|
|
||||||
|
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.entity import get_device_class
|
|
||||||
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
|
|
||||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
|
||||||
|
|
||||||
from . import DOMAIN, BinarySensorDeviceClass
|
|
||||||
|
|
||||||
|
|
||||||
def get_device_class_or_undefined(
|
|
||||||
hass: HomeAssistant, entity_id: str
|
|
||||||
) -> str | None | UndefinedType:
|
|
||||||
"""Get the device class of an entity or UNDEFINED if not found."""
|
|
||||||
try:
|
|
||||||
return get_device_class(hass, entity_id)
|
|
||||||
except HomeAssistantError:
|
|
||||||
return UNDEFINED
|
|
||||||
|
|
||||||
|
|
||||||
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
|
|
||||||
"""Class for binary sensor on/off triggers."""
|
|
||||||
|
|
||||||
_device_class: BinarySensorDeviceClass | None
|
|
||||||
_domain: str = DOMAIN
|
|
||||||
|
|
||||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
|
||||||
"""Filter entities of this domain."""
|
|
||||||
entities = super().entity_filter(entities)
|
|
||||||
return {
|
|
||||||
entity_id
|
|
||||||
for entity_id in entities
|
|
||||||
if get_device_class_or_undefined(self._hass, entity_id)
|
|
||||||
== self._device_class
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def make_binary_sensor_trigger(
|
|
||||||
device_class: BinarySensorDeviceClass | None,
|
|
||||||
to_state: str,
|
|
||||||
) -> type[BinarySensorOnOffTrigger]:
|
|
||||||
"""Create an entity state trigger class."""
|
|
||||||
|
|
||||||
class CustomTrigger(BinarySensorOnOffTrigger):
|
|
||||||
"""Trigger for entity state changes."""
|
|
||||||
|
|
||||||
_device_class = device_class
|
|
||||||
_to_state = to_state
|
|
||||||
|
|
||||||
return CustomTrigger
|
|
||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
|
||||||
"occupancy_detected": make_binary_sensor_trigger(
|
|
||||||
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
|
|
||||||
),
|
|
||||||
"occupancy_cleared": make_binary_sensor_trigger(
|
|
||||||
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
|
||||||
"""Return the triggers for binary sensors."""
|
|
||||||
return TRIGGERS
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
.trigger_common_fields: &trigger_common_fields
|
|
||||||
behavior:
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
translation_key: trigger_behavior
|
|
||||||
options:
|
|
||||||
- first
|
|
||||||
- last
|
|
||||||
- any
|
|
||||||
|
|
||||||
occupancy_cleared:
|
|
||||||
fields: *trigger_common_fields
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: binary_sensor
|
|
||||||
device_class: occupancy
|
|
||||||
|
|
||||||
occupancy_detected:
|
|
||||||
fields: *trigger_common_fields
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: binary_sensor
|
|
||||||
device_class: occupancy
|
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
"bluetooth-auto-recovery==1.5.3",
|
"bluetooth-auto-recovery==1.5.3",
|
||||||
"bluetooth-data-tools==1.28.4",
|
"bluetooth-data-tools==1.28.4",
|
||||||
"dbus-fast==3.1.2",
|
"dbus-fast==3.1.2",
|
||||||
"habluetooth==5.8.0"
|
"habluetooth==5.7.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
|
|||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
|
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
|
||||||
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
|
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
|
||||||
name=f"Bosch {panel.model.name}",
|
name=f"Bosch {panel.model}",
|
||||||
manufacturer="Bosch Security Systems",
|
manufacturer="Bosch Security Systems",
|
||||||
model=panel.model.name,
|
model=panel.model,
|
||||||
sw_version=panel.firmware_version,
|
sw_version=panel.firmware_version,
|
||||||
)
|
)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ async def try_connect(
|
|||||||
finally:
|
finally:
|
||||||
await panel.disconnect()
|
await panel.disconnect()
|
||||||
|
|
||||||
return (panel.model.name, panel.serial_number)
|
return (panel.model, panel.serial_number)
|
||||||
|
|
||||||
|
|
||||||
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
return {
|
return {
|
||||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||||
"data": {
|
"data": {
|
||||||
"model": entry.runtime_data.model.name,
|
"model": entry.runtime_data.model,
|
||||||
"family": entry.runtime_data.model.family.name,
|
|
||||||
"serial_number": entry.runtime_data.serial_number,
|
"serial_number": entry.runtime_data.serial_number,
|
||||||
"protocol_version": entry.runtime_data.protocol_version,
|
"protocol_version": entry.runtime_data.protocol_version,
|
||||||
"firmware_version": entry.runtime_data.firmware_version,
|
"firmware_version": entry.runtime_data.firmware_version,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
|
|||||||
self._attr_should_poll = False
|
self._attr_should_poll = False
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, unique_id)},
|
identifiers={(DOMAIN, unique_id)},
|
||||||
name=f"Bosch {panel.model.name}",
|
name=f"Bosch {panel.model}",
|
||||||
manufacturer="Bosch Security Systems",
|
manufacturer="Bosch Security Systems",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["bosch-alarm-mode2==0.4.10"]
|
"requirements": ["bosch-alarm-mode2==0.4.6"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/broadlink",
|
"documentation": "https://www.home-assistant.io/integrations/broadlink",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["broadlink"],
|
"loggers": ["broadlink"],
|
||||||
"requirements": ["broadlink==0.19.0"]
|
"requirements": ["broadlink==0.19.0"]
|
||||||
|
|||||||
@@ -407,8 +407,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
return [
|
return [
|
||||||
RTCIceServer(
|
RTCIceServer(
|
||||||
urls=[
|
urls=[
|
||||||
"stun:stun.home-assistant.io:3478",
|
|
||||||
"stun:stun.home-assistant.io:80",
|
"stun:stun.home-assistant.io:80",
|
||||||
|
"stun:stun.home-assistant.io:3478",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -299,54 +299,59 @@
|
|||||||
"title": "Climate",
|
"title": "Climate",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"started_cooling": {
|
"started_cooling": {
|
||||||
"description": "Triggers after one or more climate-control devices start cooling.",
|
"description": "Triggers when a climate started cooling.",
|
||||||
|
"description_configured": "[%key:component::climate::triggers::started_cooling::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device started cooling"
|
"name": "When a climate started cooling"
|
||||||
},
|
},
|
||||||
"started_drying": {
|
"started_drying": {
|
||||||
"description": "Triggers after one or more climate-control devices start drying.",
|
"description": "Triggers when a climate started drying.",
|
||||||
|
"description_configured": "[%key:component::climate::triggers::started_drying::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device started drying"
|
"name": "When a climate started drying"
|
||||||
},
|
},
|
||||||
"started_heating": {
|
"started_heating": {
|
||||||
"description": "Triggers after one or more climate-control devices start heating.",
|
"description": "Triggers when a climate starts to heat.",
|
||||||
|
"description_configured": "[%key:component::climate::triggers::started_heating::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device started heating"
|
"name": "When a climate starts to heat"
|
||||||
},
|
},
|
||||||
"turned_off": {
|
"turned_off": {
|
||||||
"description": "Triggers after one or more climate-control devices turn off.",
|
"description": "Triggers when a climate is turned off.",
|
||||||
|
"description_configured": "[%key:component::climate::triggers::turned_off::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device turned off"
|
"name": "When a climate is turned off"
|
||||||
},
|
},
|
||||||
"turned_on": {
|
"turned_on": {
|
||||||
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
|
"description": "Triggers when a climate is turned on.",
|
||||||
|
"description_configured": "[%key:component::climate::triggers::turned_on::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device turned on"
|
"name": "When a climate is turned on"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from contextlib import suppress
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from hass_nabucasa import Cloud, NabuCasaBaseError
|
from hass_nabucasa import Cloud
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import alexa, google_assistant
|
from homeassistant.components import alexa, google_assistant
|
||||||
@@ -79,16 +78,13 @@ from .subscription import async_subscription_info
|
|||||||
DEFAULT_MODE = MODE_PROD
|
DEFAULT_MODE = MODE_PROD
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
|
Platform.AI_TASK,
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.CONVERSATION,
|
||||||
Platform.STT,
|
Platform.STT,
|
||||||
Platform.TTS,
|
Platform.TTS,
|
||||||
]
|
]
|
||||||
|
|
||||||
LLM_PLATFORMS = [
|
|
||||||
Platform.AI_TASK,
|
|
||||||
Platform.CONVERSATION,
|
|
||||||
]
|
|
||||||
|
|
||||||
SERVICE_REMOTE_CONNECT = "remote_connect"
|
SERVICE_REMOTE_CONNECT = "remote_connect"
|
||||||
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
|
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
|
||||||
|
|
||||||
@@ -435,14 +431,7 @@ def _handle_prefs_updated(hass: HomeAssistant, cloud: Cloud[CloudClient]) -> Non
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
platforms = PLATFORMS.copy()
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
if (cloud := hass.data[DATA_CLOUD]).is_logged_in:
|
|
||||||
with suppress(NabuCasaBaseError):
|
|
||||||
await cloud.llm.async_ensure_token()
|
|
||||||
platforms += LLM_PLATFORMS
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, platforms)
|
|
||||||
entry.runtime_data = {"platforms": platforms}
|
|
||||||
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
|
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
|
||||||
stt_tts_entities_added.set()
|
stt_tts_entities_added.set()
|
||||||
|
|
||||||
@@ -451,9 +440,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
entry, entry.runtime_data["platforms"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import io
|
|||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from hass_nabucasa import NabuCasaBaseError
|
||||||
from hass_nabucasa.llm import (
|
from hass_nabucasa.llm import (
|
||||||
LLMAuthenticationError,
|
LLMAuthenticationError,
|
||||||
LLMError,
|
LLMError,
|
||||||
@@ -19,7 +20,7 @@ from PIL import Image
|
|||||||
from homeassistant.components import ai_task, conversation
|
from homeassistant.components import ai_task, conversation
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.json import json_loads
|
from homeassistant.util.json import json_loads
|
||||||
|
|
||||||
@@ -93,11 +94,17 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Home Assistant Cloud AI Task entity."""
|
"""Set up Home Assistant Cloud AI Task entity."""
|
||||||
cloud = hass.data[DATA_CLOUD]
|
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
|
||||||
async_add_entities([CloudAITaskEntity(cloud, config_entry)])
|
return
|
||||||
|
try:
|
||||||
|
await cloud.llm.async_ensure_token()
|
||||||
|
except (LLMError, NabuCasaBaseError):
|
||||||
|
return
|
||||||
|
|
||||||
|
async_add_entities([CloudLLMTaskEntity(cloud, config_entry)])
|
||||||
|
|
||||||
|
|
||||||
class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
|
class CloudLLMTaskEntity(ai_task.AITaskEntity, BaseCloudLLMEntity):
|
||||||
"""Home Assistant Cloud AI Task entity."""
|
"""Home Assistant Cloud AI Task entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
@@ -174,7 +181,7 @@ class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
|
|||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
)
|
)
|
||||||
except LLMAuthenticationError as err:
|
except LLMAuthenticationError as err:
|
||||||
raise HomeAssistantError("Cloud LLM authentication failed") from err
|
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
|
||||||
except LLMRateLimitError as err:
|
except LLMRateLimitError as err:
|
||||||
raise HomeAssistantError("Cloud LLM is rate limited") from err
|
raise HomeAssistantError("Cloud LLM is rate limited") from err
|
||||||
except LLMResponseError as err:
|
except LLMResponseError as err:
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class CloudClient(Interface):
|
|||||||
self._google_config_init_lock = asyncio.Lock()
|
self._google_config_init_lock = asyncio.Lock()
|
||||||
self._relayer_region: str | None = None
|
self._relayer_region: str | None = None
|
||||||
self._cloud_ice_servers_listener: Callable[[], None] | None = None
|
self._cloud_ice_servers_listener: Callable[[], None] | None = None
|
||||||
self._ice_servers: list[RTCIceServer] = []
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_path(self) -> Path:
|
def base_path(self) -> Path:
|
||||||
@@ -118,11 +117,6 @@ class CloudClient(Interface):
|
|||||||
"""Return the connected relayer region."""
|
"""Return the connected relayer region."""
|
||||||
return self._relayer_region
|
return self._relayer_region
|
||||||
|
|
||||||
@property
|
|
||||||
def ice_servers(self) -> list[RTCIceServer]:
|
|
||||||
"""Return the current ICE servers."""
|
|
||||||
return self._ice_servers
|
|
||||||
|
|
||||||
async def get_alexa_config(self) -> alexa_config.CloudAlexaConfig:
|
async def get_alexa_config(self) -> alexa_config.CloudAlexaConfig:
|
||||||
"""Return Alexa config."""
|
"""Return Alexa config."""
|
||||||
if self._alexa_config is None:
|
if self._alexa_config is None:
|
||||||
@@ -209,8 +203,11 @@ class CloudClient(Interface):
|
|||||||
ice_servers: list[RTCIceServer],
|
ice_servers: list[RTCIceServer],
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
"""Register cloud ice server."""
|
"""Register cloud ice server."""
|
||||||
self._ice_servers = ice_servers
|
|
||||||
return async_register_ice_servers(self._hass, lambda: self._ice_servers)
|
def get_ice_servers() -> list[RTCIceServer]:
|
||||||
|
return ice_servers
|
||||||
|
|
||||||
|
return async_register_ice_servers(self._hass, get_ice_servers)
|
||||||
|
|
||||||
async def async_register_cloud_ice_servers_listener(
|
async def async_register_cloud_ice_servers_listener(
|
||||||
prefs: CloudPreferences,
|
prefs: CloudPreferences,
|
||||||
@@ -271,7 +268,6 @@ class CloudClient(Interface):
|
|||||||
|
|
||||||
async def logout_cleanups(self) -> None:
|
async def logout_cleanups(self) -> None:
|
||||||
"""Cleanup some stuff after logout."""
|
"""Cleanup some stuff after logout."""
|
||||||
self._ice_servers = []
|
|
||||||
await self.prefs.async_set_username(None)
|
await self.prefs.async_set_username(None)
|
||||||
|
|
||||||
if self._alexa_config:
|
if self._alexa_config:
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
from hass_nabucasa import NabuCasaBaseError
|
||||||
|
from hass_nabucasa.llm import LLMError
|
||||||
|
|
||||||
from homeassistant.components import conversation
|
from homeassistant.components import conversation
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import MATCH_ALL
|
from homeassistant.const import MATCH_ALL
|
||||||
@@ -21,13 +24,19 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Home Assistant Cloud conversation entity."""
|
"""Set up the Home Assistant Cloud conversation entity."""
|
||||||
cloud = hass.data[DATA_CLOUD]
|
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await cloud.llm.async_ensure_token()
|
||||||
|
except (LLMError, NabuCasaBaseError):
|
||||||
|
return
|
||||||
|
|
||||||
async_add_entities([CloudConversationEntity(cloud, config_entry)])
|
async_add_entities([CloudConversationEntity(cloud, config_entry)])
|
||||||
|
|
||||||
|
|
||||||
class CloudConversationEntity(
|
class CloudConversationEntity(
|
||||||
BaseCloudLLMEntity,
|
|
||||||
conversation.ConversationEntity,
|
conversation.ConversationEntity,
|
||||||
|
BaseCloudLLMEntity,
|
||||||
):
|
):
|
||||||
"""Home Assistant Cloud conversation agent."""
|
"""Home Assistant Cloud conversation agent."""
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Literal, cast
|
from typing import Any, Literal, cast
|
||||||
|
|
||||||
from hass_nabucasa import Cloud, NabuCasaBaseError
|
from hass_nabucasa import Cloud
|
||||||
from hass_nabucasa.llm import (
|
from hass_nabucasa.llm import (
|
||||||
LLMAuthenticationError,
|
LLMAuthenticationError,
|
||||||
|
LLMError,
|
||||||
LLMRateLimitError,
|
LLMRateLimitError,
|
||||||
LLMResponseError,
|
LLMResponseError,
|
||||||
LLMServiceError,
|
LLMServiceError,
|
||||||
@@ -36,7 +37,7 @@ from voluptuous_openapi import convert
|
|||||||
|
|
||||||
from homeassistant.components import conversation
|
from homeassistant.components import conversation
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||||
from homeassistant.helpers import llm
|
from homeassistant.helpers import llm
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
@@ -561,7 +562,7 @@ class BaseCloudLLMEntity(Entity):
|
|||||||
"schema": _format_structured_output(
|
"schema": _format_structured_output(
|
||||||
structure, chat_log.llm_api
|
structure, chat_log.llm_api
|
||||||
),
|
),
|
||||||
"strict": False,
|
"strict": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,14 +601,14 @@ class BaseCloudLLMEntity(Entity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except LLMAuthenticationError as err:
|
except LLMAuthenticationError as err:
|
||||||
raise HomeAssistantError("Cloud LLM authentication failed") from err
|
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
|
||||||
except LLMRateLimitError as err:
|
except LLMRateLimitError as err:
|
||||||
raise HomeAssistantError("Cloud LLM is rate limited") from err
|
raise HomeAssistantError("Cloud LLM is rate limited") from err
|
||||||
except LLMResponseError as err:
|
except LLMResponseError as err:
|
||||||
raise HomeAssistantError(str(err)) from err
|
raise HomeAssistantError(str(err)) from err
|
||||||
except LLMServiceError as err:
|
except LLMServiceError as err:
|
||||||
raise HomeAssistantError("Error talking to Cloud LLM") from err
|
raise HomeAssistantError("Error talking to Cloud LLM") from err
|
||||||
except NabuCasaBaseError as err:
|
except LLMError as err:
|
||||||
raise HomeAssistantError(str(err)) from err
|
raise HomeAssistantError(str(err)) from err
|
||||||
|
|
||||||
if not chat_log.unresponded_tool_results:
|
if not chat_log.unresponded_tool_results:
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||||||
websocket_api.async_register_command(hass, websocket_hook_delete)
|
websocket_api.async_register_command(hass, websocket_hook_delete)
|
||||||
websocket_api.async_register_command(hass, websocket_remote_connect)
|
websocket_api.async_register_command(hass, websocket_remote_connect)
|
||||||
websocket_api.async_register_command(hass, websocket_remote_disconnect)
|
websocket_api.async_register_command(hass, websocket_remote_disconnect)
|
||||||
websocket_api.async_register_command(hass, websocket_webrtc_ice_servers)
|
|
||||||
|
|
||||||
websocket_api.async_register_command(hass, google_assistant_get)
|
websocket_api.async_register_command(hass, google_assistant_get)
|
||||||
websocket_api.async_register_command(hass, google_assistant_list)
|
websocket_api.async_register_command(hass, google_assistant_list)
|
||||||
@@ -1108,7 +1107,6 @@ async def alexa_sync(
|
|||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command({"type": "cloud/tts/info"})
|
@websocket_api.websocket_command({"type": "cloud/tts/info"})
|
||||||
@callback
|
|
||||||
def tts_info(
|
def tts_info(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
@@ -1136,22 +1134,3 @@ def tts_info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
connection.send_result(msg["id"], {"languages": result})
|
connection.send_result(msg["id"], {"languages": result})
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): "cloud/webrtc/ice_servers",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@_require_cloud_login
|
|
||||||
@callback
|
|
||||||
def websocket_webrtc_ice_servers(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
connection: websocket_api.ActiveConnection,
|
|
||||||
msg: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Handle get WebRTC ICE servers websocket command."""
|
|
||||||
connection.send_result(
|
|
||||||
msg["id"],
|
|
||||||
[server.to_dict() for server in hass.data[DATA_CLOUD].client.ice_servers],
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==1.7.0"],
|
"requirements": ["hass-nabucasa==1.6.2"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "entity",
|
"integration_type": "entity",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2025.12.2"]
|
"requirements": ["hassil==3.4.0", "home-assistant-intents==2025.11.24"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,5 +108,34 @@
|
|||||||
"toggle_cover_tilt": {
|
"toggle_cover_tilt": {
|
||||||
"service": "mdi:arrow-top-right-bottom-left"
|
"service": "mdi:arrow-top-right-bottom-left"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"awning_opened": {
|
||||||
|
"trigger": "mdi:awning-outline"
|
||||||
|
},
|
||||||
|
"blind_opened": {
|
||||||
|
"trigger": "mdi:blinds-horizontal"
|
||||||
|
},
|
||||||
|
"curtain_opened": {
|
||||||
|
"trigger": "mdi:curtains"
|
||||||
|
},
|
||||||
|
"door_opened": {
|
||||||
|
"trigger": "mdi:door-open"
|
||||||
|
},
|
||||||
|
"garage_opened": {
|
||||||
|
"trigger": "mdi:garage-open"
|
||||||
|
},
|
||||||
|
"gate_opened": {
|
||||||
|
"trigger": "mdi:gate-open"
|
||||||
|
},
|
||||||
|
"shade_opened": {
|
||||||
|
"trigger": "mdi:roller-shade"
|
||||||
|
},
|
||||||
|
"shutter_opened": {
|
||||||
|
"trigger": "mdi:window-shutter-open"
|
||||||
|
},
|
||||||
|
"window_opened": {
|
||||||
|
"trigger": "mdi:window-open"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"trigger_behavior_description_awning": "The behavior of the targeted awnings to trigger on.",
|
||||||
|
"trigger_behavior_description_blind": "The behavior of the targeted blinds to trigger on.",
|
||||||
|
"trigger_behavior_description_curtain": "The behavior of the targeted curtains to trigger on.",
|
||||||
|
"trigger_behavior_description_door": "The behavior of the targeted doors to trigger on.",
|
||||||
|
"trigger_behavior_description_garage": "The behavior of the targeted garage doors to trigger on.",
|
||||||
|
"trigger_behavior_description_gate": "The behavior of the targeted gates to trigger on.",
|
||||||
|
"trigger_behavior_description_shade": "The behavior of the targeted shades to trigger on.",
|
||||||
|
"trigger_behavior_description_shutter": "The behavior of the targeted shutters to trigger on.",
|
||||||
|
"trigger_behavior_description_window": "The behavior of the targeted windows to trigger on.",
|
||||||
|
"trigger_behavior_name": "Behavior"
|
||||||
|
},
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"action_type": {
|
"action_type": {
|
||||||
"close": "Close {entity_name}",
|
"close": "Close {entity_name}",
|
||||||
@@ -82,6 +94,15 @@
|
|||||||
"name": "Window"
|
"name": "Window"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"selector": {
|
||||||
|
"trigger_behavior": {
|
||||||
|
"options": {
|
||||||
|
"any": "Any",
|
||||||
|
"first": "First",
|
||||||
|
"last": "Last"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"close_cover": {
|
"close_cover": {
|
||||||
"description": "Closes a cover.",
|
"description": "Closes a cover.",
|
||||||
@@ -136,5 +157,142 @@
|
|||||||
"name": "Toggle tilt"
|
"name": "Toggle tilt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Cover"
|
"title": "Cover",
|
||||||
|
"triggers": {
|
||||||
|
"awning_opened": {
|
||||||
|
"description": "Triggers when an awning opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::awning_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_awning%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the awnings to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When an awning opens"
|
||||||
|
},
|
||||||
|
"blind_opened": {
|
||||||
|
"description": "Triggers when a blind opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::blind_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_blind%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the blinds to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a blind opens"
|
||||||
|
},
|
||||||
|
"curtain_opened": {
|
||||||
|
"description": "Triggers when a curtain opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::curtain_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_curtain%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the curtains to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a curtain opens"
|
||||||
|
},
|
||||||
|
"door_opened": {
|
||||||
|
"description": "Triggers when a door opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::door_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_door%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the doors to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a door opens"
|
||||||
|
},
|
||||||
|
"garage_opened": {
|
||||||
|
"description": "Triggers when a garage door opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::garage_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_garage%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the garage doors to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a garage door opens"
|
||||||
|
},
|
||||||
|
"gate_opened": {
|
||||||
|
"description": "Triggers when a gate opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::gate_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_gate%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the gates to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a gate opens"
|
||||||
|
},
|
||||||
|
"shade_opened": {
|
||||||
|
"description": "Triggers when a shade opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::shade_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_shade%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the shades to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a shade opens"
|
||||||
|
},
|
||||||
|
"shutter_opened": {
|
||||||
|
"description": "Triggers when a shutter opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::shutter_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_shutter%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the shutters to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a shutter opens"
|
||||||
|
},
|
||||||
|
"window_opened": {
|
||||||
|
"description": "Triggers when a window opens.",
|
||||||
|
"description_configured": "[%key:component::cover::triggers::window_opened::description%]",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::cover::common::trigger_behavior_description_window%]",
|
||||||
|
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"fully_opened": {
|
||||||
|
"description": "Require the windows to be fully opened before triggering.",
|
||||||
|
"name": "Fully opened"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a window opens"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
homeassistant/components/cover/trigger.py
Normal file
116
homeassistant/components/cover/trigger.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Provides triggers for covers."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_OPTIONS
|
||||||
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity import get_device_class
|
||||||
|
from homeassistant.helpers.trigger import (
|
||||||
|
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||||
|
EntityTriggerBase,
|
||||||
|
Trigger,
|
||||||
|
TriggerConfig,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
|
|
||||||
|
from . import ATTR_CURRENT_POSITION, CoverDeviceClass, CoverState
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
ATTR_FULLY_OPENED: Final = "fully_opened"
|
||||||
|
|
||||||
|
COVER_OPENED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_OPTIONS): {
|
||||||
|
vol.Required(ATTR_FULLY_OPENED, default=False): bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_class_or_undefined(
|
||||||
|
hass: HomeAssistant, entity_id: str
|
||||||
|
) -> str | None | UndefinedType:
|
||||||
|
"""Get the device class of an entity or UNDEFINED if not found."""
|
||||||
|
try:
|
||||||
|
return get_device_class(hass, entity_id)
|
||||||
|
except HomeAssistantError:
|
||||||
|
return UNDEFINED
|
||||||
|
|
||||||
|
|
||||||
|
class CoverOpenedClosedTrigger(EntityTriggerBase):
|
||||||
|
"""Class for cover opened and closed triggers."""
|
||||||
|
|
||||||
|
_attribute: str = ATTR_CURRENT_POSITION
|
||||||
|
_attribute_value: int | None = None
|
||||||
|
_device_class: CoverDeviceClass | None
|
||||||
|
_domain: str = DOMAIN
|
||||||
|
_to_states: set[str]
|
||||||
|
|
||||||
|
def is_to_state(self, state: State) -> bool:
|
||||||
|
"""Check if the state matches the target state."""
|
||||||
|
if state.state not in self._to_states:
|
||||||
|
return False
|
||||||
|
if (
|
||||||
|
self._attribute_value is not None
|
||||||
|
and (value := state.attributes.get(self._attribute)) is not None
|
||||||
|
and value != self._attribute_value
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||||
|
"""Filter entities of this domain."""
|
||||||
|
entities = super().entity_filter(entities)
|
||||||
|
return {
|
||||||
|
entity_id
|
||||||
|
for entity_id in entities
|
||||||
|
if get_device_class_or_undefined(self._hass, entity_id)
|
||||||
|
== self._device_class
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CoverOpenedTrigger(CoverOpenedClosedTrigger):
|
||||||
|
"""Class for cover opened triggers."""
|
||||||
|
|
||||||
|
_schema = COVER_OPENED_TRIGGER_SCHEMA
|
||||||
|
_to_states = {CoverState.OPEN, CoverState.OPENING}
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||||
|
"""Initialize the state trigger."""
|
||||||
|
super().__init__(hass, config)
|
||||||
|
if self._options.get(ATTR_FULLY_OPENED):
|
||||||
|
self._attribute_value = 100
|
||||||
|
|
||||||
|
|
||||||
|
def make_cover_opened_trigger(
|
||||||
|
device_class: CoverDeviceClass | None,
|
||||||
|
) -> type[CoverOpenedTrigger]:
|
||||||
|
"""Create an entity state attribute trigger class."""
|
||||||
|
|
||||||
|
class CustomTrigger(CoverOpenedTrigger):
|
||||||
|
"""Trigger for entity state changes."""
|
||||||
|
|
||||||
|
_device_class = device_class
|
||||||
|
|
||||||
|
return CustomTrigger
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
|
"awning_opened": make_cover_opened_trigger(CoverDeviceClass.AWNING),
|
||||||
|
"blind_opened": make_cover_opened_trigger(CoverDeviceClass.BLIND),
|
||||||
|
"curtain_opened": make_cover_opened_trigger(CoverDeviceClass.CURTAIN),
|
||||||
|
"door_opened": make_cover_opened_trigger(CoverDeviceClass.DOOR),
|
||||||
|
"garage_opened": make_cover_opened_trigger(CoverDeviceClass.GARAGE),
|
||||||
|
"gate_opened": make_cover_opened_trigger(CoverDeviceClass.GATE),
|
||||||
|
"shade_opened": make_cover_opened_trigger(CoverDeviceClass.SHADE),
|
||||||
|
"shutter_opened": make_cover_opened_trigger(CoverDeviceClass.SHUTTER),
|
||||||
|
"window_opened": make_cover_opened_trigger(CoverDeviceClass.WINDOW),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||||
|
"""Return the triggers for covers."""
|
||||||
|
return TRIGGERS
|
||||||
79
homeassistant/components/cover/triggers.yaml
Normal file
79
homeassistant/components/cover/triggers.yaml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
.trigger_common_fields: &trigger_common_fields
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: trigger_behavior
|
||||||
|
options:
|
||||||
|
- first
|
||||||
|
- last
|
||||||
|
- any
|
||||||
|
fully_opened:
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
|
||||||
|
awning_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: awning
|
||||||
|
|
||||||
|
blind_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: blind
|
||||||
|
|
||||||
|
curtain_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: curtain
|
||||||
|
|
||||||
|
door_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: door
|
||||||
|
|
||||||
|
garage_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: garage
|
||||||
|
|
||||||
|
gate_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: gate
|
||||||
|
|
||||||
|
shade_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: shade
|
||||||
|
|
||||||
|
shutter_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: shutter
|
||||||
|
|
||||||
|
window_opened:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: cover
|
||||||
|
device_class: window
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["async_upnp_client"],
|
"loggers": ["async_upnp_client"],
|
||||||
"requirements": ["async-upnp-client==0.46.0", "getmac==0.9.5"],
|
"requirements": ["async-upnp-client==0.46.0", "getmac==0.9.5"],
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["async-upnp-client==0.46.0"],
|
"requirements": ["async-upnp-client==0.46.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["webhook"],
|
"dependencies": ["webhook"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["aioecowitt==2025.9.2"]
|
"requirements": ["aioecowitt==2025.9.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ DEFAULT_TTS_MODEL = "eleven_multilingual_v2"
|
|||||||
DEFAULT_STABILITY = 0.5
|
DEFAULT_STABILITY = 0.5
|
||||||
DEFAULT_SIMILARITY = 0.75
|
DEFAULT_SIMILARITY = 0.75
|
||||||
DEFAULT_STT_AUTO_LANGUAGE = False
|
DEFAULT_STT_AUTO_LANGUAGE = False
|
||||||
DEFAULT_STT_MODEL = "scribe_v2"
|
DEFAULT_STT_MODEL = "scribe_v1"
|
||||||
DEFAULT_STYLE = 0
|
DEFAULT_STYLE = 0
|
||||||
DEFAULT_USE_SPEAKER_BOOST = True
|
DEFAULT_USE_SPEAKER_BOOST = True
|
||||||
|
|
||||||
@@ -129,5 +129,4 @@ STT_LANGUAGES = [
|
|||||||
STT_MODELS = {
|
STT_MODELS = {
|
||||||
"scribe_v1": "Scribe v1",
|
"scribe_v1": "Scribe v1",
|
||||||
"scribe_v1_experimental": "Scribe v1 Experimental",
|
"scribe_v1_experimental": "Scribe v1 Experimental",
|
||||||
"scribe_v2": "Scribe v2 Realtime",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"mqtt": ["esphome/discover/#"],
|
"mqtt": ["esphome/discover/#"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==42.10.0",
|
"aioesphomeapi==42.8.0",
|
||||||
"esphome-dashboard-api==1.3.0",
|
"esphome-dashboard-api==1.3.0",
|
||||||
"bleak-esphome==3.4.0"
|
"bleak-esphome==3.4.0"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -166,24 +166,26 @@
|
|||||||
"title": "Fan",
|
"title": "Fan",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"turned_off": {
|
"turned_off": {
|
||||||
"description": "Triggers after one or more fans turn off.",
|
"description": "Triggers when a fan is turned off.",
|
||||||
|
"description_configured": "[%key:component::fan::triggers::turned_off::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::fan::common::trigger_behavior_description%]",
|
"description": "[%key:component::fan::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::fan::common::trigger_behavior_name%]"
|
"name": "[%key:component::fan::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Fan turned off"
|
"name": "When a fan is turned off"
|
||||||
},
|
},
|
||||||
"turned_on": {
|
"turned_on": {
|
||||||
"description": "Triggers after one or more fans turn on.",
|
"description": "Triggers when a fan is turned on.",
|
||||||
|
"description_configured": "[%key:component::fan::triggers::turned_on::description%]",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::fan::common::trigger_behavior_description%]",
|
"description": "[%key:component::fan::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::fan::common::trigger_behavior_name%]"
|
"name": "[%key:component::fan::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Fan turned on"
|
"name": "When a fan is turned on"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
"""The Fressnapf Tracker integration."""
|
|
||||||
|
|
||||||
from fressnapftracker import AuthClient
|
|
||||||
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
|
||||||
|
|
||||||
from .const import CONF_USER_ID
|
|
||||||
from .coordinator import (
|
|
||||||
FressnapfTrackerConfigEntry,
|
|
||||||
FressnapfTrackerDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
|
||||||
Platform.BINARY_SENSOR,
|
|
||||||
Platform.DEVICE_TRACKER,
|
|
||||||
Platform.SENSOR,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant, entry: FressnapfTrackerConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Set up Fressnapf Tracker from a config entry."""
|
|
||||||
auth_client = AuthClient(client=get_async_client(hass))
|
|
||||||
devices = await auth_client.get_devices(
|
|
||||||
user_id=entry.data[CONF_USER_ID],
|
|
||||||
user_access_token=entry.data[CONF_ACCESS_TOKEN],
|
|
||||||
)
|
|
||||||
|
|
||||||
coordinators: list[FressnapfTrackerDataUpdateCoordinator] = []
|
|
||||||
for device in devices:
|
|
||||||
coordinator = FressnapfTrackerDataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
entry,
|
|
||||||
device,
|
|
||||||
)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
coordinators.append(coordinator)
|
|
||||||
|
|
||||||
entry.runtime_data = coordinators
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
|
||||||
hass: HomeAssistant, entry: FressnapfTrackerConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""Binary Sensor platform for fressnapf_tracker."""
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from fressnapftracker import Tracker
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
|
||||||
BinarySensorDeviceClass,
|
|
||||||
BinarySensorEntity,
|
|
||||||
BinarySensorEntityDescription,
|
|
||||||
)
|
|
||||||
from homeassistant.const import EntityCategory
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import FressnapfTrackerConfigEntry
|
|
||||||
from .entity import FressnapfTrackerEntity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class FressnapfTrackerBinarySensorDescription(BinarySensorEntityDescription):
|
|
||||||
"""Class describing Fressnapf Tracker binary_sensor entities."""
|
|
||||||
|
|
||||||
value_fn: Callable[[Tracker], bool]
|
|
||||||
|
|
||||||
|
|
||||||
BINARY_SENSOR_ENTITY_DESCRIPTIONS: tuple[
|
|
||||||
FressnapfTrackerBinarySensorDescription, ...
|
|
||||||
] = (
|
|
||||||
FressnapfTrackerBinarySensorDescription(
|
|
||||||
key="charging",
|
|
||||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda data: data.charging,
|
|
||||||
),
|
|
||||||
FressnapfTrackerBinarySensorDescription(
|
|
||||||
translation_key="deep_sleep",
|
|
||||||
key="deep_sleep_value",
|
|
||||||
device_class=BinarySensorDeviceClass.POWER,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda data: bool(data.deep_sleep_value),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: FressnapfTrackerConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Fressnapf Tracker binary_sensors."""
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
FressnapfTrackerBinarySensor(coordinator, sensor_description)
|
|
||||||
for sensor_description in BINARY_SENSOR_ENTITY_DESCRIPTIONS
|
|
||||||
for coordinator in entry.runtime_data
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerBinarySensor(FressnapfTrackerEntity, BinarySensorEntity):
|
|
||||||
"""Fressnapf Tracker binary_sensor for general information."""
|
|
||||||
|
|
||||||
entity_description: FressnapfTrackerBinarySensorDescription
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return True if the binary sensor is on."""
|
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
"""Config flow for the Fressnapf Tracker integration."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fressnapftracker import (
|
|
||||||
AuthClient,
|
|
||||||
FressnapfTrackerInvalidPhoneNumberError,
|
|
||||||
FressnapfTrackerInvalidTokenError,
|
|
||||||
)
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
|
||||||
|
|
||||||
from .const import CONF_PHONE_NUMBER, CONF_SMS_CODE, CONF_USER_ID, DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PHONE_NUMBER): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
STEP_SMS_CODE_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_SMS_CODE): int,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for Fressnapf Tracker."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Init Config Flow."""
|
|
||||||
self._context: dict[str, Any] = {}
|
|
||||||
self._auth_client: AuthClient | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_client(self) -> AuthClient:
|
|
||||||
"""Return the auth client, creating it if needed."""
|
|
||||||
if self._auth_client is None:
|
|
||||||
self._auth_client = AuthClient(client=get_async_client(self.hass))
|
|
||||||
return self._auth_client
|
|
||||||
|
|
||||||
async def _async_request_sms_code(
|
|
||||||
self, phone_number: str
|
|
||||||
) -> tuple[dict[str, str], bool]:
|
|
||||||
"""Request SMS code and return errors dict and success flag."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
try:
|
|
||||||
response = await self.auth_client.request_sms_code(
|
|
||||||
phone_number=phone_number
|
|
||||||
)
|
|
||||||
except FressnapfTrackerInvalidPhoneNumberError:
|
|
||||||
errors["base"] = "invalid_phone_number"
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
_LOGGER.debug("SMS code request response: %s", response)
|
|
||||||
self._context[CONF_USER_ID] = response.id
|
|
||||||
self._context[CONF_PHONE_NUMBER] = phone_number
|
|
||||||
return errors, True
|
|
||||||
return errors, False
|
|
||||||
|
|
||||||
async def _async_verify_sms_code(
|
|
||||||
self, sms_code: int
|
|
||||||
) -> tuple[dict[str, str], str | None]:
|
|
||||||
"""Verify SMS code and return errors and access_token."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
try:
|
|
||||||
verification_response = await self.auth_client.verify_phone_number(
|
|
||||||
user_id=self._context[CONF_USER_ID],
|
|
||||||
sms_code=sms_code,
|
|
||||||
)
|
|
||||||
except FressnapfTrackerInvalidTokenError:
|
|
||||||
errors["base"] = "invalid_sms_code"
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception("Unexpected exception during SMS code verification")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Phone number verification response: %s", verification_response
|
|
||||||
)
|
|
||||||
return errors, verification_response.user_token.access_token
|
|
||||||
return errors, None
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
if user_input is not None:
|
|
||||||
self._async_abort_entries_match(
|
|
||||||
{CONF_PHONE_NUMBER: user_input[CONF_PHONE_NUMBER]}
|
|
||||||
)
|
|
||||||
errors, success = await self._async_request_sms_code(
|
|
||||||
user_input[CONF_PHONE_NUMBER]
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
await self.async_set_unique_id(str(self._context[CONF_USER_ID]))
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
return await self.async_step_sms_code()
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_sms_code(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the SMS code step."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
if user_input is not None:
|
|
||||||
errors, access_token = await self._async_verify_sms_code(
|
|
||||||
user_input[CONF_SMS_CODE]
|
|
||||||
)
|
|
||||||
if access_token:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self._context[CONF_PHONE_NUMBER],
|
|
||||||
data={
|
|
||||||
CONF_PHONE_NUMBER: self._context[CONF_PHONE_NUMBER],
|
|
||||||
CONF_USER_ID: self._context[CONF_USER_ID],
|
|
||||||
CONF_ACCESS_TOKEN: access_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="sms_code",
|
|
||||||
data_schema=STEP_SMS_CODE_DATA_SCHEMA,
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_reconfigure(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reconfiguration of the integration."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
reconfigure_entry = self._get_reconfigure_entry()
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
errors, success = await self._async_request_sms_code(
|
|
||||||
user_input[CONF_PHONE_NUMBER]
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
if reconfigure_entry.data[CONF_USER_ID] != self._context[CONF_USER_ID]:
|
|
||||||
errors["base"] = "account_change_not_allowed"
|
|
||||||
else:
|
|
||||||
return await self.async_step_reconfigure_sms_code()
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reconfigure",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(
|
|
||||||
CONF_PHONE_NUMBER,
|
|
||||||
default=reconfigure_entry.data.get(CONF_PHONE_NUMBER),
|
|
||||||
): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_reconfigure_sms_code(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the SMS code step during reconfiguration."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
errors, access_token = await self._async_verify_sms_code(
|
|
||||||
user_input[CONF_SMS_CODE]
|
|
||||||
)
|
|
||||||
if access_token:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reconfigure_entry(),
|
|
||||||
data={
|
|
||||||
CONF_PHONE_NUMBER: self._context[CONF_PHONE_NUMBER],
|
|
||||||
CONF_USER_ID: self._context[CONF_USER_ID],
|
|
||||||
CONF_ACCESS_TOKEN: access_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reconfigure_sms_code",
|
|
||||||
data_schema=STEP_SMS_CODE_DATA_SCHEMA,
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Constants for the Fressnapf Tracker integration."""
|
|
||||||
|
|
||||||
DOMAIN = "fressnapf_tracker"
|
|
||||||
CONF_PHONE_NUMBER = "phone_number"
|
|
||||||
CONF_SMS_CODE = "sms_code"
|
|
||||||
CONF_USER_ID = "user_id"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
"""Data update coordinator for Fressnapf Tracker integration."""
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from fressnapftracker import ApiClient, Device, FressnapfTrackerError, Tracker
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
type FressnapfTrackerConfigEntry = ConfigEntry[
|
|
||||||
list[FressnapfTrackerDataUpdateCoordinator]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerDataUpdateCoordinator(DataUpdateCoordinator[Tracker]):
|
|
||||||
"""Class to manage fetching data from the API."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: FressnapfTrackerConfigEntry,
|
|
||||||
device: Device,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_interval=timedelta(minutes=15),
|
|
||||||
config_entry=config_entry,
|
|
||||||
)
|
|
||||||
self.device = device
|
|
||||||
self.client = ApiClient(
|
|
||||||
serial_number=device.serialnumber,
|
|
||||||
device_token=device.token,
|
|
||||||
client=get_async_client(hass),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> Tracker:
|
|
||||||
try:
|
|
||||||
return await self.client.get_tracker()
|
|
||||||
except FressnapfTrackerError as exception:
|
|
||||||
raise UpdateFailed(exception) from exception
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""Device tracker platform for fressnapf_tracker."""
|
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import SourceType
|
|
||||||
from homeassistant.components.device_tracker.config_entry import TrackerEntity
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import FressnapfTrackerConfigEntry, FressnapfTrackerDataUpdateCoordinator
|
|
||||||
from .entity import FressnapfTrackerBaseEntity
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: FressnapfTrackerConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the fressnapf_tracker device_trackers."""
|
|
||||||
async_add_entities(
|
|
||||||
FressnapfTrackerDeviceTracker(coordinator) for coordinator in entry.runtime_data
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerDeviceTracker(FressnapfTrackerBaseEntity, TrackerEntity):
|
|
||||||
"""fressnapf_tracker device tracker."""
|
|
||||||
|
|
||||||
_attr_name = None
|
|
||||||
_attr_translation_key = "pet"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: FressnapfTrackerDataUpdateCoordinator,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the device tracker."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._attr_unique_id = coordinator.device.serialnumber
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return super().available and self.coordinator.data.position is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latitude(self) -> float | None:
|
|
||||||
"""Return latitude value of the device."""
|
|
||||||
if self.coordinator.data.position is not None:
|
|
||||||
return self.coordinator.data.position.lat
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def longitude(self) -> float | None:
|
|
||||||
"""Return longitude value of the device."""
|
|
||||||
if self.coordinator.data.position is not None:
|
|
||||||
return self.coordinator.data.position.lng
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source_type(self) -> SourceType:
|
|
||||||
"""Return the source type, eg gps or router, of the device."""
|
|
||||||
return SourceType.GPS
|
|
||||||
|
|
||||||
@property
|
|
||||||
def location_accuracy(self) -> float:
|
|
||||||
"""Return the location accuracy of the device.
|
|
||||||
|
|
||||||
Value in meters.
|
|
||||||
"""
|
|
||||||
if self.coordinator.data.position is not None:
|
|
||||||
return float(self.coordinator.data.position.accuracy)
|
|
||||||
return 0
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"""fressnapf_tracker class."""
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from . import FressnapfTrackerDataUpdateCoordinator
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerBaseEntity(
|
|
||||||
CoordinatorEntity[FressnapfTrackerDataUpdateCoordinator]
|
|
||||||
):
|
|
||||||
"""Base entity for Fressnapf Tracker."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(self, coordinator: FressnapfTrackerDataUpdateCoordinator) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.id = coordinator.device.serialnumber
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, str(self.id))},
|
|
||||||
name=str(self.coordinator.data.name),
|
|
||||||
model=str(self.coordinator.data.tracker_settings.generation),
|
|
||||||
manufacturer="Fressnapf",
|
|
||||||
serial_number=str(self.id),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerEntity(FressnapfTrackerBaseEntity):
|
|
||||||
"""Entity for fressnapf_tracker."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: FressnapfTrackerDataUpdateCoordinator,
|
|
||||||
entity_description: EntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
self._attr_unique_id = f"{self.id}_{entity_description.key}"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"entity": {
|
|
||||||
"device_tracker": {
|
|
||||||
"pet": {
|
|
||||||
"default": "mdi:paw"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "fressnapf_tracker",
|
|
||||||
"name": "Fressnapf Tracker",
|
|
||||||
"codeowners": ["@eifinger"],
|
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/fressnapf_tracker",
|
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
|
||||||
"quality_scale": "bronze",
|
|
||||||
"requirements": ["fressnapftracker==0.1.2"]
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
No custom actions are defined.
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
No custom actions are defined.
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup: done
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: todo
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters: todo
|
|
||||||
docs-installation-parameters: todo
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: todo
|
|
||||||
log-when-unavailable: todo
|
|
||||||
parallel-updates: todo
|
|
||||||
reauthentication-flow: todo
|
|
||||||
test-coverage: todo
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: todo
|
|
||||||
discovery-update-info: todo
|
|
||||||
discovery: todo
|
|
||||||
docs-data-update: todo
|
|
||||||
docs-examples: todo
|
|
||||||
docs-known-limitations: todo
|
|
||||||
docs-supported-devices: todo
|
|
||||||
docs-supported-functions: todo
|
|
||||||
docs-troubleshooting: todo
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category: todo
|
|
||||||
entity-device-class: todo
|
|
||||||
entity-disabled-by-default: todo
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: todo
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: done
|
|
||||||
repair-issues: todo
|
|
||||||
stale-devices: todo
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: done
|
|
||||||
inject-websession: done
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
"""Sensor platform for fressnapf_tracker."""
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from fressnapftracker import Tracker
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import FressnapfTrackerConfigEntry
|
|
||||||
from .entity import FressnapfTrackerEntity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class FressnapfTrackerSensorDescription(SensorEntityDescription):
|
|
||||||
"""Class describing Fressnapf Tracker sensor entities."""
|
|
||||||
|
|
||||||
value_fn: Callable[[Tracker], int]
|
|
||||||
|
|
||||||
|
|
||||||
SENSOR_ENTITY_DESCRIPTIONS: tuple[FressnapfTrackerSensorDescription, ...] = (
|
|
||||||
FressnapfTrackerSensorDescription(
|
|
||||||
key="battery",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda data: data.battery,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: FressnapfTrackerConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Fressnapf Tracker sensors."""
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
FressnapfTrackerSensor(coordinator, sensor_description)
|
|
||||||
for sensor_description in SENSOR_ENTITY_DESCRIPTIONS
|
|
||||||
for coordinator in entry.runtime_data
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FressnapfTrackerSensor(FressnapfTrackerEntity, SensorEntity):
|
|
||||||
"""fressnapf_tracker sensor for general information."""
|
|
||||||
|
|
||||||
entity_description: FressnapfTrackerSensorDescription
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> int:
|
|
||||||
"""Return the state of the resources if it has been received yet."""
|
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"account_change_not_allowed": "Reconfiguring to a different account is not allowed. Please create a new entry instead.",
|
|
||||||
"invalid_phone_number": "Please enter a valid phone number.",
|
|
||||||
"invalid_sms_code": "The SMS code you entered is invalid.",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"reconfigure": {
|
|
||||||
"data": {
|
|
||||||
"phone_number": "[%key:component::fressnapf_tracker::config::step::user::data::phone_number%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"phone_number": "[%key:component::fressnapf_tracker::config::step::user::data_description::phone_number%]"
|
|
||||||
},
|
|
||||||
"description": "Re-authenticate with your Fressnapf Tracker account to refresh your credentials."
|
|
||||||
},
|
|
||||||
"reconfigure_sms_code": {
|
|
||||||
"data": {
|
|
||||||
"sms_code": "[%key:component::fressnapf_tracker::config::step::sms_code::data::sms_code%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"sms_code": "[%key:component::fressnapf_tracker::config::step::sms_code::data_description::sms_code%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sms_code": {
|
|
||||||
"data": {
|
|
||||||
"sms_code": "SMS code"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"sms_code": "Enter the SMS code you received on your phone."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"phone_number": "Phone number"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"phone_number": "Enter your phone number in international format (e.g., +4917612345678)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"binary_sensor": {
|
|
||||||
"deep_sleep": {
|
|
||||||
"name": "Deep sleep"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/fronius",
|
"documentation": "https://www.home-assistant.io/integrations/fronius",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyfronius"],
|
"loggers": ["pyfronius"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
|
|||||||
@@ -23,5 +23,5 @@
|
|||||||
"winter_mode": {}
|
"winter_mode": {}
|
||||||
},
|
},
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20251202.0"]
|
"requirements": ["home-assistant-frontend==20251127.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -16,9 +15,7 @@ from homeassistant.helpers import singleton
|
|||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
DATA_STORAGE: HassKey[dict[str, asyncio.Future[UserStore]]] = HassKey(
|
DATA_STORAGE: HassKey[dict[str, UserStore]] = HassKey("frontend_storage")
|
||||||
"frontend_storage"
|
|
||||||
)
|
|
||||||
DATA_SYSTEM_STORAGE: HassKey[SystemStore] = HassKey("frontend_system_storage")
|
DATA_SYSTEM_STORAGE: HassKey[SystemStore] = HassKey("frontend_system_storage")
|
||||||
STORAGE_VERSION_USER_DATA = 1
|
STORAGE_VERSION_USER_DATA = 1
|
||||||
STORAGE_VERSION_SYSTEM_DATA = 1
|
STORAGE_VERSION_SYSTEM_DATA = 1
|
||||||
@@ -37,18 +34,11 @@ async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
|
|||||||
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
|
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
|
||||||
"""Access a user store."""
|
"""Access a user store."""
|
||||||
stores = hass.data.setdefault(DATA_STORAGE, {})
|
stores = hass.data.setdefault(DATA_STORAGE, {})
|
||||||
if (future := stores.get(user_id)) is None:
|
if (store := stores.get(user_id)) is None:
|
||||||
future = stores[user_id] = hass.loop.create_future()
|
store = stores[user_id] = UserStore(hass, user_id)
|
||||||
store = UserStore(hass, user_id)
|
await store.async_load()
|
||||||
try:
|
|
||||||
await store.async_load()
|
|
||||||
except BaseException as ex:
|
|
||||||
del stores[user_id]
|
|
||||||
future.set_exception(ex)
|
|
||||||
raise
|
|
||||||
future.set_result(store)
|
|
||||||
|
|
||||||
return await future
|
return store
|
||||||
|
|
||||||
|
|
||||||
class UserStore:
|
class UserStore:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
from secrets import token_hex
|
from secrets import token_hex
|
||||||
import shutil
|
import shutil
|
||||||
from tempfile import mkdtemp
|
|
||||||
|
|
||||||
from aiohttp import BasicAuth, ClientSession, UnixConnector
|
from aiohttp import BasicAuth, ClientSession, UnixConnector
|
||||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||||
@@ -63,11 +62,11 @@ from .const import (
|
|||||||
CONF_DEBUG_UI,
|
CONF_DEBUG_UI,
|
||||||
DEBUG_UI_URL_MESSAGE,
|
DEBUG_UI_URL_MESSAGE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
HA_MANAGED_UNIX_SOCKET,
|
||||||
HA_MANAGED_URL,
|
HA_MANAGED_URL,
|
||||||
RECOMMENDED_VERSION,
|
RECOMMENDED_VERSION,
|
||||||
)
|
)
|
||||||
from .server import Server
|
from .server import Server
|
||||||
from .util import get_go2rtc_unix_socket_path
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -155,12 +154,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
auth = BasicAuth(username, password)
|
auth = BasicAuth(username, password)
|
||||||
# HA will manage the binary
|
# HA will manage the binary
|
||||||
temp_dir = mkdtemp(prefix="go2rtc-")
|
|
||||||
# Manually created session (not using the helper) needs to be closed manually
|
# Manually created session (not using the helper) needs to be closed manually
|
||||||
# See on_stop listener below
|
# See on_stop listener below
|
||||||
session = ClientSession(
|
session = ClientSession(
|
||||||
connector=UnixConnector(path=get_go2rtc_unix_socket_path(temp_dir)),
|
connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth
|
||||||
auth=auth,
|
|
||||||
)
|
)
|
||||||
server = Server(
|
server = Server(
|
||||||
hass,
|
hass,
|
||||||
@@ -169,7 +166,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
enable_ui=domain_config.get(CONF_DEBUG_UI, False),
|
enable_ui=domain_config.get(CONF_DEBUG_UI, False),
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
working_dir=temp_dir,
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await server.start()
|
await server.start()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ CONF_DEBUG_UI = "debug_ui"
|
|||||||
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
|
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
|
||||||
HA_MANAGED_API_PORT = 11984
|
HA_MANAGED_API_PORT = 11984
|
||||||
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
|
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
|
||||||
|
HA_MANAGED_UNIX_SOCKET = "/run/go2rtc.sock"
|
||||||
# When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA)
|
# When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA)
|
||||||
# in script/hassfest/docker.py.
|
# in script/hassfest/docker.py.
|
||||||
RECOMMENDED_VERSION = "1.9.12"
|
RECOMMENDED_VERSION = "1.9.12"
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ from go2rtc_client import Go2RtcRestClient
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
|
from .const import HA_MANAGED_API_PORT, HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL
|
||||||
from .util import get_go2rtc_unix_socket_path
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_TERMINATE_TIMEOUT = 5
|
_TERMINATE_TIMEOUT = 5
|
||||||
_SETUP_TIMEOUT = 30
|
_SETUP_TIMEOUT = 30
|
||||||
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
|
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
|
||||||
|
_LOCALHOST_IP = "127.0.0.1"
|
||||||
_LOG_BUFFER_SIZE = 512
|
_LOG_BUFFER_SIZE = 512
|
||||||
_RESPAWN_COOLDOWN = 1
|
_RESPAWN_COOLDOWN = 1
|
||||||
|
|
||||||
@@ -122,9 +122,7 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str:
|
|||||||
return f"[{formatted_items}]"
|
return f"[{formatted_items}]"
|
||||||
|
|
||||||
|
|
||||||
def _create_temp_file(
|
def _create_temp_file(enable_ui: bool, username: str, password: str) -> str:
|
||||||
enable_ui: bool, username: str, password: str, working_dir: str
|
|
||||||
) -> str:
|
|
||||||
"""Create temporary config file."""
|
"""Create temporary config file."""
|
||||||
app_modules: tuple[str, ...] = _APP_MODULES
|
app_modules: tuple[str, ...] = _APP_MODULES
|
||||||
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
|
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
|
||||||
@@ -141,13 +139,11 @@ def _create_temp_file(
|
|||||||
|
|
||||||
# Set delete=False to prevent the file from being deleted when the file is closed
|
# Set delete=False to prevent the file from being deleted when the file is closed
|
||||||
# Linux is clearing tmp folder on reboot, so no need to delete it manually
|
# Linux is clearing tmp folder on reboot, so no need to delete it manually
|
||||||
with NamedTemporaryFile(
|
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
|
||||||
prefix="go2rtc_", suffix=".yaml", dir=working_dir, delete=False
|
|
||||||
) as file:
|
|
||||||
file.write(
|
file.write(
|
||||||
_GO2RTC_CONFIG_FORMAT.format(
|
_GO2RTC_CONFIG_FORMAT.format(
|
||||||
listen_config=listen_config,
|
listen_config=listen_config,
|
||||||
unix_socket=get_go2rtc_unix_socket_path(working_dir),
|
unix_socket=HA_MANAGED_UNIX_SOCKET,
|
||||||
app_modules=_format_list_for_yaml(app_modules),
|
app_modules=_format_list_for_yaml(app_modules),
|
||||||
api_allow_paths=_format_list_for_yaml(api_paths),
|
api_allow_paths=_format_list_for_yaml(api_paths),
|
||||||
username=username,
|
username=username,
|
||||||
@@ -169,7 +165,6 @@ class Server:
|
|||||||
enable_ui: bool = False,
|
enable_ui: bool = False,
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
working_dir: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the server."""
|
"""Initialize the server."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
@@ -178,7 +173,6 @@ class Server:
|
|||||||
self._enable_ui = enable_ui
|
self._enable_ui = enable_ui
|
||||||
self._username = username
|
self._username = username
|
||||||
self._password = password
|
self._password = password
|
||||||
self._working_dir = working_dir
|
|
||||||
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
|
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
|
||||||
self._process: asyncio.subprocess.Process | None = None
|
self._process: asyncio.subprocess.Process | None = None
|
||||||
self._startup_complete = asyncio.Event()
|
self._startup_complete = asyncio.Event()
|
||||||
@@ -196,11 +190,7 @@ class Server:
|
|||||||
"""Start the server."""
|
"""Start the server."""
|
||||||
_LOGGER.debug("Starting go2rtc server")
|
_LOGGER.debug("Starting go2rtc server")
|
||||||
config_file = await self._hass.async_add_executor_job(
|
config_file = await self._hass.async_add_executor_job(
|
||||||
_create_temp_file,
|
_create_temp_file, self._enable_ui, self._username, self._password
|
||||||
self._enable_ui,
|
|
||||||
self._username,
|
|
||||||
self._password,
|
|
||||||
self._working_dir,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._startup_complete.clear()
|
self._startup_complete.clear()
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
"""Go2rtc utility functions."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
_HA_MANAGED_UNIX_SOCKET_FILE = "go2rtc.sock"
|
|
||||||
|
|
||||||
|
|
||||||
def get_go2rtc_unix_socket_path(path: str | Path) -> str:
|
|
||||||
"""Get the Go2rtc unix socket path."""
|
|
||||||
if not isinstance(path, Path):
|
|
||||||
path = Path(path)
|
|
||||||
return str(path / _HA_MANAGED_UNIX_SOCKET_FILE)
|
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["application_credentials"],
|
"dependencies": ["application_credentials"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"loggers": ["googleapiclient"],
|
||||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"]
|
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"]
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["google_air_quality_api"],
|
"loggers": ["google_air_quality_api"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["google_air_quality_api==1.1.3"]
|
"requirements": ["google_air_quality_api==1.1.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/google_translate",
|
"documentation": "https://www.home-assistant.io/integrations/google_translate",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["gtts"],
|
"loggers": ["gtts"],
|
||||||
"requirements": ["gTTS==2.5.3"]
|
"requirements": ["gTTS==2.5.3"]
|
||||||
|
|||||||
@@ -18,12 +18,10 @@ from homeassistant.components.notify import (
|
|||||||
SERVICE_SEND_MESSAGE,
|
SERVICE_SEND_MESSAGE,
|
||||||
BaseNotificationService,
|
BaseNotificationService,
|
||||||
NotifyEntity,
|
NotifyEntity,
|
||||||
NotifyEntityFeature,
|
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
|
||||||
CONF_ACTION,
|
CONF_ACTION,
|
||||||
CONF_ENTITIES,
|
CONF_ENTITIES,
|
||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
@@ -175,23 +173,14 @@ class NotifyGroup(GroupEntity, NotifyEntity):
|
|||||||
|
|
||||||
async def async_send_message(self, message: str, title: str | None = None) -> None:
|
async def async_send_message(self, message: str, title: str | None = None) -> None:
|
||||||
"""Send a message to all members of the group."""
|
"""Send a message to all members of the group."""
|
||||||
|
|
||||||
data = {
|
|
||||||
ATTR_MESSAGE: message,
|
|
||||||
ATTR_ENTITY_ID: self._entity_ids,
|
|
||||||
}
|
|
||||||
|
|
||||||
# add title only if supported and provided
|
|
||||||
if (
|
|
||||||
title is not None
|
|
||||||
and self._attr_supported_features & NotifyEntityFeature.TITLE
|
|
||||||
):
|
|
||||||
data[ATTR_TITLE] = title
|
|
||||||
|
|
||||||
await self.hass.services.async_call(
|
await self.hass.services.async_call(
|
||||||
NOTIFY_DOMAIN,
|
NOTIFY_DOMAIN,
|
||||||
SERVICE_SEND_MESSAGE,
|
SERVICE_SEND_MESSAGE,
|
||||||
data,
|
{
|
||||||
|
ATTR_MESSAGE: message,
|
||||||
|
ATTR_TITLE: title,
|
||||||
|
ATTR_ENTITY_ID: self._entity_ids,
|
||||||
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
context=self._context,
|
context=self._context,
|
||||||
)
|
)
|
||||||
@@ -205,15 +194,3 @@ class NotifyGroup(GroupEntity, NotifyEntity):
|
|||||||
for entity_id in self._entity_ids
|
for entity_id in self._entity_ids
|
||||||
if (state := self.hass.states.get(entity_id)) is not None
|
if (state := self.hass.states.get(entity_id)) is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Support title if all members support it
|
|
||||||
self._attr_supported_features |= NotifyEntityFeature.TITLE
|
|
||||||
for entity_id in self._entity_ids:
|
|
||||||
state = self.hass.states.get(entity_id)
|
|
||||||
if (
|
|
||||||
state is None
|
|
||||||
or not state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
||||||
& NotifyEntityFeature.TITLE
|
|
||||||
):
|
|
||||||
self._attr_supported_features &= ~NotifyEntityFeature.TITLE
|
|
||||||
break
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ def get_device_list_classic(
|
|||||||
login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD])
|
login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||||
# DEBUG: Log the actual response structure
|
# DEBUG: Log the actual response structure
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
_LOGGER.error("DEBUG - Login response: %s", login_response)
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryError(
|
||||||
f"Error communicating with Growatt API during login: {ex}"
|
f"Error communicating with Growatt API during login: {ex}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
min_settings = self.api.min_settings(self.device_id)
|
min_settings = self.api.min_settings(self.device_id)
|
||||||
min_energy = self.api.min_energy(self.device_id)
|
min_energy = self.api.min_energy(self.device_id)
|
||||||
except growattServer.GrowattV1ApiError as err:
|
except growattServer.GrowattV1ApiError as err:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error fetching min device data for %s: %s", self.device_id, err
|
||||||
|
)
|
||||||
raise UpdateFailed(f"Error fetching min device data: {err}") from err
|
raise UpdateFailed(f"Error fetching min device data: {err}") from err
|
||||||
|
|
||||||
min_info = {**min_details, **min_settings, **min_energy}
|
min_info = {**min_details, **min_settings, **min_energy}
|
||||||
@@ -177,6 +180,7 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
try:
|
try:
|
||||||
return await self.hass.async_add_executor_job(self._sync_update_data)
|
return await self.hass.async_add_executor_job(self._sync_update_data)
|
||||||
except json.decoder.JSONDecodeError as err:
|
except json.decoder.JSONDecodeError as err:
|
||||||
|
_LOGGER.error("Unable to fetch data from Growatt server: %s", err)
|
||||||
raise UpdateFailed(f"Error fetching data: {err}") from err
|
raise UpdateFailed(f"Error fetching data: {err}") from err
|
||||||
|
|
||||||
def get_currency(self):
|
def get_currency(self):
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup: done
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow:
|
|
||||||
status: todo
|
|
||||||
comment: data-descriptions missing
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions: done
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup: done
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: done
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters:
|
|
||||||
status: todo
|
|
||||||
comment: Update server URL dropdown to show regional descriptions (e.g., 'China', 'United States') instead of raw URLs.
|
|
||||||
docs-installation-parameters: todo
|
|
||||||
entity-unavailable:
|
|
||||||
status: todo
|
|
||||||
comment: Replace bare Exception catches in __init__.py with specific growattServer exceptions.
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: done
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow: todo
|
|
||||||
test-coverage: todo
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices:
|
|
||||||
status: todo
|
|
||||||
comment: Add serial_number field to DeviceInfo in sensor, number, and switch platforms using device_id/serial_id.
|
|
||||||
diagnostics: todo
|
|
||||||
discovery-update-info: todo
|
|
||||||
discovery: todo
|
|
||||||
docs-data-update: todo
|
|
||||||
docs-examples: todo
|
|
||||||
docs-known-limitations: todo
|
|
||||||
docs-supported-devices: todo
|
|
||||||
docs-supported-functions: todo
|
|
||||||
docs-troubleshooting: todo
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category:
|
|
||||||
status: todo
|
|
||||||
comment: Add EntityCategory.DIAGNOSTIC to temperature and other diagnostic sensors. Merge GrowattRequiredKeysMixin into GrowattSensorEntityDescription using kw_only=True.
|
|
||||||
entity-device-class:
|
|
||||||
status: todo
|
|
||||||
comment: Replace custom precision field with suggested_display_precision to preserve full data granularity.
|
|
||||||
entity-disabled-by-default: todo
|
|
||||||
entity-translations: todo
|
|
||||||
exception-translations: todo
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not raise repairable issues.
|
|
||||||
stale-devices: todo
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: todo
|
|
||||||
inject-websession: todo
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -17,7 +17,7 @@ from aiohasupervisor.models import GreenOptions, YellowOptions # noqa: F401
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.components import panel_custom
|
from homeassistant.components import frontend, panel_custom
|
||||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||||
from homeassistant.components.http import StaticPathConfig
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||||
@@ -329,6 +329,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async_load_websocket_api(hass)
|
async_load_websocket_api(hass)
|
||||||
|
frontend.async_register_built_in_panel(hass, "app")
|
||||||
|
|
||||||
host = os.environ["SUPERVISOR"]
|
host = os.environ["SUPERVISOR"]
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ EXTRA_PLACEHOLDERS = {
|
|||||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED: HELP_URLS,
|
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED: HELP_URLS,
|
||||||
ISSUE_KEY_SYSTEM_FREE_SPACE: {
|
ISSUE_KEY_SYSTEM_FREE_SPACE: {
|
||||||
"more_info_free_space": "https://www.home-assistant.io/more-info/free-space",
|
"more_info_free_space": "https://www.home-assistant.io/more-info/free-space",
|
||||||
"storage_url": "/config/storage",
|
|
||||||
},
|
},
|
||||||
ISSUE_KEY_ADDON_PWNED: {
|
ISSUE_KEY_ADDON_PWNED: {
|
||||||
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
|
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
"title": "Restart(s) required"
|
"title": "Restart(s) required"
|
||||||
},
|
},
|
||||||
"issue_system_free_space": {
|
"issue_system_free_space": {
|
||||||
"description": "The data disk has only {free_space}GB free space left. This may cause issues with system stability and interfere with functionality such as backups and updates. Go to [storage]({storage_url}) to see what is taking up space or see [clear up storage]({more_info_free_space}) for tips on how to free up space.",
|
"description": "The data disk has only {free_space}GB free space left. This may cause issues with system stability and interfere with functionality such as backups and updates. See [clear up storage]({more_info_free_space}) for tips on how to free up space.",
|
||||||
"title": "Data disk is running low on free space"
|
"title": "Data disk is running low on free space"
|
||||||
},
|
},
|
||||||
"issue_system_multiple_data_disks": {
|
"issue_system_multiple_data_disks": {
|
||||||
|
|||||||
@@ -19,10 +19,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiohomeconnect==0.24.0"],
|
"requirements": ["aiohomeconnect==0.23.1"],
|
||||||
"zeroconf": ["_homeconnect._tcp.local."]
|
"zeroconf": ["_homeconnect._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ from .const import (
|
|||||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||||
PID,
|
PID,
|
||||||
PRODUCT,
|
PRODUCT,
|
||||||
|
RADIO_TX_POWER_DBM_BY_COUNTRY,
|
||||||
|
RADIO_TX_POWER_DBM_DEFAULT,
|
||||||
SERIAL_NUMBER,
|
SERIAL_NUMBER,
|
||||||
VID,
|
VID,
|
||||||
)
|
)
|
||||||
@@ -112,6 +114,21 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
|||||||
next_step_id="finish_thread_installation",
|
next_step_id="finish_thread_installation",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _extra_zha_hardware_options(self) -> dict[str, Any]:
|
||||||
|
"""Return extra ZHA hardware options."""
|
||||||
|
country = self.hass.config.country
|
||||||
|
|
||||||
|
if country is None:
|
||||||
|
tx_power = RADIO_TX_POWER_DBM_DEFAULT
|
||||||
|
else:
|
||||||
|
tx_power = RADIO_TX_POWER_DBM_BY_COUNTRY.get(
|
||||||
|
country, RADIO_TX_POWER_DBM_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tx_power": tx_power,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantConnectZBT2ConfigFlow(
|
class HomeAssistantConnectZBT2ConfigFlow(
|
||||||
ZBT2FirmwareMixin,
|
ZBT2FirmwareMixin,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Constants for the Home Assistant Connect ZBT-2 integration."""
|
"""Constants for the Home Assistant Connect ZBT-2 integration."""
|
||||||
|
|
||||||
|
from homeassistant.generated.countries import COUNTRIES
|
||||||
|
|
||||||
DOMAIN = "homeassistant_connect_zbt2"
|
DOMAIN = "homeassistant_connect_zbt2"
|
||||||
|
|
||||||
NABU_CASA_FIRMWARE_RELEASES_URL = (
|
NABU_CASA_FIRMWARE_RELEASES_URL = (
|
||||||
@@ -17,3 +19,59 @@ VID = "vid"
|
|||||||
DEVICE = "device"
|
DEVICE = "device"
|
||||||
|
|
||||||
HARDWARE_NAME = "Home Assistant Connect ZBT-2"
|
HARDWARE_NAME = "Home Assistant Connect ZBT-2"
|
||||||
|
|
||||||
|
RADIO_TX_POWER_DBM_DEFAULT = 8
|
||||||
|
RADIO_TX_POWER_DBM_BY_COUNTRY = {
|
||||||
|
# EU Member States
|
||||||
|
"AT": 10,
|
||||||
|
"BE": 10,
|
||||||
|
"BG": 10,
|
||||||
|
"HR": 10,
|
||||||
|
"CY": 10,
|
||||||
|
"CZ": 10,
|
||||||
|
"DK": 10,
|
||||||
|
"EE": 10,
|
||||||
|
"FI": 10,
|
||||||
|
"FR": 10,
|
||||||
|
"DE": 10,
|
||||||
|
"GR": 10,
|
||||||
|
"HU": 10,
|
||||||
|
"IE": 10,
|
||||||
|
"IT": 10,
|
||||||
|
"LV": 10,
|
||||||
|
"LT": 10,
|
||||||
|
"LU": 10,
|
||||||
|
"MT": 10,
|
||||||
|
"NL": 10,
|
||||||
|
"PL": 10,
|
||||||
|
"PT": 10,
|
||||||
|
"RO": 10,
|
||||||
|
"SK": 10,
|
||||||
|
"SI": 10,
|
||||||
|
"ES": 10,
|
||||||
|
"SE": 10,
|
||||||
|
# EEA Members
|
||||||
|
"IS": 10,
|
||||||
|
"LI": 10,
|
||||||
|
"NO": 10,
|
||||||
|
# Standards harmonized with RED or ETSI
|
||||||
|
"CH": 10,
|
||||||
|
"GB": 10,
|
||||||
|
"TR": 10,
|
||||||
|
"AL": 10,
|
||||||
|
"BA": 10,
|
||||||
|
"GE": 10,
|
||||||
|
"MD": 10,
|
||||||
|
"ME": 10,
|
||||||
|
"MK": 10,
|
||||||
|
"RS": 10,
|
||||||
|
"UA": 10,
|
||||||
|
# Other CEPT nations
|
||||||
|
"AD": 10,
|
||||||
|
"AZ": 10,
|
||||||
|
"MC": 10,
|
||||||
|
"SM": 10,
|
||||||
|
"VA": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert set(RADIO_TX_POWER_DBM_BY_COUNTRY) <= COUNTRIES
|
||||||
|
|||||||
@@ -33,14 +33,13 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
|
|
||||||
from .const import DOMAIN, OTBR_DOMAIN, Z2M_EMBER_DOCS_URL, ZHA_DOMAIN
|
from .const import OTBR_DOMAIN, Z2M_EMBER_DOCS_URL, ZHA_DOMAIN
|
||||||
from .util import (
|
from .util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
OwningAddon,
|
OwningAddon,
|
||||||
OwningIntegration,
|
OwningIntegration,
|
||||||
ResetTarget,
|
ResetTarget,
|
||||||
async_firmware_flashing_context,
|
|
||||||
async_flash_silabs_firmware,
|
async_flash_silabs_firmware,
|
||||||
get_otbr_addon_manager,
|
get_otbr_addon_manager,
|
||||||
guess_firmware_info,
|
guess_firmware_info,
|
||||||
@@ -229,95 +228,83 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
|||||||
# Keep track of the firmware we're working with, for error messages
|
# Keep track of the firmware we're working with, for error messages
|
||||||
self.installing_firmware_name = firmware_name
|
self.installing_firmware_name = firmware_name
|
||||||
|
|
||||||
# For the duration of firmware flashing, hint to other integrations (i.e. ZHA)
|
# Installing new firmware is only truly required if the wrong type is
|
||||||
# that the hardware is in use and should not be accessed. This is separate from
|
# installed: upgrading to the latest release of the current firmware type
|
||||||
# locking the serial port itself, since a momentary release of the port may
|
# isn't strictly necessary for functionality.
|
||||||
# still allow for ZHA to reclaim the device.
|
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||||
async with async_firmware_flashing_context(self.hass, self._device, DOMAIN):
|
self._device,
|
||||||
# Installing new firmware is only truly required if the wrong type is
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
# installed: upgrading to the latest release of the current firmware type
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
# isn't strictly necessary for functionality.
|
)
|
||||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
|
||||||
self._device,
|
firmware_install_required = self._probed_firmware_info is None or (
|
||||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
self._probed_firmware_info.firmware_type != expected_installed_firmware_type
|
||||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
)
|
||||||
|
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
client = FirmwareUpdateClient(fw_update_url, session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = await client.async_update_data()
|
||||||
|
fw_manifest = next(
|
||||||
|
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
|
||||||
)
|
)
|
||||||
|
except (StopIteration, TimeoutError, ClientError, ManifestMissing) as err:
|
||||||
|
_LOGGER.warning("Failed to fetch firmware update manifest", exc_info=True)
|
||||||
|
|
||||||
firmware_install_required = self._probed_firmware_info is None or (
|
# Not having internet access should not prevent setup
|
||||||
self._probed_firmware_info.firmware_type
|
|
||||||
!= expected_installed_firmware_type
|
|
||||||
)
|
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
client = FirmwareUpdateClient(fw_update_url, session)
|
|
||||||
|
|
||||||
try:
|
|
||||||
manifest = await client.async_update_data()
|
|
||||||
fw_manifest = next(
|
|
||||||
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
|
|
||||||
)
|
|
||||||
except (StopIteration, TimeoutError, ClientError, ManifestMissing) as err:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Failed to fetch firmware update manifest", exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Not having internet access should not prevent setup
|
|
||||||
if not firmware_install_required:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Skipping firmware upgrade due to index download failure"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
raise AbortFlow(
|
|
||||||
reason="fw_download_failed",
|
|
||||||
description_placeholders=self._get_translation_placeholders(),
|
|
||||||
) from err
|
|
||||||
|
|
||||||
if not firmware_install_required:
|
if not firmware_install_required:
|
||||||
assert self._probed_firmware_info is not None
|
_LOGGER.debug("Skipping firmware upgrade due to index download failure")
|
||||||
|
return
|
||||||
|
|
||||||
# Make sure we do not downgrade the firmware
|
raise AbortFlow(
|
||||||
fw_metadata = NabuCasaMetadata.from_json(fw_manifest.metadata)
|
reason="fw_download_failed",
|
||||||
fw_version = fw_metadata.get_public_version()
|
description_placeholders=self._get_translation_placeholders(),
|
||||||
probed_fw_version = Version(self._probed_firmware_info.firmware_version)
|
) from err
|
||||||
|
|
||||||
if probed_fw_version >= fw_version:
|
if not firmware_install_required:
|
||||||
_LOGGER.debug(
|
assert self._probed_firmware_info is not None
|
||||||
"Not downgrading firmware, installed %s is newer than available %s",
|
|
||||||
probed_fw_version,
|
|
||||||
fw_version,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
# Make sure we do not downgrade the firmware
|
||||||
fw_data = await client.async_fetch_firmware(fw_manifest)
|
fw_metadata = NabuCasaMetadata.from_json(fw_manifest.metadata)
|
||||||
except (TimeoutError, ClientError, ValueError) as err:
|
fw_version = fw_metadata.get_public_version()
|
||||||
_LOGGER.warning("Failed to fetch firmware update", exc_info=True)
|
probed_fw_version = Version(self._probed_firmware_info.firmware_version)
|
||||||
|
|
||||||
# If we cannot download new firmware, we shouldn't block setup
|
if probed_fw_version >= fw_version:
|
||||||
if not firmware_install_required:
|
_LOGGER.debug(
|
||||||
_LOGGER.debug(
|
"Not downgrading firmware, installed %s is newer than available %s",
|
||||||
"Skipping firmware upgrade due to image download failure"
|
probed_fw_version,
|
||||||
)
|
fw_version,
|
||||||
return
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Otherwise, fail
|
try:
|
||||||
raise AbortFlow(
|
fw_data = await client.async_fetch_firmware(fw_manifest)
|
||||||
reason="fw_download_failed",
|
except (TimeoutError, ClientError, ValueError) as err:
|
||||||
description_placeholders=self._get_translation_placeholders(),
|
_LOGGER.warning("Failed to fetch firmware update", exc_info=True)
|
||||||
) from err
|
|
||||||
|
|
||||||
self._probed_firmware_info = await async_flash_silabs_firmware(
|
# If we cannot download new firmware, we shouldn't block setup
|
||||||
hass=self.hass,
|
if not firmware_install_required:
|
||||||
device=self._device,
|
_LOGGER.debug("Skipping firmware upgrade due to image download failure")
|
||||||
fw_data=fw_data,
|
return
|
||||||
expected_installed_firmware_type=expected_installed_firmware_type,
|
|
||||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
# Otherwise, fail
|
||||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
raise AbortFlow(
|
||||||
progress_callback=lambda offset, total: self.async_update_progress(
|
reason="fw_download_failed",
|
||||||
offset / total
|
description_placeholders=self._get_translation_placeholders(),
|
||||||
),
|
) from err
|
||||||
)
|
|
||||||
|
self._probed_firmware_info = await async_flash_silabs_firmware(
|
||||||
|
hass=self.hass,
|
||||||
|
device=self._device,
|
||||||
|
fw_data=fw_data,
|
||||||
|
expected_installed_firmware_type=expected_installed_firmware_type,
|
||||||
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
|
progress_callback=lambda offset, total: self.async_update_progress(
|
||||||
|
offset / total
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async def _configure_and_start_otbr_addon(self) -> None:
|
async def _configure_and_start_otbr_addon(self) -> None:
|
||||||
"""Configure and start the OTBR addon."""
|
"""Configure and start the OTBR addon."""
|
||||||
@@ -457,6 +444,10 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
|||||||
# This step is necessary to prevent `user_input` from being passed through
|
# This step is necessary to prevent `user_input` from being passed through
|
||||||
return await self.async_step_continue_zigbee()
|
return await self.async_step_continue_zigbee()
|
||||||
|
|
||||||
|
def _extra_zha_hardware_options(self) -> dict[str, Any]:
|
||||||
|
"""Return extra ZHA hardware options."""
|
||||||
|
return {}
|
||||||
|
|
||||||
async def async_step_continue_zigbee(
|
async def async_step_continue_zigbee(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -479,6 +470,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
|||||||
},
|
},
|
||||||
"radio_type": "ezsp",
|
"radio_type": "ezsp",
|
||||||
"flow_strategy": self._zigbee_flow_strategy,
|
"flow_strategy": self._zigbee_flow_strategy,
|
||||||
|
**self._extra_zha_hardware_options(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return self._continue_zha_flow(result)
|
return self._continue_zha_flow(result)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ from .util import (
|
|||||||
ApplicationType,
|
ApplicationType,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
ResetTarget,
|
ResetTarget,
|
||||||
async_firmware_flashing_context,
|
|
||||||
async_flash_silabs_firmware,
|
async_flash_silabs_firmware,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -275,18 +274,16 @@ class BaseFirmwareUpdateEntity(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_firmware_flashing_context(
|
firmware_info = await async_flash_silabs_firmware(
|
||||||
self.hass, self._current_device, self._config_entry.domain
|
hass=self.hass,
|
||||||
):
|
device=self._current_device,
|
||||||
firmware_info = await async_flash_silabs_firmware(
|
fw_data=fw_data,
|
||||||
hass=self.hass,
|
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||||
device=self._current_device,
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
fw_data=fw_data,
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
progress_callback=self._update_progress,
|
||||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
domain=self._config_entry.domain,
|
||||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
)
|
||||||
progress_callback=self._update_progress,
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
self._attr_in_progress = False
|
self._attr_in_progress = False
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.singleton import singleton
|
|||||||
|
|
||||||
from . import DATA_COMPONENT
|
from . import DATA_COMPONENT
|
||||||
from .const import (
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
OTBR_ADDON_MANAGER_DATA,
|
OTBR_ADDON_MANAGER_DATA,
|
||||||
OTBR_ADDON_NAME,
|
OTBR_ADDON_NAME,
|
||||||
OTBR_ADDON_SLUG,
|
OTBR_ADDON_SLUG,
|
||||||
@@ -365,22 +366,6 @@ async def probe_silabs_firmware_type(
|
|||||||
return fw_info.firmware_type
|
return fw_info.firmware_type
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def async_firmware_flashing_context(
|
|
||||||
hass: HomeAssistant, device: str, source_domain: str
|
|
||||||
) -> AsyncIterator[None]:
|
|
||||||
"""Register a device as having its firmware being actively interacted with."""
|
|
||||||
async with async_firmware_update_context(hass, device, source_domain):
|
|
||||||
firmware_info = await guess_firmware_info(hass, device)
|
|
||||||
_LOGGER.debug("Guessed firmware info before update: %s", firmware_info)
|
|
||||||
|
|
||||||
async with AsyncExitStack() as stack:
|
|
||||||
for owner in firmware_info.owners:
|
|
||||||
await stack.enter_async_context(owner.temporarily_stop(hass))
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
async def async_flash_silabs_firmware(
|
async def async_flash_silabs_firmware(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device: str,
|
device: str,
|
||||||
@@ -389,11 +374,10 @@ async def async_flash_silabs_firmware(
|
|||||||
bootloader_reset_methods: Sequence[ResetTarget],
|
bootloader_reset_methods: Sequence[ResetTarget],
|
||||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||||
progress_callback: Callable[[int, int], None] | None = None,
|
progress_callback: Callable[[int, int], None] | None = None,
|
||||||
|
*,
|
||||||
|
domain: str = DOMAIN,
|
||||||
) -> FirmwareInfo:
|
) -> FirmwareInfo:
|
||||||
"""Flash firmware to the SiLabs device.
|
"""Flash firmware to the SiLabs device."""
|
||||||
|
|
||||||
This function is meant to be used within a firmware update context.
|
|
||||||
"""
|
|
||||||
if not any(
|
if not any(
|
||||||
method == expected_installed_firmware_type
|
method == expected_installed_firmware_type
|
||||||
for method, _ in application_probe_methods
|
for method, _ in application_probe_methods
|
||||||
@@ -403,44 +387,54 @@ async def async_flash_silabs_firmware(
|
|||||||
f" not in application probe methods {application_probe_methods!r}"
|
f" not in application probe methods {application_probe_methods!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
async with async_firmware_update_context(hass, device, domain):
|
||||||
|
firmware_info = await guess_firmware_info(hass, device)
|
||||||
|
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||||
|
|
||||||
flasher = Flasher(
|
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||||
device=device,
|
|
||||||
probe_methods=tuple(
|
|
||||||
(m.as_flasher_application_type(), baudrate)
|
|
||||||
for m, baudrate in application_probe_methods
|
|
||||||
),
|
|
||||||
bootloader_reset=tuple(
|
|
||||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
flasher = Flasher(
|
||||||
# Enter the bootloader with indeterminate progress
|
device=device,
|
||||||
await flasher.enter_bootloader()
|
probe_methods=tuple(
|
||||||
|
(m.as_flasher_application_type(), baudrate)
|
||||||
|
for m, baudrate in application_probe_methods
|
||||||
|
),
|
||||||
|
bootloader_reset=tuple(
|
||||||
|
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Flash the firmware, with progress
|
async with AsyncExitStack() as stack:
|
||||||
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
|
for owner in firmware_info.owners:
|
||||||
except PermissionError as err:
|
await stack.enter_async_context(owner.temporarily_stop(hass))
|
||||||
raise HomeAssistantError(
|
|
||||||
"Failed to flash firmware: Device is used by another application"
|
|
||||||
) from err
|
|
||||||
except Exception as err:
|
|
||||||
raise HomeAssistantError("Failed to flash firmware") from err
|
|
||||||
|
|
||||||
probed_firmware_info = await probe_silabs_firmware_info(
|
try:
|
||||||
device,
|
# Enter the bootloader with indeterminate progress
|
||||||
bootloader_reset_methods=bootloader_reset_methods,
|
await flasher.enter_bootloader()
|
||||||
# Only probe for the expected installed firmware type
|
|
||||||
application_probe_methods=[
|
|
||||||
(method, baudrate)
|
|
||||||
for method, baudrate in application_probe_methods
|
|
||||||
if method == expected_installed_firmware_type
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
if probed_firmware_info is None:
|
# Flash the firmware, with progress
|
||||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
await flasher.flash_firmware(
|
||||||
|
fw_image, progress_callback=progress_callback
|
||||||
|
)
|
||||||
|
except PermissionError as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
"Failed to flash firmware: Device is used by another application"
|
||||||
|
) from err
|
||||||
|
except Exception as err:
|
||||||
|
raise HomeAssistantError("Failed to flash firmware") from err
|
||||||
|
|
||||||
return probed_firmware_info
|
probed_firmware_info = await probe_silabs_firmware_info(
|
||||||
|
device,
|
||||||
|
bootloader_reset_methods=bootloader_reset_methods,
|
||||||
|
# Only probe for the expected installed firmware type
|
||||||
|
application_probe_methods=[
|
||||||
|
(method, baudrate)
|
||||||
|
for method, baudrate in application_probe_methods
|
||||||
|
if method == expected_installed_firmware_type
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if probed_firmware_info is None:
|
||||||
|
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||||
|
|
||||||
|
return probed_firmware_info
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["bluetooth_adapters", "zeroconf"],
|
"dependencies": ["bluetooth_adapters", "zeroconf"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiohomekit", "commentjson"],
|
"loggers": ["aiohomekit", "commentjson"],
|
||||||
"requirements": ["aiohomekit==3.2.20"],
|
"requirements": ["aiohomekit==3.2.20"],
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homewizard",
|
"documentation": "https://www.home-assistant.io/integrations/homewizard",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["homewizard_energy"],
|
"loggers": ["homewizard_energy"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"not_implemented": "This integration can only be set up via discovery."
|
"not_implemented": "This integration can only be setup via discovery."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
|
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["bleak"],
|
"loggers": ["bleak"],
|
||||||
"requirements": ["ibeacon-ble==1.2.0"],
|
"requirements": ["ibeacon-ble==1.2.0"],
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["iometer==0.3.0"],
|
"requirements": ["iometer==0.2.0"],
|
||||||
"zeroconf": ["_iometer._tcp.local."]
|
"zeroconf": ["_iometer._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,8 +94,6 @@ SERVICE_KNX_EVENT_REGISTER: Final = "event_register"
|
|||||||
SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register"
|
SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register"
|
||||||
SERVICE_KNX_READ: Final = "read"
|
SERVICE_KNX_READ: Final = "read"
|
||||||
|
|
||||||
REPAIR_ISSUE_DATA_SECURE_GROUP_KEY: Final = "data_secure_group_key_issue"
|
|
||||||
|
|
||||||
|
|
||||||
class KNXConfigEntryData(TypedDict, total=False):
|
class KNXConfigEntryData(TypedDict, total=False):
|
||||||
"""Config entry for the KNX integration."""
|
"""Config entry for the KNX integration."""
|
||||||
@@ -164,11 +162,8 @@ SUPPORTED_PLATFORMS_UI: Final = {
|
|||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.COVER,
|
Platform.COVER,
|
||||||
Platform.DATE,
|
|
||||||
Platform.DATETIME,
|
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
Platform.TIME,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Map KNX controller modes to HA modes. This list might not be complete.
|
# Map KNX controller modes to HA modes. This list might not be complete.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date as dt_date
|
from datetime import date as dt_date
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
from xknx import XKNX
|
||||||
from xknx.devices import DateDevice as XknxDateDevice
|
from xknx.devices import DateDevice as XknxDateDevice
|
||||||
from xknx.dpt.dpt_11 import KNXDate as XKNXDate
|
from xknx.dpt.dpt_11 import KNXDate as XKNXDate
|
||||||
|
|
||||||
@@ -18,10 +18,7 @@ from homeassistant.const import (
|
|||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import (
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
AddConfigEntryEntitiesCallback,
|
|
||||||
async_get_current_platform,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
@@ -29,14 +26,11 @@ from .const import (
|
|||||||
CONF_RESPOND_TO_READ,
|
CONF_RESPOND_TO_READ,
|
||||||
CONF_STATE_ADDRESS,
|
CONF_STATE_ADDRESS,
|
||||||
CONF_SYNC_STATE,
|
CONF_SYNC_STATE,
|
||||||
DOMAIN,
|
|
||||||
KNX_ADDRESS,
|
KNX_ADDRESS,
|
||||||
KNX_MODULE_KEY,
|
KNX_MODULE_KEY,
|
||||||
)
|
)
|
||||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
from .entity import KnxYamlEntity
|
||||||
from .knx_module import KNXModule
|
from .knx_module import KNXModule
|
||||||
from .storage.const import CONF_ENTITY, CONF_GA_DATE
|
|
||||||
from .storage.util import ConfigExtractor
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -46,36 +40,40 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up entities for KNX platform."""
|
"""Set up entities for KNX platform."""
|
||||||
knx_module = hass.data[KNX_MODULE_KEY]
|
knx_module = hass.data[KNX_MODULE_KEY]
|
||||||
platform = async_get_current_platform()
|
config: list[ConfigType] = knx_module.config_yaml[Platform.DATE]
|
||||||
knx_module.config_store.add_platform(
|
|
||||||
platform=Platform.DATE,
|
async_add_entities(
|
||||||
controller=KnxUiEntityPlatformController(
|
KNXDateEntity(knx_module, entity_config) for entity_config in config
|
||||||
knx_module=knx_module,
|
|
||||||
entity_platform=platform,
|
|
||||||
entity_class=KnxUiDate,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
entities: list[KnxYamlEntity | KnxUiEntity] = []
|
|
||||||
if yaml_platform_config := knx_module.config_yaml.get(Platform.DATE):
|
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice:
|
||||||
entities.extend(
|
"""Return a XKNX DateTime object to be used within XKNX."""
|
||||||
KnxYamlDate(knx_module, entity_config)
|
return XknxDateDevice(
|
||||||
for entity_config in yaml_platform_config
|
xknx,
|
||||||
)
|
name=config[CONF_NAME],
|
||||||
if ui_config := knx_module.config_store.data["entities"].get(Platform.DATE):
|
localtime=False,
|
||||||
entities.extend(
|
group_address=config[KNX_ADDRESS],
|
||||||
KnxUiDate(knx_module, unique_id, config)
|
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||||
for unique_id, config in ui_config.items()
|
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||||
)
|
sync_state=config[CONF_SYNC_STATE],
|
||||||
if entities:
|
)
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class _KNXDate(DateEntity, RestoreEntity):
|
class KNXDateEntity(KnxYamlEntity, DateEntity, RestoreEntity):
|
||||||
"""Representation of a KNX date."""
|
"""Representation of a KNX date."""
|
||||||
|
|
||||||
_device: XknxDateDevice
|
_device: XknxDateDevice
|
||||||
|
|
||||||
|
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||||
|
"""Initialize a KNX time."""
|
||||||
|
super().__init__(
|
||||||
|
knx_module=knx_module,
|
||||||
|
device=_create_xknx_device(knx_module.xknx, config),
|
||||||
|
)
|
||||||
|
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||||
|
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Restore last state."""
|
"""Restore last state."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
@@ -96,52 +94,3 @@ class _KNXDate(DateEntity, RestoreEntity):
|
|||||||
async def async_set_value(self, value: dt_date) -> None:
|
async def async_set_value(self, value: dt_date) -> None:
|
||||||
"""Change the value."""
|
"""Change the value."""
|
||||||
await self._device.set(value)
|
await self._device.set(value)
|
||||||
|
|
||||||
|
|
||||||
class KnxYamlDate(_KNXDate, KnxYamlEntity):
|
|
||||||
"""Representation of a KNX date configured from YAML."""
|
|
||||||
|
|
||||||
_device: XknxDateDevice
|
|
||||||
|
|
||||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
|
||||||
"""Initialize a KNX date."""
|
|
||||||
super().__init__(
|
|
||||||
knx_module=knx_module,
|
|
||||||
device=XknxDateDevice(
|
|
||||||
knx_module.xknx,
|
|
||||||
name=config[CONF_NAME],
|
|
||||||
localtime=False,
|
|
||||||
group_address=config[KNX_ADDRESS],
|
|
||||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
|
||||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
|
||||||
sync_state=config[CONF_SYNC_STATE],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
|
||||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
|
||||||
|
|
||||||
|
|
||||||
class KnxUiDate(_KNXDate, KnxUiEntity):
|
|
||||||
"""Representation of a KNX date configured from the UI."""
|
|
||||||
|
|
||||||
_device: XknxDateDevice
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""Initialize KNX date."""
|
|
||||||
super().__init__(
|
|
||||||
knx_module=knx_module,
|
|
||||||
unique_id=unique_id,
|
|
||||||
entity_config=config[CONF_ENTITY],
|
|
||||||
)
|
|
||||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
|
||||||
self._device = XknxDateDevice(
|
|
||||||
knx_module.xknx,
|
|
||||||
name=config[CONF_ENTITY][CONF_NAME],
|
|
||||||
localtime=False,
|
|
||||||
group_address=knx_conf.get_write(CONF_GA_DATE),
|
|
||||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_DATE),
|
|
||||||
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
|
|
||||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
from xknx import XKNX
|
||||||
from xknx.devices import DateTimeDevice as XknxDateTimeDevice
|
from xknx.devices import DateTimeDevice as XknxDateTimeDevice
|
||||||
from xknx.dpt.dpt_19 import KNXDateTime as XKNXDateTime
|
from xknx.dpt.dpt_19 import KNXDateTime as XKNXDateTime
|
||||||
|
|
||||||
@@ -18,10 +18,7 @@ from homeassistant.const import (
|
|||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import (
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
AddConfigEntryEntitiesCallback,
|
|
||||||
async_get_current_platform,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
@@ -30,14 +27,11 @@ from .const import (
|
|||||||
CONF_RESPOND_TO_READ,
|
CONF_RESPOND_TO_READ,
|
||||||
CONF_STATE_ADDRESS,
|
CONF_STATE_ADDRESS,
|
||||||
CONF_SYNC_STATE,
|
CONF_SYNC_STATE,
|
||||||
DOMAIN,
|
|
||||||
KNX_ADDRESS,
|
KNX_ADDRESS,
|
||||||
KNX_MODULE_KEY,
|
KNX_MODULE_KEY,
|
||||||
)
|
)
|
||||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
from .entity import KnxYamlEntity
|
||||||
from .knx_module import KNXModule
|
from .knx_module import KNXModule
|
||||||
from .storage.const import CONF_ENTITY, CONF_GA_DATETIME
|
|
||||||
from .storage.util import ConfigExtractor
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -47,36 +41,40 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up entities for KNX platform."""
|
"""Set up entities for KNX platform."""
|
||||||
knx_module = hass.data[KNX_MODULE_KEY]
|
knx_module = hass.data[KNX_MODULE_KEY]
|
||||||
platform = async_get_current_platform()
|
config: list[ConfigType] = knx_module.config_yaml[Platform.DATETIME]
|
||||||
knx_module.config_store.add_platform(
|
|
||||||
platform=Platform.DATETIME,
|
async_add_entities(
|
||||||
controller=KnxUiEntityPlatformController(
|
KNXDateTimeEntity(knx_module, entity_config) for entity_config in config
|
||||||
knx_module=knx_module,
|
|
||||||
entity_platform=platform,
|
|
||||||
entity_class=KnxUiDateTime,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
entities: list[KnxYamlEntity | KnxUiEntity] = []
|
|
||||||
if yaml_platform_config := knx_module.config_yaml.get(Platform.DATETIME):
|
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice:
|
||||||
entities.extend(
|
"""Return a XKNX DateTime object to be used within XKNX."""
|
||||||
KnxYamlDateTime(knx_module, entity_config)
|
return XknxDateTimeDevice(
|
||||||
for entity_config in yaml_platform_config
|
xknx,
|
||||||
)
|
name=config[CONF_NAME],
|
||||||
if ui_config := knx_module.config_store.data["entities"].get(Platform.DATETIME):
|
localtime=False,
|
||||||
entities.extend(
|
group_address=config[KNX_ADDRESS],
|
||||||
KnxUiDateTime(knx_module, unique_id, config)
|
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||||
for unique_id, config in ui_config.items()
|
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||||
)
|
sync_state=config[CONF_SYNC_STATE],
|
||||||
if entities:
|
)
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class _KNXDateTime(DateTimeEntity, RestoreEntity):
|
class KNXDateTimeEntity(KnxYamlEntity, DateTimeEntity, RestoreEntity):
|
||||||
"""Representation of a KNX datetime."""
|
"""Representation of a KNX datetime."""
|
||||||
|
|
||||||
_device: XknxDateTimeDevice
|
_device: XknxDateTimeDevice
|
||||||
|
|
||||||
|
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||||
|
"""Initialize a KNX time."""
|
||||||
|
super().__init__(
|
||||||
|
knx_module=knx_module,
|
||||||
|
device=_create_xknx_device(knx_module.xknx, config),
|
||||||
|
)
|
||||||
|
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||||
|
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Restore last state."""
|
"""Restore last state."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
@@ -101,52 +99,3 @@ class _KNXDateTime(DateTimeEntity, RestoreEntity):
|
|||||||
async def async_set_value(self, value: datetime) -> None:
|
async def async_set_value(self, value: datetime) -> None:
|
||||||
"""Change the value."""
|
"""Change the value."""
|
||||||
await self._device.set(value.astimezone(dt_util.get_default_time_zone()))
|
await self._device.set(value.astimezone(dt_util.get_default_time_zone()))
|
||||||
|
|
||||||
|
|
||||||
class KnxYamlDateTime(_KNXDateTime, KnxYamlEntity):
|
|
||||||
"""Representation of a KNX datetime configured from YAML."""
|
|
||||||
|
|
||||||
_device: XknxDateTimeDevice
|
|
||||||
|
|
||||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
|
||||||
"""Initialize a KNX datetime."""
|
|
||||||
super().__init__(
|
|
||||||
knx_module=knx_module,
|
|
||||||
device=XknxDateTimeDevice(
|
|
||||||
knx_module.xknx,
|
|
||||||
name=config[CONF_NAME],
|
|
||||||
localtime=False,
|
|
||||||
group_address=config[KNX_ADDRESS],
|
|
||||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
|
||||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
|
||||||
sync_state=config[CONF_SYNC_STATE],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
|
||||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
|
||||||
|
|
||||||
|
|
||||||
class KnxUiDateTime(_KNXDateTime, KnxUiEntity):
|
|
||||||
"""Representation of a KNX datetime configured from the UI."""
|
|
||||||
|
|
||||||
_device: XknxDateTimeDevice
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""Initialize KNX datetime."""
|
|
||||||
super().__init__(
|
|
||||||
knx_module=knx_module,
|
|
||||||
unique_id=unique_id,
|
|
||||||
entity_config=config[CONF_ENTITY],
|
|
||||||
)
|
|
||||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
|
||||||
self._device = XknxDateTimeDevice(
|
|
||||||
knx_module.xknx,
|
|
||||||
name=config[CONF_ENTITY][CONF_NAME],
|
|
||||||
localtime=False,
|
|
||||||
group_address=knx_conf.get_write(CONF_GA_DATETIME),
|
|
||||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_DATETIME),
|
|
||||||
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
|
|
||||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -77,11 +77,6 @@ class _KnxEntityBase(Entity):
|
|||||||
"""Store register state change callback and start device object."""
|
"""Store register state change callback and start device object."""
|
||||||
self._device.register_device_updated_cb(self.after_update_callback)
|
self._device.register_device_updated_cb(self.after_update_callback)
|
||||||
self._device.xknx.devices.async_add(self._device)
|
self._device.xknx.devices.async_add(self._device)
|
||||||
if uid := self.unique_id:
|
|
||||||
self._knx_module.add_to_group_address_entities(
|
|
||||||
group_addresses=self._device.group_addresses(),
|
|
||||||
identifier=(self.platform_data.domain, uid),
|
|
||||||
)
|
|
||||||
# super call needed to have methods of multi-inherited classes called
|
# super call needed to have methods of multi-inherited classes called
|
||||||
# eg. for restoring state (like _KNXSwitch)
|
# eg. for restoring state (like _KNXSwitch)
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
@@ -90,11 +85,6 @@ class _KnxEntityBase(Entity):
|
|||||||
"""Disconnect device object when removed."""
|
"""Disconnect device object when removed."""
|
||||||
self._device.unregister_device_updated_cb(self.after_update_callback)
|
self._device.unregister_device_updated_cb(self.after_update_callback)
|
||||||
self._device.xknx.devices.async_remove(self._device)
|
self._device.xknx.devices.async_remove(self._device)
|
||||||
if uid := self.unique_id:
|
|
||||||
self._knx_module.remove_from_group_address_entities(
|
|
||||||
group_addresses=self._device.group_addresses(),
|
|
||||||
identifier=(self.platform_data.domain, uid),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class KnxYamlEntity(_KnxEntityBase):
|
class KnxYamlEntity(_KnxEntityBase):
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ from .const import (
|
|||||||
from .device import KNXInterfaceDevice
|
from .device import KNXInterfaceDevice
|
||||||
from .expose import KNXExposeSensor, KNXExposeTime
|
from .expose import KNXExposeSensor, KNXExposeTime
|
||||||
from .project import KNXProject
|
from .project import KNXProject
|
||||||
from .repairs import data_secure_group_key_issue_dispatcher
|
|
||||||
from .storage.config_store import KNXConfigStore
|
from .storage.config_store import KNXConfigStore
|
||||||
from .telegrams import Telegrams
|
from .telegrams import Telegrams
|
||||||
|
|
||||||
@@ -108,12 +107,8 @@ class KNXModule:
|
|||||||
|
|
||||||
self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
|
self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
|
||||||
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
|
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
|
||||||
self.group_address_entities: dict[
|
|
||||||
DeviceGroupAddress, set[tuple[str, str]] # {(platform, unique_id),}
|
|
||||||
] = {}
|
|
||||||
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()
|
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()
|
||||||
|
|
||||||
self.entry.async_on_unload(data_secure_group_key_issue_dispatcher(self))
|
|
||||||
self.entry.async_on_unload(
|
self.entry.async_on_unload(
|
||||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||||
)
|
)
|
||||||
@@ -230,29 +225,6 @@ class KNXModule:
|
|||||||
threaded=True,
|
threaded=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_to_group_address_entities(
|
|
||||||
self,
|
|
||||||
group_addresses: set[DeviceGroupAddress],
|
|
||||||
identifier: tuple[str, str], # (platform, unique_id)
|
|
||||||
) -> None:
|
|
||||||
"""Register entity in group_address_entities map."""
|
|
||||||
for ga in group_addresses:
|
|
||||||
if ga not in self.group_address_entities:
|
|
||||||
self.group_address_entities[ga] = set()
|
|
||||||
self.group_address_entities[ga].add(identifier)
|
|
||||||
|
|
||||||
def remove_from_group_address_entities(
|
|
||||||
self,
|
|
||||||
group_addresses: set[DeviceGroupAddress],
|
|
||||||
identifier: tuple[str, str],
|
|
||||||
) -> None:
|
|
||||||
"""Unregister entity from group_address_entities map."""
|
|
||||||
for ga in group_addresses:
|
|
||||||
if ga in self.group_address_entities:
|
|
||||||
self.group_address_entities[ga].discard(identifier)
|
|
||||||
if not self.group_address_entities[ga]:
|
|
||||||
del self.group_address_entities[ga]
|
|
||||||
|
|
||||||
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
|
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
|
||||||
"""Call invoked after a KNX connection state change was received."""
|
"""Call invoked after a KNX connection state change was received."""
|
||||||
self.connected = state == XknxConnectionState.CONNECTED
|
self.connected = state == XknxConnectionState.CONNECTED
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"loggers": ["xknx", "xknxproject"],
|
"loggers": ["xknx", "xknxproject"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"xknx==3.12.0",
|
"xknx==3.11.0",
|
||||||
"xknxproject==3.8.2",
|
"xknxproject==3.8.2",
|
||||||
"knx-frontend==2025.10.31.195356"
|
"knx-frontend==2025.10.31.195356"
|
||||||
],
|
],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user