mirror of
https://github.com/home-assistant/core.git
synced 2025-10-09 11:49:27 +00:00
Compare commits
47 Commits
target_web
...
2025.10.0b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
77f897a768 | ||
![]() |
4f0a6ef9a1 | ||
![]() |
66c6b0f5fc | ||
![]() |
dd01243391 | ||
![]() |
66c17e250a | ||
![]() |
723902e233 | ||
![]() |
59fdb9f3b5 | ||
![]() |
d83502514a | ||
![]() |
08e81b2ba6 | ||
![]() |
1e808c965d | ||
![]() |
563b58c9aa | ||
![]() |
cf223880e8 | ||
![]() |
4058ca59ed | ||
![]() |
1386c01733 | ||
![]() |
46504947f7 | ||
![]() |
0a44682014 | ||
![]() |
06a57473a9 | ||
![]() |
fbed66ef1f | ||
![]() |
99a0380ec5 | ||
![]() |
68c51dc7aa | ||
![]() |
3d945b0fc5 | ||
![]() |
7b26a93d38 | ||
![]() |
1b2eab00be | ||
![]() |
750e849f09 | ||
![]() |
6aaddad56b | ||
![]() |
a5af974209 | ||
![]() |
09e45f6f54 | ||
![]() |
d857d8850c | ||
![]() |
ccc50f2412 | ||
![]() |
3905723900 | ||
![]() |
cee88473a2 | ||
![]() |
cdf613d3f8 | ||
![]() |
156a0f1a3d | ||
![]() |
cc2a5b43dd | ||
![]() |
731064f7e9 | ||
![]() |
2f75661c20 | ||
![]() |
be6f056f30 | ||
![]() |
79599e1284 | ||
![]() |
a255585ab6 | ||
![]() |
e9bde225fe | ||
![]() |
d9521ac2a0 | ||
![]() |
d8b24ccccd | ||
![]() |
b4417a76d5 | ||
![]() |
274f6eb54a | ||
![]() |
21a5aaf35c | ||
![]() |
05820a49d0 | ||
![]() |
17b12d29af |
64
.github/workflows/ci.yaml
vendored
64
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 8
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.11"
|
||||
HA_SHORT_VERSION: "2025.10"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -263,7 +263,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -349,7 +349,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -358,7 +358,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -398,7 +398,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -513,7 +513,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-
|
||||
- name: Check if apt cache exists
|
||||
id: cache-apt-check
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||
path: |
|
||||
@@ -570,7 +570,7 @@ jobs:
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -622,7 +622,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -651,7 +651,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -684,7 +684,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -711,7 +711,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||
uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
@@ -741,7 +741,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -784,7 +784,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -831,7 +831,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -883,7 +883,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -891,7 +891,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -935,7 +935,7 @@ jobs:
|
||||
name: Split tests for full run
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -967,7 +967,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1009,7 +1009,7 @@ jobs:
|
||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1042,7 +1042,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1156,7 +1156,7 @@ jobs:
|
||||
Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1189,7 +1189,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1310,7 +1310,7 @@ jobs:
|
||||
Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1345,7 +1345,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1485,7 +1485,7 @@ jobs:
|
||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1518,7 +1518,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -15,7 +14,7 @@ from airos.exceptions import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -25,11 +24,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.data_entry_flow import section
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
||||
from .coordinator import AirOS8
|
||||
@@ -60,45 +54,26 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
super().__init__()
|
||||
self.airos_device: AirOS8
|
||||
self.errors: dict[str, str] = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the manual input of host and credentials."""
|
||||
self.errors = {}
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
validated_info = await self._validate_and_get_device_info(user_input)
|
||||
if validated_info:
|
||||
return self.async_create_entry(
|
||||
title=validated_info["title"],
|
||||
data=validated_info["data"],
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=self.errors
|
||||
)
|
||||
|
||||
async def _validate_and_get_device_info(
|
||||
self, config_data: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
"""Validate user input with the device API."""
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
||||
verify_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
airos_device = AirOS8(
|
||||
host=config_data[CONF_HOST],
|
||||
username=config_data[CONF_USERNAME],
|
||||
password=config_data[CONF_PASSWORD],
|
||||
host=user_input[CONF_HOST],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||
use_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||
)
|
||||
try:
|
||||
await airos_device.login()
|
||||
@@ -108,59 +83,21 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
self.errors["base"] = "cannot_connect"
|
||||
errors["base"] = "cannot_connect"
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
self.errors["base"] = "invalid_auth"
|
||||
errors["base"] = "invalid_auth"
|
||||
except AirOSKeyDataMissingError:
|
||||
self.errors["base"] = "key_data_missing"
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception during credential validation")
|
||||
self.errors["base"] = "unknown"
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(airos_data.derived.mac)
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
else:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return {"title": airos_data.host.hostname, "data": config_data}
|
||||
|
||||
return None
|
||||
|
||||
async def async_step_reauth(
|
||||
self,
|
||||
user_input: Mapping[str, Any],
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauthentication upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm(user_input)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self,
|
||||
user_input: Mapping[str, Any],
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauthentication upon an API authentication error."""
|
||||
self.errors = {}
|
||||
|
||||
if user_input:
|
||||
validate_data = {**self._get_reauth_entry().data, **user_input}
|
||||
if await self._validate_and_get_device_info(config_data=validate_data):
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates=validate_data,
|
||||
return self.async_create_entry(
|
||||
title=airos_data.host.hostname, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=self.errors,
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
@@ -14,7 +14,7 @@ from airos.exceptions import (
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
@@ -47,9 +47,9 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except AirOSConnectionAuthenticationError as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.5.3"]
|
||||
"requirements": ["airos==0.5.1"]
|
||||
}
|
||||
|
@@ -2,14 +2,6 @@
|
||||
"config": {
|
||||
"flow_title": "Ubiquiti airOS device",
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::airos::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
@@ -42,9 +34,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -75,6 +75,13 @@ async def async_setup_entry(
|
||||
"detectionState",
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
|
||||
)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.2.7"]
|
||||
"requirements": ["aioamazondevices==6.2.6"]
|
||||
}
|
||||
|
@@ -551,7 +551,7 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
for domain, integration_info in integration_inputs.items()
|
||||
if (integration := integrations.get(domain)) is not None
|
||||
and integration.is_built_in
|
||||
and integration.manifest.get("integration_type") in ("device", "hub")
|
||||
and integration.integration_type in ("device", "hub")
|
||||
}
|
||||
|
||||
# Call integrations that implement the analytics platform
|
||||
|
@@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import frame
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.async_iterator import AsyncIteratorReader, AsyncIteratorWriter
|
||||
|
||||
from . import util
|
||||
from .agent import BackupAgent
|
||||
@@ -145,7 +144,7 @@ class DownloadBackupView(HomeAssistantView):
|
||||
return Response(status=HTTPStatus.NOT_FOUND)
|
||||
else:
|
||||
stream = await agent.async_download_backup(backup_id)
|
||||
reader = cast(IO[bytes], AsyncIteratorReader(hass.loop, stream))
|
||||
reader = cast(IO[bytes], util.AsyncIteratorReader(hass, stream))
|
||||
|
||||
worker_done_event = asyncio.Event()
|
||||
|
||||
@@ -153,7 +152,7 @@ class DownloadBackupView(HomeAssistantView):
|
||||
"""Call by the worker thread when it's done."""
|
||||
hass.loop.call_soon_threadsafe(worker_done_event.set)
|
||||
|
||||
stream = AsyncIteratorWriter(hass.loop)
|
||||
stream = util.AsyncIteratorWriter(hass)
|
||||
worker = threading.Thread(
|
||||
target=util.decrypt_backup,
|
||||
args=[backup, reader, stream, password, on_done, 0, []],
|
||||
|
@@ -38,7 +38,6 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
from homeassistant.util import dt as dt_util, json as json_util
|
||||
from homeassistant.util.async_iterator import AsyncIteratorReader
|
||||
|
||||
from . import util as backup_util
|
||||
from .agent import (
|
||||
@@ -73,6 +72,7 @@ from .models import (
|
||||
)
|
||||
from .store import BackupStore
|
||||
from .util import (
|
||||
AsyncIteratorReader,
|
||||
DecryptedBackupStreamer,
|
||||
EncryptedBackupStreamer,
|
||||
make_backup_dir,
|
||||
@@ -1525,7 +1525,7 @@ class BackupManager:
|
||||
reader = await self.hass.async_add_executor_job(open, path.as_posix(), "rb")
|
||||
else:
|
||||
backup_stream = await agent.async_download_backup(backup_id)
|
||||
reader = cast(IO[bytes], AsyncIteratorReader(self.hass.loop, backup_stream))
|
||||
reader = cast(IO[bytes], AsyncIteratorReader(self.hass, backup_stream))
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
validate_password_stream, reader, password
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
from concurrent.futures import CancelledError, Future
|
||||
import copy
|
||||
from dataclasses import dataclass, replace
|
||||
from io import BytesIO
|
||||
@@ -13,7 +14,7 @@ from pathlib import Path, PurePath
|
||||
from queue import SimpleQueue
|
||||
import tarfile
|
||||
import threading
|
||||
from typing import IO, Any, cast
|
||||
from typing import IO, Any, Self, cast
|
||||
|
||||
import aiohttp
|
||||
from securetar import SecureTarError, SecureTarFile, SecureTarReadError
|
||||
@@ -22,11 +23,6 @@ from homeassistant.backup_restore import password_to_key
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_iterator import (
|
||||
Abort,
|
||||
AsyncIteratorReader,
|
||||
AsyncIteratorWriter,
|
||||
)
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .const import BUF_SIZE, LOGGER
|
||||
@@ -63,6 +59,12 @@ class BackupEmpty(DecryptError):
|
||||
_message = "No tar files found in the backup."
|
||||
|
||||
|
||||
class AbortCipher(HomeAssistantError):
|
||||
"""Abort the cipher operation."""
|
||||
|
||||
_message = "Abort cipher operation."
|
||||
|
||||
|
||||
def make_backup_dir(path: Path) -> None:
|
||||
"""Create a backup directory if it does not exist."""
|
||||
path.mkdir(exist_ok=True)
|
||||
@@ -164,6 +166,106 @@ def validate_password(path: Path, password: str | None) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class AsyncIteratorReader:
|
||||
"""Wrap an AsyncIterator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, stream: AsyncIterator[bytes]) -> None:
|
||||
"""Initialize the wrapper."""
|
||||
self._aborted = False
|
||||
self._hass = hass
|
||||
self._stream = stream
|
||||
self._buffer: bytes | None = None
|
||||
self._next_future: Future[bytes | None] | None = None
|
||||
self._pos: int = 0
|
||||
|
||||
async def _next(self) -> bytes | None:
|
||||
"""Get the next chunk from the iterator."""
|
||||
return await anext(self._stream, None)
|
||||
|
||||
def abort(self) -> None:
|
||||
"""Abort the reader."""
|
||||
self._aborted = True
|
||||
if self._next_future is not None:
|
||||
self._next_future.cancel()
|
||||
|
||||
def read(self, n: int = -1, /) -> bytes:
|
||||
"""Read data from the iterator."""
|
||||
result = bytearray()
|
||||
while n < 0 or len(result) < n:
|
||||
if not self._buffer:
|
||||
self._next_future = asyncio.run_coroutine_threadsafe(
|
||||
self._next(), self._hass.loop
|
||||
)
|
||||
if self._aborted:
|
||||
self._next_future.cancel()
|
||||
raise AbortCipher
|
||||
try:
|
||||
self._buffer = self._next_future.result()
|
||||
except CancelledError as err:
|
||||
raise AbortCipher from err
|
||||
self._pos = 0
|
||||
if not self._buffer:
|
||||
# The stream is exhausted
|
||||
break
|
||||
chunk = self._buffer[self._pos : self._pos + n]
|
||||
result.extend(chunk)
|
||||
n -= len(chunk)
|
||||
self._pos += len(chunk)
|
||||
if self._pos == len(self._buffer):
|
||||
self._buffer = None
|
||||
return bytes(result)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the iterator."""
|
||||
|
||||
|
||||
class AsyncIteratorWriter:
|
||||
"""Wrap an AsyncIterator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the wrapper."""
|
||||
self._aborted = False
|
||||
self._hass = hass
|
||||
self._pos: int = 0
|
||||
self._queue: asyncio.Queue[bytes | None] = asyncio.Queue(maxsize=1)
|
||||
self._write_future: Future[bytes | None] | None = None
|
||||
|
||||
def __aiter__(self) -> Self:
|
||||
"""Return the iterator."""
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> bytes:
|
||||
"""Get the next chunk from the iterator."""
|
||||
if data := await self._queue.get():
|
||||
return data
|
||||
raise StopAsyncIteration
|
||||
|
||||
def abort(self) -> None:
|
||||
"""Abort the writer."""
|
||||
self._aborted = True
|
||||
if self._write_future is not None:
|
||||
self._write_future.cancel()
|
||||
|
||||
def tell(self) -> int:
|
||||
"""Return the current position in the iterator."""
|
||||
return self._pos
|
||||
|
||||
def write(self, s: bytes, /) -> int:
|
||||
"""Write data to the iterator."""
|
||||
self._write_future = asyncio.run_coroutine_threadsafe(
|
||||
self._queue.put(s), self._hass.loop
|
||||
)
|
||||
if self._aborted:
|
||||
self._write_future.cancel()
|
||||
raise AbortCipher
|
||||
try:
|
||||
self._write_future.result()
|
||||
except CancelledError as err:
|
||||
raise AbortCipher from err
|
||||
self._pos += len(s)
|
||||
return len(s)
|
||||
|
||||
|
||||
def validate_password_stream(
|
||||
input_stream: IO[bytes],
|
||||
password: str | None,
|
||||
@@ -240,7 +342,7 @@ def decrypt_backup(
|
||||
finally:
|
||||
# Write an empty chunk to signal the end of the stream
|
||||
output_stream.write(b"")
|
||||
except Abort:
|
||||
except AbortCipher:
|
||||
LOGGER.debug("Cipher operation aborted")
|
||||
finally:
|
||||
on_done(error)
|
||||
@@ -328,7 +430,7 @@ def encrypt_backup(
|
||||
finally:
|
||||
# Write an empty chunk to signal the end of the stream
|
||||
output_stream.write(b"")
|
||||
except Abort:
|
||||
except AbortCipher:
|
||||
LOGGER.debug("Cipher operation aborted")
|
||||
finally:
|
||||
on_done(error)
|
||||
@@ -455,8 +557,8 @@ class _CipherBackupStreamer:
|
||||
self._hass.loop.call_soon_threadsafe(worker_status.done.set)
|
||||
|
||||
stream = await self._open_stream()
|
||||
reader = AsyncIteratorReader(self._hass.loop, stream)
|
||||
writer = AsyncIteratorWriter(self._hass.loop)
|
||||
reader = AsyncIteratorReader(self._hass, stream)
|
||||
writer = AsyncIteratorWriter(self._hass)
|
||||
worker = threading.Thread(
|
||||
target=self._cipher_func,
|
||||
args=[
|
||||
|
@@ -3,12 +3,16 @@ beolink_allstandby:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
|
||||
beolink_expand:
|
||||
target:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
fields:
|
||||
all_discovered:
|
||||
required: false
|
||||
@@ -33,6 +37,8 @@ beolink_join:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
fields:
|
||||
jid_options:
|
||||
collapsed: false
|
||||
@@ -65,12 +71,16 @@ beolink_leave:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
|
||||
beolink_unexpand:
|
||||
target:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
fields:
|
||||
jid_options:
|
||||
collapsed: false
|
||||
|
@@ -315,7 +315,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.http.register_view(CalendarListView(component))
|
||||
hass.http.register_view(CalendarEventView(component))
|
||||
|
||||
frontend.async_register_built_in_panel(hass, "calendar", "calendar", "mdi:calendar")
|
||||
frontend.async_register_built_in_panel(
|
||||
hass, "calendar", "calendar", "hass:calendar"
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, handle_calendar_event_create)
|
||||
websocket_api.async_register_command(hass, handle_calendar_event_delete)
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.1.2"],
|
||||
"requirements": ["hass-nabucasa==1.1.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the config component."""
|
||||
frontend.async_register_built_in_panel(
|
||||
hass, "config", "config", "mdi:cog", require_admin=True
|
||||
hass, "config", "config", "hass:cog", require_admin=True
|
||||
)
|
||||
|
||||
for panel in SECTIONS:
|
||||
|
@@ -32,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
@@ -47,9 +46,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,))
|
||||
|
@@ -140,7 +140,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 4
|
||||
|
@@ -6,13 +6,12 @@ from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DOMAIN, CONF_OPTIONS
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionCheckerType,
|
||||
ConditionConfig,
|
||||
trace_condition_function,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -56,40 +55,19 @@ class DeviceAutomationConditionProtocol(Protocol):
|
||||
class DeviceCondition(Condition):
|
||||
"""Device condition."""
|
||||
|
||||
_hass: HomeAssistant
|
||||
_config: ConfigType
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate complete config."""
|
||||
complete_config = await async_validate_device_automation_config(
|
||||
hass,
|
||||
complete_config,
|
||||
cv.DEVICE_CONDITION_SCHEMA,
|
||||
DeviceAutomationType.CONDITION,
|
||||
)
|
||||
# Since we don't want to migrate device conditions to a new format
|
||||
# we just pass the entire config as options.
|
||||
complete_config[CONF_OPTIONS] = complete_config.copy()
|
||||
return complete_config
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Initialize condition."""
|
||||
self._config = config
|
||||
self._hass = hass
|
||||
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config.
|
||||
|
||||
This is here just to satisfy the abstract class interface. It is never called.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize condition."""
|
||||
self._hass = hass
|
||||
assert config.options is not None
|
||||
self._config = config.options
|
||||
"""Validate device condition config."""
|
||||
return await async_validate_device_automation_config(
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
|
||||
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import logging
|
||||
@@ -56,16 +55,16 @@ async def async_setup_entry(
|
||||
hostname = entry.data[CONF_HOSTNAME]
|
||||
name = entry.data[CONF_NAME]
|
||||
|
||||
nameserver_ipv4 = entry.options[CONF_RESOLVER]
|
||||
nameserver_ipv6 = entry.options[CONF_RESOLVER_IPV6]
|
||||
resolver_ipv4 = entry.options[CONF_RESOLVER]
|
||||
resolver_ipv6 = entry.options[CONF_RESOLVER_IPV6]
|
||||
port_ipv4 = entry.options[CONF_PORT]
|
||||
port_ipv6 = entry.options[CONF_PORT_IPV6]
|
||||
|
||||
entities = []
|
||||
if entry.data[CONF_IPV4]:
|
||||
entities.append(WanIpSensor(name, hostname, nameserver_ipv4, False, port_ipv4))
|
||||
entities.append(WanIpSensor(name, hostname, resolver_ipv4, False, port_ipv4))
|
||||
if entry.data[CONF_IPV6]:
|
||||
entities.append(WanIpSensor(name, hostname, nameserver_ipv6, True, port_ipv6))
|
||||
entities.append(WanIpSensor(name, hostname, resolver_ipv6, True, port_ipv6))
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
@@ -77,13 +76,11 @@ class WanIpSensor(SensorEntity):
|
||||
_attr_translation_key = "dnsip"
|
||||
_unrecorded_attributes = frozenset({"resolver", "querytype", "ip_addresses"})
|
||||
|
||||
resolver: aiodns.DNSResolver
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
hostname: str,
|
||||
nameserver: str,
|
||||
resolver: str,
|
||||
ipv6: bool,
|
||||
port: int,
|
||||
) -> None:
|
||||
@@ -91,12 +88,12 @@ class WanIpSensor(SensorEntity):
|
||||
self._attr_name = "IPv6" if ipv6 else None
|
||||
self._attr_unique_id = f"{hostname}_{ipv6}"
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.nameserver = nameserver
|
||||
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
|
||||
self.resolver.nameservers = [resolver]
|
||||
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
|
||||
self._retries = DEFAULT_RETRIES
|
||||
self._attr_extra_state_attributes = {
|
||||
"resolver": nameserver,
|
||||
"resolver": resolver,
|
||||
"querytype": self.querytype,
|
||||
}
|
||||
self._attr_device_info = DeviceInfo(
|
||||
@@ -106,26 +103,14 @@ class WanIpSensor(SensorEntity):
|
||||
model=aiodns.__version__,
|
||||
name=name,
|
||||
)
|
||||
self.create_dns_resolver()
|
||||
|
||||
def create_dns_resolver(self) -> None:
|
||||
"""Create the DNS resolver."""
|
||||
self.resolver = aiodns.DNSResolver(
|
||||
nameservers=[self.nameserver], tcp_port=self.port, udp_port=self.port
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the current DNS IP address for hostname."""
|
||||
if self.resolver._closed: # noqa: SLF001
|
||||
self.create_dns_resolver()
|
||||
response = None
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
response = await self.resolver.query(self.hostname, self.querytype)
|
||||
except TimeoutError:
|
||||
await self.resolver.close()
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
response = None
|
||||
|
||||
if response:
|
||||
sorted_ips = sort_ips(
|
||||
|
@@ -116,11 +116,7 @@ class EbusdData:
|
||||
try:
|
||||
_LOGGER.debug("Opening socket to ebusd %s", name)
|
||||
command_result = ebusdpy.write(self._address, self._circuit, name, value)
|
||||
if (
|
||||
command_result is not None
|
||||
and "done" not in command_result
|
||||
and "empty" not in command_result
|
||||
):
|
||||
if command_result is not None and "done" not in command_result:
|
||||
_LOGGER.warning("Write command failed: %s", name)
|
||||
except RuntimeError as err:
|
||||
_LOGGER.error(err)
|
||||
|
@@ -69,9 +69,7 @@ class EcovacsMap(
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def on_info(event: CachedMapInfoEvent) -> None:
|
||||
for map_obj in event.maps:
|
||||
if map_obj.using:
|
||||
self._attr_extra_state_attributes["map_name"] = map_obj.name
|
||||
self._attr_extra_state_attributes["map_name"] = event.name
|
||||
|
||||
async def on_changed(event: MapChangedEvent) -> None:
|
||||
self._attr_image_last_updated = event.when
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.0.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==14.0.0"]
|
||||
}
|
||||
|
@@ -2,4 +2,3 @@ raw_get_positions:
|
||||
target:
|
||||
entity:
|
||||
domain: vacuum
|
||||
integration: ecovacs
|
||||
|
@@ -3,15 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyephember2.pyephember2 import (
|
||||
EphEmber,
|
||||
ZoneMode,
|
||||
boiler_state,
|
||||
zone_current_temperature,
|
||||
zone_is_active,
|
||||
zone_is_hotwater,
|
||||
zone_mode,
|
||||
zone_name,
|
||||
@@ -54,15 +53,6 @@ EPH_TO_HA_STATE = {
|
||||
"OFF": HVACMode.OFF,
|
||||
}
|
||||
|
||||
|
||||
class EPHBoilerStates(IntEnum):
|
||||
"""Boiler states for a zone given by the api."""
|
||||
|
||||
FIXME = 0
|
||||
OFF = 1
|
||||
ON = 2
|
||||
|
||||
|
||||
HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()}
|
||||
|
||||
|
||||
@@ -133,7 +123,7 @@ class EphEmberThermostat(ClimateEntity):
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return current HVAC action."""
|
||||
if boiler_state(self._zone) == EPHBoilerStates.ON:
|
||||
if zone_is_active(self._zone):
|
||||
return HVACAction.HEATING
|
||||
|
||||
return HVACAction.IDLE
|
||||
|
@@ -10,6 +10,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Filter from a config entry."""
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@@ -17,3 +18,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Filter config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
@@ -246,7 +246,6 @@ class FilterConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -46,9 +46,6 @@ async def async_get_config_entry_diagnostics(
|
||||
}
|
||||
for _, device in avm_wrapper.devices.items()
|
||||
],
|
||||
"cpu_temperatures": await hass.async_add_executor_job(
|
||||
avm_wrapper.fritz_status.get_cpu_temperatures
|
||||
),
|
||||
"wan_link_properties": await avm_wrapper.async_get_wan_link_properties(),
|
||||
},
|
||||
}
|
||||
|
@@ -459,7 +459,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"developer-tools",
|
||||
require_admin=True,
|
||||
sidebar_title="developer_tools",
|
||||
sidebar_icon="mdi:hammer",
|
||||
sidebar_icon="hass:hammer",
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@@ -1,10 +1,8 @@
|
||||
load_url:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
target:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
url:
|
||||
example: "https://home-assistant.io"
|
||||
required: true
|
||||
@@ -12,12 +10,10 @@ load_url:
|
||||
text:
|
||||
|
||||
set_config:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
target:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
key:
|
||||
example: "motionSensitivity"
|
||||
required: true
|
||||
@@ -30,14 +26,12 @@ set_config:
|
||||
text:
|
||||
|
||||
start_application:
|
||||
target:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
application:
|
||||
example: "de.ozerov.fully"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
|
@@ -147,10 +147,6 @@
|
||||
"name": "Load URL",
|
||||
"description": "Loads a URL on Fully Kiosk Browser.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The target device for this action."
|
||||
},
|
||||
"url": {
|
||||
"name": "[%key:common::config_flow::data::url%]",
|
||||
"description": "URL to load."
|
||||
@@ -161,10 +157,6 @@
|
||||
"name": "Set configuration",
|
||||
"description": "Sets a configuration parameter on Fully Kiosk Browser.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "[%key:component::fully_kiosk::services::load_url::fields::device_id::name%]",
|
||||
"description": "[%key:component::fully_kiosk::services::load_url::fields::device_id::description%]"
|
||||
},
|
||||
"key": {
|
||||
"name": "Key",
|
||||
"description": "Configuration parameter to set."
|
||||
@@ -182,10 +174,6 @@
|
||||
"application": {
|
||||
"name": "Application",
|
||||
"description": "Package name of the application to start."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "[%key:component::fully_kiosk::services::load_url::fields::device_id::name%]",
|
||||
"description": "[%key:component::fully_kiosk::services::load_url::fields::device_id::description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -108,7 +108,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
||||
@@ -141,7 +140,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
@@ -150,6 +148,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
@@ -187,6 +186,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
|
@@ -96,7 +96,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -35,7 +35,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_HEATER: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the heater, but
|
||||
@@ -68,7 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
@@ -77,6 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
@@ -114,6 +113,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@@ -104,7 +104,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -1,5 +1,7 @@
|
||||
set_vacation:
|
||||
target:
|
||||
device:
|
||||
integration: google_mail
|
||||
entity:
|
||||
integration: google_mail
|
||||
fields:
|
||||
|
@@ -141,9 +141,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["group_type"],)
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
|
@@ -329,7 +329,6 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
@callback
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
|
@@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the history hooks."""
|
||||
hass.http.register_view(HistoryPeriodView())
|
||||
frontend.async_register_built_in_panel(hass, "history", "history", "mdi:chart-box")
|
||||
frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box")
|
||||
websocket_api.async_setup(hass)
|
||||
return True
|
||||
|
||||
|
@@ -65,7 +65,6 @@ async def async_setup_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we remove the config entry because
|
||||
@@ -87,6 +86,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@@ -130,3 +130,8 @@ async def async_unload_entry(
|
||||
) -> bool:
|
||||
"""Unload History stats config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
@@ -162,7 +162,6 @@ class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -22,6 +22,6 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohomeconnect==0.20.0"],
|
||||
"requirements": ["aiohomeconnect==0.19.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
@@ -32,12 +32,15 @@ set_location:
|
||||
stop:
|
||||
toggle:
|
||||
target:
|
||||
entity: {}
|
||||
|
||||
turn_on:
|
||||
target:
|
||||
entity: {}
|
||||
|
||||
turn_off:
|
||||
target:
|
||||
entity: {}
|
||||
|
||||
update_entity:
|
||||
fields:
|
||||
@@ -50,6 +53,8 @@ update_entity:
|
||||
reload_custom_templates:
|
||||
reload_config_entry:
|
||||
target:
|
||||
entity: {}
|
||||
device: {}
|
||||
fields:
|
||||
entry_id:
|
||||
advanced: true
|
||||
|
@@ -27,12 +27,6 @@
|
||||
"install_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]"
|
||||
@@ -75,10 +69,12 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -133,21 +129,14 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{model}",
|
||||
"step": {
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"pick_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
|
||||
@@ -169,10 +158,12 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -224,10 +215,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
@@ -61,13 +61,6 @@ class PickedFirmwareType(StrEnum):
|
||||
ZIGBEE = "zigbee"
|
||||
|
||||
|
||||
class ZigbeeFlowStrategy(StrEnum):
|
||||
"""Zigbee setup strategies that can be picked."""
|
||||
|
||||
ADVANCED = "advanced"
|
||||
RECOMMENDED = "recommended"
|
||||
|
||||
|
||||
class ZigbeeIntegration(StrEnum):
|
||||
"""Zigbee integrations that can be picked."""
|
||||
|
||||
@@ -80,7 +73,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
||||
_picked_firmware_type: PickedFirmwareType
|
||||
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Instantiate base flow."""
|
||||
@@ -403,14 +395,12 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
) -> ConfigFlowResult:
|
||||
"""Select recommended installation type."""
|
||||
self._zigbee_integration = ZigbeeIntegration.ZHA
|
||||
self._zigbee_flow_strategy = ZigbeeFlowStrategy.RECOMMENDED
|
||||
return await self._async_continue_picked_firmware()
|
||||
|
||||
async def async_step_zigbee_intent_custom(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Select custom installation type."""
|
||||
self._zigbee_flow_strategy = ZigbeeFlowStrategy.ADVANCED
|
||||
return await self.async_step_zigbee_integration()
|
||||
|
||||
async def async_step_zigbee_integration(
|
||||
@@ -531,7 +521,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"flow_control": "hardware",
|
||||
},
|
||||
"radio_type": "ezsp",
|
||||
"flow_strategy": self._zigbee_flow_strategy,
|
||||
},
|
||||
)
|
||||
return self._continue_zha_flow(result)
|
||||
|
@@ -23,16 +23,12 @@
|
||||
"description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration."
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "Configuring Thread"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "Updating adapter"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "Updating adapter"
|
||||
"title": "Installing OpenThread Border Router add-on",
|
||||
"description": "The OpenThread Border Router (OTBR) add-on is being installed."
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "Configuring Thread"
|
||||
"title": "Starting OpenThread Border Router add-on",
|
||||
"description": "The OpenThread Border Router (OTBR) add-on is now starting."
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "Failed to set up OpenThread Border Router",
|
||||
@@ -76,9 +72,7 @@
|
||||
"fw_install_failed": "{firmware_name} firmware failed to install, check Home Assistant logs for more information."
|
||||
},
|
||||
"progress": {
|
||||
"install_firmware": "Installing {firmware_name} firmware.\n\nDo not make any changes to your hardware or software until this finishes.",
|
||||
"install_otbr_addon": "Installing add-on",
|
||||
"start_otbr_addon": "Starting add-on"
|
||||
"install_firmware": "Please wait while {firmware_name} firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -27,12 +27,6 @@
|
||||
"install_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]"
|
||||
@@ -75,10 +69,12 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -133,10 +129,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
@@ -163,16 +158,12 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -224,10 +215,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
@@ -35,12 +35,6 @@
|
||||
"install_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]"
|
||||
@@ -98,10 +92,12 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -158,10 +154,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -145,11 +145,7 @@ class HueMotionSensor(HueBaseEntity, BinarySensorEntity):
|
||||
if not self.resource.enabled:
|
||||
# Force None (unknown) if the sensor is set to disabled in Hue
|
||||
return None
|
||||
if not (motion_feature := self.resource.motion):
|
||||
return None
|
||||
if motion_feature.motion_report is not None:
|
||||
return motion_feature.motion_report.motion
|
||||
return motion_feature.motion
|
||||
return self.resource.motion.value
|
||||
|
||||
|
||||
# pylint: disable-next=hass-enforce-class-module
|
||||
|
@@ -3,9 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from email.message import Message
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioimaplib import IMAP4_SSL, AioImapException, Response
|
||||
import voluptuous as vol
|
||||
@@ -35,7 +33,6 @@ from .coordinator import (
|
||||
ImapPollingDataUpdateCoordinator,
|
||||
ImapPushDataUpdateCoordinator,
|
||||
connect_to_server,
|
||||
get_parts,
|
||||
)
|
||||
from .errors import InvalidAuth, InvalidFolder
|
||||
|
||||
@@ -43,7 +40,6 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
CONF_ENTRY = "entry"
|
||||
CONF_SEEN = "seen"
|
||||
CONF_PART = "part"
|
||||
CONF_UID = "uid"
|
||||
CONF_TARGET_FOLDER = "target_folder"
|
||||
|
||||
@@ -68,11 +64,6 @@ SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend(
|
||||
)
|
||||
SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA
|
||||
SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA
|
||||
SERVICE_FETCH_PART_SCHEMA = _SERVICE_UID_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PART): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
type ImapConfigEntry = ConfigEntry[ImapDataUpdateCoordinator]
|
||||
|
||||
@@ -225,14 +216,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
raise_on_error(response, "fetch_failed")
|
||||
# Index 1 of of the response lines contains the bytearray with the message data
|
||||
message = ImapMessage(response.lines[1])
|
||||
await client.close()
|
||||
return {
|
||||
"text": message.text,
|
||||
"sender": message.sender,
|
||||
"subject": message.subject,
|
||||
"parts": get_parts(message.email_message),
|
||||
"uid": uid,
|
||||
}
|
||||
|
||||
@@ -244,73 +233,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
async def async_fetch_part(call: ServiceCall) -> ServiceResponse:
|
||||
"""Process fetch email part service and return content."""
|
||||
|
||||
@callback
|
||||
def get_message_part(message: Message, part_key: str) -> Message:
|
||||
part: Message | Any = message
|
||||
for index in part_key.split(","):
|
||||
sub_parts = part.get_payload()
|
||||
try:
|
||||
assert isinstance(sub_parts, list)
|
||||
part = sub_parts[int(index)]
|
||||
except (AssertionError, ValueError, IndexError) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_part_index",
|
||||
) from exc
|
||||
|
||||
return part
|
||||
|
||||
entry_id: str = call.data[CONF_ENTRY]
|
||||
uid: str = call.data[CONF_UID]
|
||||
part_key: str = call.data[CONF_PART]
|
||||
_LOGGER.debug(
|
||||
"Fetch part %s for message %s. Entry: %s",
|
||||
part_key,
|
||||
uid,
|
||||
entry_id,
|
||||
)
|
||||
client = await async_get_imap_client(hass, entry_id)
|
||||
try:
|
||||
response = await client.fetch(uid, "BODY.PEEK[]")
|
||||
except (TimeoutError, AioImapException) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="imap_server_fail",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
raise_on_error(response, "fetch_failed")
|
||||
# Index 1 of of the response lines contains the bytearray with the message data
|
||||
message = ImapMessage(response.lines[1])
|
||||
await client.close()
|
||||
part_data = get_message_part(message.email_message, part_key)
|
||||
part_data_content = part_data.get_payload(decode=False)
|
||||
try:
|
||||
assert isinstance(part_data_content, str)
|
||||
except AssertionError as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_part_index",
|
||||
) from exc
|
||||
return {
|
||||
"part_data": part_data_content,
|
||||
"content_type": part_data.get_content_type(),
|
||||
"content_transfer_encoding": part_data.get("Content-Transfer-Encoding"),
|
||||
"filename": part_data.get_filename(),
|
||||
"part": part_key,
|
||||
"uid": uid,
|
||||
}
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"fetch_part",
|
||||
async_fetch_part,
|
||||
SERVICE_FETCH_PART_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
CONF_VERIFY_SSL,
|
||||
CONTENT_TYPE_TEXT_PLAIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
@@ -209,28 +209,6 @@ class ImapMessage:
|
||||
return str(self.email_message.get_payload())
|
||||
|
||||
|
||||
@callback
|
||||
def get_parts(message: Message, prefix: str | None = None) -> dict[str, Any]:
|
||||
"""Return information about the parts of a multipart message."""
|
||||
parts: dict[str, Any] = {}
|
||||
if not message.is_multipart():
|
||||
return {}
|
||||
for index, part in enumerate(message.get_payload(), 0):
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(part, Message)
|
||||
key = f"{prefix},{index}" if prefix else f"{index}"
|
||||
if part.is_multipart():
|
||||
parts |= get_parts(part, key)
|
||||
continue
|
||||
parts[key] = {"content_type": part.get_content_type()}
|
||||
if filename := part.get_filename():
|
||||
parts[key]["filename"] = filename
|
||||
if content_transfer_encoding := part.get("Content-Transfer-Encoding"):
|
||||
parts[key]["content_transfer_encoding"] = content_transfer_encoding
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
||||
"""Base class for imap client."""
|
||||
|
||||
@@ -297,7 +275,6 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
||||
"sender": message.sender,
|
||||
"subject": message.subject,
|
||||
"uid": last_message_uid,
|
||||
"parts": get_parts(message.email_message),
|
||||
}
|
||||
data.update({key: getattr(message, key) for key in self._event_data_keys})
|
||||
if self.custom_event_template is not None:
|
||||
|
@@ -21,9 +21,6 @@
|
||||
},
|
||||
"fetch": {
|
||||
"service": "mdi:email-sync-outline"
|
||||
},
|
||||
"fetch_part": {
|
||||
"service": "mdi:email-sync-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -56,22 +56,3 @@ fetch:
|
||||
example: "12"
|
||||
selector:
|
||||
text:
|
||||
|
||||
fetch_part:
|
||||
fields:
|
||||
entry:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: "imap"
|
||||
uid:
|
||||
required: true
|
||||
example: "12"
|
||||
selector:
|
||||
text:
|
||||
|
||||
part:
|
||||
required: true
|
||||
example: "0,1"
|
||||
selector:
|
||||
text:
|
||||
|
@@ -84,9 +84,6 @@
|
||||
"imap_server_fail": {
|
||||
"message": "The IMAP server failed to connect: {error}."
|
||||
},
|
||||
"invalid_part_index": {
|
||||
"message": "Invalid part index."
|
||||
},
|
||||
"seen_failed": {
|
||||
"message": "Marking message as seen failed with \"{error}\"."
|
||||
}
|
||||
@@ -151,24 +148,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch_part": {
|
||||
"name": "Fetch message part",
|
||||
"description": "Fetches a message part or attachment from an email message.",
|
||||
"fields": {
|
||||
"entry": {
|
||||
"name": "[%key:component::imap::services::fetch::fields::entry::name%]",
|
||||
"description": "[%key:component::imap::services::fetch::fields::entry::description%]"
|
||||
},
|
||||
"uid": {
|
||||
"name": "[%key:component::imap::services::fetch::fields::uid::name%]",
|
||||
"description": "[%key:component::imap::services::fetch::fields::uid::description%]"
|
||||
},
|
||||
"part": {
|
||||
"name": "Part",
|
||||
"description": "The message part index."
|
||||
}
|
||||
}
|
||||
},
|
||||
"seen": {
|
||||
"name": "Mark message as seen",
|
||||
"description": "Marks an email as seen.",
|
||||
|
@@ -35,7 +35,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config_flow import ( # Loading the config flow file will register the flow
|
||||
@@ -221,19 +221,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Konnected platform."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_firmware",
|
||||
breaks_in_ha_version="2026.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_firmware",
|
||||
translation_placeholders={
|
||||
"kb_page_url": "https://support.konnected.io/migrating-from-konnected-legacy-home-assistant-integration-to-esphome",
|
||||
},
|
||||
)
|
||||
if (cfg := config.get(DOMAIN)) is None:
|
||||
cfg = {}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "konnected",
|
||||
"name": "Konnected.io (Legacy)",
|
||||
"name": "Konnected.io",
|
||||
"codeowners": ["@heythisisnate"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
|
@@ -105,11 +105,5 @@
|
||||
"abort": {
|
||||
"not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_firmware": {
|
||||
"title": "Konnected firmware is deprecated",
|
||||
"description": "Konnected's integration is deprecated and Konnected strongly recommends migrating to their ESPHome based firmware and integration by following the guide at {kb_page_url}. After this migration, make sure you don't have any Konnected YAML configuration left in your configuration.yaml file and remove this integration from Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import PRECISION_WHOLE, EntityCategory, UnitOfTime
|
||||
from homeassistant.const import PRECISION_WHOLE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -72,7 +72,6 @@ NUMBERS: tuple[LetPotNumberEntityDescription, ...] = (
|
||||
LetPotNumberEntityDescription(
|
||||
key="plant_days",
|
||||
translation_key="plant_days",
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
value_fn=lambda coordinator: coordinator.data.plant_days,
|
||||
set_value_fn=(
|
||||
lambda device_client, serial, value: device_client.set_plant_days(
|
||||
|
@@ -54,7 +54,8 @@
|
||||
"name": "Light brightness"
|
||||
},
|
||||
"plant_days": {
|
||||
"name": "Plants age"
|
||||
"name": "Plants age",
|
||||
"unit_of_measurement": "days"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
|
@@ -28,7 +28,7 @@ rules:
|
||||
docs-configuration-parameters:
|
||||
status: done
|
||||
comment: No options to configure
|
||||
docs-installation-parameters: done
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
|
@@ -22,6 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@@ -29,3 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Local file config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
@@ -65,7 +65,6 @@ class LocalFileConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -115,7 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_log_entry(hass, name, message, domain, entity_id, service.context)
|
||||
|
||||
frontend.async_register_built_in_panel(
|
||||
hass, "logbook", "logbook", "mdi:format-list-bulleted-type"
|
||||
hass, "logbook", "logbook", "hass:format-list-bulleted-type"
|
||||
)
|
||||
|
||||
recorder_conf = config.get(RECORDER_DOMAIN, {})
|
||||
|
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
DOMAIN = "lovelace"
|
||||
LOVELACE_DATA: HassKey[LovelaceData] = HassKey(DOMAIN)
|
||||
|
||||
DEFAULT_ICON = "mdi:view-dashboard"
|
||||
DEFAULT_ICON = "hass:view-dashboard"
|
||||
|
||||
MODE_YAML = "yaml"
|
||||
MODE_STORAGE = "storage"
|
||||
|
@@ -1,5 +1,7 @@
|
||||
set_hold_time:
|
||||
target:
|
||||
device:
|
||||
integration: lyric
|
||||
entity:
|
||||
integration: lyric
|
||||
domain: climate
|
||||
|
@@ -148,9 +148,6 @@
|
||||
},
|
||||
"evse_charging_switch": {
|
||||
"default": "mdi:ev-station"
|
||||
},
|
||||
"privacy_mode_button": {
|
||||
"default": "mdi:shield-lock"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -80,7 +80,9 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
sendvalue = int(value)
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
sendvalue = value_convert(value)
|
||||
await self.write_attribute(value=sendvalue)
|
||||
await self.write_attribute(
|
||||
value=sendvalue,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
@@ -435,35 +437,4 @@ DISCOVERY_SCHEMAS = [
|
||||
custom_clusters.InovelliCluster.Attributes.LEDIndicatorIntensityOn,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="DoorLockWrongCodeEntryLimit",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="wrong_code_entry_limit",
|
||||
native_max_value=255,
|
||||
native_min_value=1,
|
||||
native_step=1,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(clusters.DoorLock.Attributes.WrongCodeEntryLimit,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="DoorLockUserCodeTemporaryDisableTime",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="user_code_temporary_disable_time",
|
||||
native_max_value=255,
|
||||
native_min_value=1,
|
||||
native_step=1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(
|
||||
clusters.DoorLock.Attributes.UserCodeTemporaryDisableTime,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@@ -198,9 +198,6 @@
|
||||
"pump_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"user_code_temporary_disable_time": {
|
||||
"name": "User code temporary disable time"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
@@ -221,9 +218,6 @@
|
||||
},
|
||||
"valve_configuration_and_control_default_open_duration": {
|
||||
"name": "Default open duration"
|
||||
},
|
||||
"wrong_code_entry_limit": {
|
||||
"name": "Wrong code limit"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
@@ -519,9 +513,6 @@
|
||||
},
|
||||
"evse_charging_switch": {
|
||||
"name": "Enable charging"
|
||||
},
|
||||
"privacy_mode_button": {
|
||||
"name": "Privacy mode button"
|
||||
}
|
||||
},
|
||||
"vacuum": {
|
||||
|
@@ -263,18 +263,6 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
vendor_id=(4874,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SWITCH,
|
||||
entity_description=MatterNumericSwitchEntityDescription(
|
||||
key="DoorLockEnablePrivacyModeButton",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="privacy_mode_button",
|
||||
device_to_ha=bool,
|
||||
ha_to_device=int,
|
||||
),
|
||||
entity_class=MatterNumericSwitch,
|
||||
required_attributes=(clusters.DoorLock.Attributes.EnablePrivacyModeButton,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SWITCH,
|
||||
entity_description=MatterGenericCommandSwitchEntityDescription(
|
||||
|
@@ -9,18 +9,6 @@
|
||||
"url": "The remote MCP server URL for the SSE endpoint, for example http://example/sse"
|
||||
}
|
||||
},
|
||||
"credentials_choice": {
|
||||
"title": "Choose how to authenticate with the MCP server",
|
||||
"description": "You can either use existing credentials from another integration or set up new credentials.",
|
||||
"menu_options": {
|
||||
"new_credentials": "Set up new credentials",
|
||||
"pick_implementation": "Use existing credentials"
|
||||
},
|
||||
"menu_option_descriptions": {
|
||||
"new_credentials": "You will be guided through setting up a new OAuth Client ID and secret.",
|
||||
"pick_implementation": "You may use previously entered OAuth credentials."
|
||||
}
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]",
|
||||
"data": {
|
||||
@@ -39,21 +27,14 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"missing_capabilities": "The MCP server does not support a required capability (Tools)",
|
||||
"missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"reauth_account_mismatch": "The authenticated user does not match the MCP Server user that needed re-authentication.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,9 +7,8 @@ from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_PORT, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION
|
||||
from .utils import create_version
|
||||
@@ -26,21 +25,13 @@ REAUTH_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_API_TOKEN): str,
|
||||
}
|
||||
)
|
||||
DISCOVERY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_TOKEN): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Mealie config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
host: str | None = None
|
||||
verify_ssl: bool = True
|
||||
_hassio_discovery: dict[str, Any] | None = None
|
||||
|
||||
async def check_connection(
|
||||
self, api_token: str
|
||||
@@ -152,59 +143,3 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=USER_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_hassio(
|
||||
self, discovery_info: HassioServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Prepare configuration for a Mealie add-on.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
|
||||
self._hassio_discovery = discovery_info.config
|
||||
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
async def async_step_hassio_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm Supervisor discovery and prompt for API token."""
|
||||
if user_input is None:
|
||||
return await self._show_hassio_form()
|
||||
|
||||
assert self._hassio_discovery
|
||||
|
||||
self.host = (
|
||||
f"{self._hassio_discovery[CONF_HOST]}:{self._hassio_discovery[CONF_PORT]}"
|
||||
)
|
||||
self.verify_ssl = True
|
||||
|
||||
errors, user_id = await self.check_connection(
|
||||
user_input[CONF_API_TOKEN],
|
||||
)
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title="Mealie",
|
||||
data={
|
||||
CONF_HOST: self.host,
|
||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
||||
CONF_VERIFY_SSL: self.verify_ssl,
|
||||
},
|
||||
)
|
||||
return await self._show_hassio_form(errors)
|
||||
|
||||
async def _show_hassio_form(
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the Hass.io confirmation form to the user."""
|
||||
assert self._hassio_discovery
|
||||
return self.async_show_form(
|
||||
step_id="hassio_confirm",
|
||||
data_schema=DISCOVERY_SCHEMA,
|
||||
description_placeholders={"addon": self._hassio_discovery["addon"]},
|
||||
errors=errors or {},
|
||||
)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiomealie==0.11.0"]
|
||||
"requirements": ["aiomealie==0.10.2"]
|
||||
}
|
||||
|
@@ -39,14 +39,8 @@ rules:
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration will only discover a Mealie addon that is local, not on the network.
|
||||
discovery:
|
||||
status: done
|
||||
comment: |
|
||||
The integration will discover a Mealie addon posting a discovery message.
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: todo
|
||||
|
@@ -39,16 +39,6 @@
|
||||
"api_token": "[%key:component::mealie::common::data_description_api_token%]",
|
||||
"verify_ssl": "[%key:component::mealie::common::data_description_verify_ssl%]"
|
||||
}
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "Mealie via Home Assistant add-on",
|
||||
"description": "Do you want to configure Home Assistant to connect to the Mealie instance provided by the add-on: {addon}?",
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "[%key:component::mealie::common::data_description_api_token%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -60,7 +50,6 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_account": "You have to use the same account that was used to configure the integration."
|
||||
|
@@ -25,7 +25,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_browse_media)
|
||||
websocket_api.async_register_command(hass, websocket_resolve_media)
|
||||
frontend.async_register_built_in_panel(
|
||||
hass, "media-browser", "media_browser", "mdi:play-box-multiple"
|
||||
hass, "media-browser", "media_browser", "hass:play-box-multiple"
|
||||
)
|
||||
|
||||
|
||||
|
@@ -39,7 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_INDOOR_HUMIDITY: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidity
|
||||
@@ -80,7 +79,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, temp_sensor: data["entity_id"]},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
return async_sensor_updated
|
||||
|
||||
@@ -91,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@@ -100,6 +99,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
|
@@ -100,7 +100,6 @@ class MoldIndicatorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
@@ -1,7 +1,8 @@
|
||||
set_text_overlay:
|
||||
target:
|
||||
device:
|
||||
integration: motioneye
|
||||
entity:
|
||||
domain: camera
|
||||
integration: motioneye
|
||||
fields:
|
||||
left_text:
|
||||
@@ -47,8 +48,9 @@ set_text_overlay:
|
||||
|
||||
action:
|
||||
target:
|
||||
device:
|
||||
integration: motioneye
|
||||
entity:
|
||||
domain: camera
|
||||
integration: motioneye
|
||||
fields:
|
||||
action:
|
||||
@@ -86,6 +88,7 @@ action:
|
||||
|
||||
snapshot:
|
||||
target:
|
||||
entity:
|
||||
domain: camera
|
||||
device:
|
||||
integration: motioneye
|
||||
entity:
|
||||
integration: motioneye
|
||||
|
@@ -1235,7 +1235,6 @@
|
||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||
"pm4": "[%key:component::sensor::entity_component::pm4::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||
@@ -1243,7 +1242,6 @@
|
||||
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
||||
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
||||
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
||||
"reactive_energy": "[%key:component::sensor::entity_component::reactive_energy::name%]",
|
||||
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
||||
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
||||
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["nibe==2.19.0"]
|
||||
"requirements": ["nibe==2.18.0"]
|
||||
}
|
||||
|
@@ -13,6 +13,6 @@ NMAP_TRACKED_DEVICES: Final = "nmap_tracked_devices"
|
||||
# Interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL: Final = "home_interval"
|
||||
CONF_OPTIONS: Final = "scan_options"
|
||||
DEFAULT_OPTIONS: Final = "-n -sn -PR -T4 --min-rate 10 --host-timeout 5s"
|
||||
DEFAULT_OPTIONS: Final = "-F -T4 --min-rate 10 --host-timeout 5s"
|
||||
|
||||
TRACKER_SCAN_INTERVAL: Final = 120
|
||||
|
@@ -124,7 +124,7 @@ class NumberDeviceClass(StrEnum):
|
||||
CO = "carbon_monoxide"
|
||||
"""Carbon Monoxide gas concentration.
|
||||
|
||||
Unit of measurement: `ppm` (parts per million)
|
||||
Unit of measurement: `ppm` (parts per million), mg/m³
|
||||
"""
|
||||
|
||||
CO2 = "carbon_dioxide"
|
||||
@@ -475,7 +475,10 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
|
||||
NumberDeviceClass.BATTERY: {PERCENTAGE},
|
||||
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
|
||||
NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION},
|
||||
NumberDeviceClass.CO: {
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
|
||||
NumberDeviceClass.CONDUCTIVITY: set(UnitOfConductivity),
|
||||
NumberDeviceClass.CURRENT: set(UnitOfElectricCurrent),
|
||||
|
@@ -112,9 +112,6 @@
|
||||
"pm1": {
|
||||
"name": "[%key:component::sensor::entity_component::pm1::name%]"
|
||||
},
|
||||
"pm4": {
|
||||
"name": "[%key:component::sensor::entity_component::pm4::name%]"
|
||||
},
|
||||
"pm10": {
|
||||
"name": "[%key:component::sensor::entity_component::pm10::name%]"
|
||||
},
|
||||
|
@@ -52,10 +52,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> boo
|
||||
|
||||
try:
|
||||
info = await async_interview(host)
|
||||
except TimeoutError as exc:
|
||||
raise ConfigEntryNotReady(f"Timed out interviewing: {host}") from exc
|
||||
except OSError as exc:
|
||||
raise ConfigEntryNotReady(f"Unexpected exception interviewing: {host}") from exc
|
||||
raise ConfigEntryNotReady(f"Unable to connect to: {host}") from exc
|
||||
if info is None:
|
||||
raise ConfigEntryNotReady(f"Unable to connect to: {host}")
|
||||
|
||||
manager = ReceiverManager(hass, entry, info)
|
||||
|
||||
|
@@ -109,16 +109,18 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Config flow manual: %s", host)
|
||||
try:
|
||||
info = await async_interview(host)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning("Timed out interviewing: %s", host)
|
||||
errors["base"] = "cannot_connect"
|
||||
except OSError:
|
||||
_LOGGER.exception("Unexpected exception interviewing: %s", host)
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if info is None:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
self._receiver_info = info
|
||||
|
||||
await self.async_set_unique_id(info.identifier, raise_on_progress=False)
|
||||
await self.async_set_unique_id(
|
||||
info.identifier, raise_on_progress=False
|
||||
)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
else:
|
||||
@@ -212,13 +214,14 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
info = await async_interview(host)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning("Timed out interviewing: %s", host)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except OSError:
|
||||
_LOGGER.exception("Unexpected exception interviewing: %s", host)
|
||||
_LOGGER.exception("Unexpected exception interviewing host %s", host)
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
if info is None:
|
||||
_LOGGER.debug("SSDP eiscp is None: %s", host)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(info.identifier)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: info.host})
|
||||
|
||||
|
@@ -341,12 +341,12 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
|
||||
def process_update(self, message: status.Known) -> None:
|
||||
"""Process update."""
|
||||
match message:
|
||||
case status.Power(param=status.Power.Param.ON):
|
||||
case status.Power(status.Power.Param.ON):
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
case status.Power(param=status.Power.Param.STANDBY):
|
||||
case status.Power(status.Power.Param.STANDBY):
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
|
||||
case status.Volume(param=volume):
|
||||
case status.Volume(volume):
|
||||
if not self._supports_volume:
|
||||
self._attr_supported_features |= SUPPORTED_FEATURES_VOLUME
|
||||
self._supports_volume = True
|
||||
@@ -356,10 +356,10 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
|
||||
)
|
||||
self._attr_volume_level = min(1, volume_level)
|
||||
|
||||
case status.Muting(param=muting):
|
||||
case status.Muting(muting):
|
||||
self._attr_is_volume_muted = bool(muting == status.Muting.Param.ON)
|
||||
|
||||
case status.InputSource(param=source):
|
||||
case status.InputSource(source):
|
||||
if source in self._source_mapping:
|
||||
self._attr_source = self._source_mapping[source]
|
||||
else:
|
||||
@@ -373,7 +373,7 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
|
||||
|
||||
self._query_av_info_delayed()
|
||||
|
||||
case status.ListeningMode(param=sound_mode):
|
||||
case status.ListeningMode(sound_mode):
|
||||
if not self._supports_sound_mode:
|
||||
self._attr_supported_features |= (
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
@@ -393,13 +393,13 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
|
||||
|
||||
self._query_av_info_delayed()
|
||||
|
||||
case status.HDMIOutput(param=hdmi_output):
|
||||
case status.HDMIOutput(hdmi_output):
|
||||
self._attr_extra_state_attributes[ATTR_VIDEO_OUT] = (
|
||||
self._hdmi_output_mapping[hdmi_output]
|
||||
)
|
||||
self._query_av_info_delayed()
|
||||
|
||||
case status.TunerPreset(param=preset):
|
||||
case status.TunerPreset(preset):
|
||||
self._attr_extra_state_attributes[ATTR_PRESET] = preset
|
||||
|
||||
case status.AudioInformation():
|
||||
@@ -427,11 +427,11 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
|
||||
case status.FLDisplay():
|
||||
self._query_av_info_delayed()
|
||||
|
||||
case status.NotAvailable(kind=Kind.AUDIO_INFORMATION):
|
||||
case status.NotAvailable(Kind.AUDIO_INFORMATION):
|
||||
# Not available right now, but still supported
|
||||
self._supports_audio_info = True
|
||||
|
||||
case status.NotAvailable(kind=Kind.VIDEO_INFORMATION):
|
||||
case status.NotAvailable(Kind.VIDEO_INFORMATION):
|
||||
# Not available right now, but still supported
|
||||
self._supports_video_info = True
|
||||
|
||||
|
@@ -124,10 +124,13 @@ class ReceiverManager:
|
||||
self.callbacks.clear()
|
||||
|
||||
|
||||
async def async_interview(host: str) -> ReceiverInfo:
|
||||
async def async_interview(host: str) -> ReceiverInfo | None:
|
||||
"""Interview the receiver."""
|
||||
info: ReceiverInfo | None = None
|
||||
with contextlib.suppress(asyncio.TimeoutError):
|
||||
async with asyncio.timeout(DEVICE_INTERVIEW_TIMEOUT):
|
||||
return await aioonkyo.interview(host)
|
||||
info = await aioonkyo.interview(host)
|
||||
return info
|
||||
|
||||
|
||||
async def async_discover(hass: HomeAssistant) -> Iterable[ReceiverInfo]:
|
||||
|
@@ -5,14 +5,7 @@ from __future__ import annotations
|
||||
from pyportainer import Portainer
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_API_TOKEN,
|
||||
CONF_HOST,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
@@ -27,8 +20,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) ->
|
||||
"""Set up Portainer from a config entry."""
|
||||
|
||||
client = Portainer(
|
||||
api_url=entry.data[CONF_URL],
|
||||
api_key=entry.data[CONF_API_TOKEN],
|
||||
api_url=entry.data[CONF_HOST],
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
session=async_create_clientsession(
|
||||
hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL]
|
||||
),
|
||||
@@ -46,15 +39,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) ->
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
if entry.version < 2:
|
||||
data = dict(entry.data)
|
||||
data[CONF_URL] = data.pop(CONF_HOST)
|
||||
data[CONF_API_TOKEN] = data.pop(CONF_API_KEY)
|
||||
hass.config_entries.async_update_entry(entry=entry, data=data, version=2)
|
||||
|
||||
return True
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -15,7 +14,7 @@ from pyportainer import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -25,8 +24,8 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): str,
|
||||
vol.Required(CONF_API_TOKEN): str,
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
)
|
||||
@@ -36,11 +35,9 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
client = Portainer(
|
||||
api_url=data[CONF_URL],
|
||||
api_key=data[CONF_API_TOKEN],
|
||||
session=async_get_clientsession(
|
||||
hass=hass, verify_ssl=data.get(CONF_VERIFY_SSL, True)
|
||||
),
|
||||
api_url=data[CONF_HOST],
|
||||
api_key=data[CONF_API_KEY],
|
||||
session=async_get_clientsession(hass=hass, verify_ssl=data[CONF_VERIFY_SSL]),
|
||||
)
|
||||
try:
|
||||
await client.get_endpoints()
|
||||
@@ -51,21 +48,19 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
except PortainerTimeoutError as err:
|
||||
raise PortainerTimeout from err
|
||||
|
||||
_LOGGER.debug("Connected to Portainer API: %s", data[CONF_URL])
|
||||
_LOGGER.debug("Connected to Portainer API: %s", data[CONF_HOST])
|
||||
|
||||
|
||||
class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Portainer."""
|
||||
|
||||
VERSION = 2
|
||||
|
||||
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_URL: user_input[CONF_URL]})
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
try:
|
||||
await _validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
@@ -78,58 +73,16 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
|
||||
await self.async_set_unique_id(user_input[CONF_API_KEY])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_URL], data=user_input
|
||||
title=user_input[CONF_HOST], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth when Portainer API authentication fails."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth: ask for new API token and validate."""
|
||||
errors: dict[str, str] = {}
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
if user_input is not None:
|
||||
try:
|
||||
await _validate_input(
|
||||
self.hass,
|
||||
data={
|
||||
**reauth_entry.data,
|
||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
||||
},
|
||||
)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except PortainerTimeout:
|
||||
errors["base"] = "timeout_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
@@ -16,9 +16,9 @@ from pyportainer.models.docker import DockerContainer
|
||||
from pyportainer.models.portainer import Endpoint
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -66,7 +66,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
try:
|
||||
await self.portainer.get_endpoints()
|
||||
except PortainerAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
@@ -87,14 +87,14 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
async def _async_update_data(self) -> dict[int, PortainerCoordinatorData]:
|
||||
"""Fetch data from Portainer API."""
|
||||
_LOGGER.debug(
|
||||
"Fetching data from Portainer API: %s", self.config_entry.data[CONF_URL]
|
||||
"Fetching data from Portainer API: %s", self.config_entry.data[CONF_HOST]
|
||||
)
|
||||
|
||||
try:
|
||||
endpoints = await self.portainer.get_endpoints()
|
||||
except PortainerAuthenticationError as err:
|
||||
_LOGGER.error("Authentication error: %s", repr(err))
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
@@ -121,7 +121,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
) from err
|
||||
except PortainerAuthenticationError as err:
|
||||
_LOGGER.exception("Authentication error")
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
|
@@ -3,25 +3,16 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "The URL, including the port, of your Portainer instance",
|
||||
"api_token": "The API access token for authenticating with Portainer",
|
||||
"host": "The host/URL, including the port, of your Portainer instance",
|
||||
"api_key": "The API key for authenticating with Portainer",
|
||||
"verify_ssl": "Whether to verify SSL certificates. Disable only if you have a self-signed certificate"
|
||||
},
|
||||
"description": "You can create an access token in the Portainer UI. Go to **My account > Access tokens** and select **Add access token**"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The new API access token for authenticating with Portainer"
|
||||
},
|
||||
"description": "The access token for your Portainer instance needs to be re-authenticated. You can create a new access token in the Portainer UI. Go to **My account > Access tokens** and select **Add access token**"
|
||||
"description": "You can create an API key in the Portainer UI. Go to **My account > API keys** and select **Add API key**"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -31,8 +22,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"device": {
|
||||
|
@@ -9,9 +9,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["entity_type"],)
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
|
@@ -184,7 +184,6 @@ class RandomConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
@callback
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
|
@@ -114,7 +114,6 @@
|
||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||
"pm4": "[%key:component::sensor::entity_component::pm4::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||
|
@@ -46,6 +46,7 @@ from homeassistant.util.unit_conversion import (
|
||||
AreaConverter,
|
||||
BaseUnitConverter,
|
||||
BloodGlucoseConcentrationConverter,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
ConductivityConverter,
|
||||
DataRateConverter,
|
||||
DistanceConverter,
|
||||
@@ -204,6 +205,10 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
|
||||
**dict.fromkeys(
|
||||
MassVolumeConcentrationConverter.VALID_UNITS, MassVolumeConcentrationConverter
|
||||
),
|
||||
**dict.fromkeys(
|
||||
CarbonMonoxideConcentrationConverter.VALID_UNITS,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
**dict.fromkeys(ConductivityConverter.VALID_UNITS, ConductivityConverter),
|
||||
**dict.fromkeys(DataRateConverter.VALID_UNITS, DataRateConverter),
|
||||
**dict.fromkeys(DistanceConverter.VALID_UNITS, DistanceConverter),
|
||||
|
@@ -19,6 +19,7 @@ from homeassistant.util.unit_conversion import (
|
||||
ApparentPowerConverter,
|
||||
AreaConverter,
|
||||
BloodGlucoseConcentrationConverter,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
ConductivityConverter,
|
||||
DataRateConverter,
|
||||
DistanceConverter,
|
||||
@@ -66,6 +67,9 @@ UNIT_SCHEMA = vol.Schema(
|
||||
vol.Optional("blood_glucose_concentration"): vol.In(
|
||||
BloodGlucoseConcentrationConverter.VALID_UNITS
|
||||
),
|
||||
vol.Optional("carbon_monoxide"): vol.In(
|
||||
CarbonMonoxideConcentrationConverter.VALID_UNITS
|
||||
),
|
||||
vol.Optional("concentration"): vol.In(
|
||||
MassVolumeConcentrationConverter.VALID_UNITS
|
||||
),
|
||||
|
@@ -74,28 +74,21 @@ BINARY_PUSH_SENSORS = (
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key=PERSON_DETECTION_TYPE,
|
||||
cmd_id=[33, 600, 696],
|
||||
cmd_id=[33, 600],
|
||||
translation_key="person",
|
||||
value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE),
|
||||
supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE),
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key=VEHICLE_DETECTION_TYPE,
|
||||
cmd_id=[33, 600, 696],
|
||||
cmd_id=[33, 600],
|
||||
translation_key="vehicle",
|
||||
value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE),
|
||||
supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE),
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key="non-motor_vehicle",
|
||||
cmd_id=[600, 696],
|
||||
translation_key="non-motor_vehicle",
|
||||
value=lambda api, ch: api.ai_detected(ch, "non-motor vehicle"),
|
||||
supported=lambda api, ch: api.supported(ch, "ai_non-motor vehicle"),
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key=PET_DETECTION_TYPE,
|
||||
cmd_id=[33, 600, 696],
|
||||
cmd_id=[33, 600],
|
||||
translation_key="pet",
|
||||
value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE),
|
||||
supported=lambda api, ch: (
|
||||
@@ -105,14 +98,14 @@ BINARY_PUSH_SENSORS = (
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key=PET_DETECTION_TYPE,
|
||||
cmd_id=[33, 600, 696],
|
||||
cmd_id=[33, 600],
|
||||
translation_key="animal",
|
||||
value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE),
|
||||
supported=lambda api, ch: api.supported(ch, "ai_animal"),
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key=PACKAGE_DETECTION_TYPE,
|
||||
cmd_id=[33, 600, 696],
|
||||
cmd_id=[33, 600],
|
||||
translation_key="package",
|
||||
value=lambda api, ch: api.ai_detected(ch, PACKAGE_DETECTION_TYPE),
|
||||
supported=lambda api, ch: api.ai_supported(ch, PACKAGE_DETECTION_TYPE),
|
||||
@@ -127,7 +120,7 @@ BINARY_PUSH_SENSORS = (
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key="cry",
|
||||
cmd_id=[33],
|
||||
cmd_id=[33, 600],
|
||||
translation_key="cry",
|
||||
value=lambda api, ch: api.ai_detected(ch, "cry"),
|
||||
supported=lambda api, ch: api.ai_supported(ch, "cry"),
|
||||
|
@@ -13,12 +13,6 @@
|
||||
"on": "mdi:car"
|
||||
}
|
||||
},
|
||||
"non-motor_vehicle": {
|
||||
"default": "mdi:motorbike-off",
|
||||
"state": {
|
||||
"on": "mdi:motorbike"
|
||||
}
|
||||
},
|
||||
"pet": {
|
||||
"default": "mdi:dog-side-off",
|
||||
"state": {
|
||||
@@ -229,9 +223,6 @@
|
||||
"ai_vehicle_sensitivity": {
|
||||
"default": "mdi:car"
|
||||
},
|
||||
"ai_non_motor_vehicle_sensitivity": {
|
||||
"default": "mdi:bicycle"
|
||||
},
|
||||
"ai_package_sensitivity": {
|
||||
"default": "mdi:gift-outline"
|
||||
},
|
||||
@@ -268,9 +259,6 @@
|
||||
"ai_vehicle_delay": {
|
||||
"default": "mdi:car"
|
||||
},
|
||||
"ai_non_motor_vehicle_delay": {
|
||||
"default": "mdi:bicycle"
|
||||
},
|
||||
"ai_package_delay": {
|
||||
"default": "mdi:gift-outline"
|
||||
},
|
||||
@@ -468,15 +456,6 @@
|
||||
},
|
||||
"sd_storage": {
|
||||
"default": "mdi:micro-sd"
|
||||
},
|
||||
"person_type": {
|
||||
"default": "mdi:account"
|
||||
},
|
||||
"vehicle_type": {
|
||||
"default": "mdi:car"
|
||||
},
|
||||
"animal_type": {
|
||||
"default": "mdi:paw"
|
||||
}
|
||||
},
|
||||
"siren": {
|
||||
|
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.16.0"]
|
||||
"requirements": ["reolink-aio==0.15.2"]
|
||||
}
|
||||
|
@@ -255,23 +255,6 @@ NUMBER_ENTITIES = (
|
||||
value=lambda api, ch: api.ai_sensitivity(ch, "vehicle"),
|
||||
method=lambda api, ch, value: api.set_ai_sensitivity(ch, int(value), "vehicle"),
|
||||
),
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_non_motor_vehicle_sensitivity",
|
||||
cmd_key="GetAiAlarm",
|
||||
translation_key="ai_non_motor_vehicle_sensitivity",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
supported=lambda api, ch: (
|
||||
api.supported(ch, "ai_sensitivity")
|
||||
and api.supported(ch, "ai_non-motor vehicle")
|
||||
),
|
||||
value=lambda api, ch: api.ai_sensitivity(ch, "non-motor vehicle"),
|
||||
method=lambda api, ch, value: (
|
||||
api.set_ai_sensitivity(ch, int(value), "non-motor vehicle")
|
||||
),
|
||||
),
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_package_sensititvity",
|
||||
cmd_key="GetAiAlarm",
|
||||
@@ -362,25 +345,6 @@ NUMBER_ENTITIES = (
|
||||
value=lambda api, ch: api.ai_delay(ch, "people"),
|
||||
method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "people"),
|
||||
),
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_non_motor_vehicle_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
translation_key="ai_non_motor_vehicle_delay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
entity_registry_enabled_default=False,
|
||||
native_step=1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_min_value=0,
|
||||
native_max_value=8,
|
||||
supported=lambda api, ch: (
|
||||
api.supported(ch, "ai_delay") and api.supported(ch, "ai_non-motor vehicle")
|
||||
),
|
||||
value=lambda api, ch: api.ai_delay(ch, "non-motor vehicle"),
|
||||
method=lambda api, ch, value: (
|
||||
api.set_ai_delay(ch, int(value), "non-motor vehicle")
|
||||
),
|
||||
),
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ai_vehicle_delay",
|
||||
cmd_key="GetAiAlarm",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user