mirror of
https://github.com/home-assistant/core.git
synced 2025-08-09 23:55:07 +02:00
Merge branch 'dev' into pglab
This commit is contained in:
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -323,7 +323,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.4
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.4.0
|
uses: sigstore/cosign-installer@v3.5.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -92,8 +92,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.4
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: >-
|
run: |
|
||||||
echo "key=venv-${{ env.CACHE_VERSION }}-${{
|
# Include HA_SHORT_VERSION to force the immediate creation
|
||||||
|
# of a new uv cache entry after a version bump.
|
||||||
|
echo "key=venv-${{ env.CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-${{
|
||||||
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
|
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
|
||||||
hashFiles('requirements.txt') }}-${{
|
hashFiles('requirements.txt') }}-${{
|
||||||
hashFiles('requirements_all.txt') }}-${{
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
@@ -1104,7 +1106,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@v4.3.0
|
uses: codecov/codecov-action@v4.3.1
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@@ -1238,7 +1240,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@v4.3.0
|
uses: codecov/codecov-action@v4.3.1
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@@ -211,7 +211,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_old-cython.txt"
|
requirements: "requirements_old-cython.txt"
|
||||||
@@ -226,7 +226,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtaa"
|
requirements: "requirements_all.txtaa"
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtab"
|
requirements: "requirements_all.txtab"
|
||||||
@@ -254,7 +254,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtac"
|
requirements: "requirements_all.txtac"
|
||||||
|
@@ -244,6 +244,7 @@ homeassistant.components.image.*
|
|||||||
homeassistant.components.image_processing.*
|
homeassistant.components.image_processing.*
|
||||||
homeassistant.components.image_upload.*
|
homeassistant.components.image_upload.*
|
||||||
homeassistant.components.imap.*
|
homeassistant.components.imap.*
|
||||||
|
homeassistant.components.imgw_pib.*
|
||||||
homeassistant.components.input_button.*
|
homeassistant.components.input_button.*
|
||||||
homeassistant.components.input_select.*
|
homeassistant.components.input_select.*
|
||||||
homeassistant.components.input_text.*
|
homeassistant.components.input_text.*
|
||||||
|
10
CODEOWNERS
10
CODEOWNERS
@@ -550,14 +550,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/group/ @home-assistant/core
|
/tests/components/group/ @home-assistant/core
|
||||||
/homeassistant/components/guardian/ @bachya
|
/homeassistant/components/guardian/ @bachya
|
||||||
/tests/components/guardian/ @bachya
|
/tests/components/guardian/ @bachya
|
||||||
/homeassistant/components/habitica/ @ASMfreaK @leikoilja
|
/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
|
||||||
/tests/components/habitica/ @ASMfreaK @leikoilja
|
/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
|
||||||
/homeassistant/components/hardkernel/ @home-assistant/core
|
/homeassistant/components/hardkernel/ @home-assistant/core
|
||||||
/tests/components/hardkernel/ @home-assistant/core
|
/tests/components/hardkernel/ @home-assistant/core
|
||||||
/homeassistant/components/hardware/ @home-assistant/core
|
/homeassistant/components/hardware/ @home-assistant/core
|
||||||
/tests/components/hardware/ @home-assistant/core
|
/tests/components/hardware/ @home-assistant/core
|
||||||
/homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
/homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||||
/tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||||
/tests/components/hassio/ @home-assistant/supervisor
|
/tests/components/hassio/ @home-assistant/supervisor
|
||||||
/homeassistant/components/hdmi_cec/ @inytar
|
/homeassistant/components/hdmi_cec/ @inytar
|
||||||
@@ -650,6 +650,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/image_upload/ @home-assistant/core
|
/tests/components/image_upload/ @home-assistant/core
|
||||||
/homeassistant/components/imap/ @jbouwh
|
/homeassistant/components/imap/ @jbouwh
|
||||||
/tests/components/imap/ @jbouwh
|
/tests/components/imap/ @jbouwh
|
||||||
|
/homeassistant/components/imgw_pib/ @bieniu
|
||||||
|
/tests/components/imgw_pib/ @bieniu
|
||||||
/homeassistant/components/improv_ble/ @emontnemery
|
/homeassistant/components/improv_ble/ @emontnemery
|
||||||
/tests/components/improv_ble/ @emontnemery
|
/tests/components/improv_ble/ @emontnemery
|
||||||
/homeassistant/components/incomfort/ @zxdavb
|
/homeassistant/components/incomfort/ @zxdavb
|
||||||
|
@@ -12,7 +12,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.1.35
|
RUN pip3 install uv==0.1.39
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@@ -7,6 +7,8 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
|||||||
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||||
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||||
|
|
||||||
|
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|
||||||
|
|
||||||
|screenshot-states|
|
|screenshot-states|
|
||||||
|
|
||||||
Featured integrations
|
Featured integrations
|
||||||
@@ -25,4 +27,4 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
|
|||||||
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
||||||
:target: https://demo.home-assistant.io
|
:target: https://demo.home-assistant.io
|
||||||
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
||||||
:target: https://home-assistant.io/integrations/
|
:target: https://home-assistant.io/integrations/
|
||||||
|
@@ -90,7 +90,11 @@ from .helpers.system_info import async_get_system_info
|
|||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
from .setup import (
|
from .setup import (
|
||||||
BASE_PLATFORMS,
|
BASE_PLATFORMS,
|
||||||
DATA_SETUP_STARTED,
|
# _setup_started is marked as protected to make it clear
|
||||||
|
# that it is not part of the public API and should not be used
|
||||||
|
# by integrations. It is only used for internal tracking of
|
||||||
|
# which integrations are being set up.
|
||||||
|
_setup_started,
|
||||||
async_get_setup_timings,
|
async_get_setup_timings,
|
||||||
async_notify_setup_error,
|
async_notify_setup_error,
|
||||||
async_set_domains_to_be_loaded,
|
async_set_domains_to_be_loaded,
|
||||||
@@ -731,7 +735,7 @@ async def async_setup_multi_components(
|
|||||||
# to wait to be imported, and the sooner we can get the base platforms
|
# to wait to be imported, and the sooner we can get the base platforms
|
||||||
# loaded the sooner we can start loading the rest of the integrations.
|
# loaded the sooner we can start loading the rest of the integrations.
|
||||||
futures = {
|
futures = {
|
||||||
domain: hass.async_create_task(
|
domain: hass.async_create_task_internal(
|
||||||
async_setup_component(hass, domain, config),
|
async_setup_component(hass, domain, config),
|
||||||
f"setup component {domain}",
|
f"setup component {domain}",
|
||||||
eager_start=True,
|
eager_start=True,
|
||||||
@@ -913,9 +917,7 @@ async def _async_set_up_integrations(
|
|||||||
hass: core.HomeAssistant, config: dict[str, Any]
|
hass: core.HomeAssistant, config: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up all the integrations."""
|
"""Set up all the integrations."""
|
||||||
setup_started: dict[tuple[str, str | None], float] = {}
|
watcher = _WatchPendingSetups(hass, _setup_started(hass))
|
||||||
hass.data[DATA_SETUP_STARTED] = setup_started
|
|
||||||
watcher = _WatchPendingSetups(hass, setup_started)
|
|
||||||
watcher.async_start()
|
watcher.async_start()
|
||||||
|
|
||||||
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
||||||
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
|||||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@@ -43,6 +43,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||||
|
AdGuardConfigEntry = ConfigEntry["AdGuardData"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -53,7 +54,7 @@ class AdGuardData:
|
|||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||||
"""Set up AdGuard Home from a config entry."""
|
"""Set up AdGuard Home from a config entry."""
|
||||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||||
adguard = AdGuardHome(
|
adguard = AdGuardHome(
|
||||||
@@ -71,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
except AdGuardHomeConnectionError as exception:
|
except AdGuardHomeConnectionError as exception:
|
||||||
raise ConfigEntryNotReady from exception
|
raise ConfigEntryNotReady from exception
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AdGuardData(adguard, version)
|
entry.runtime_data = AdGuardData(adguard, version)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@@ -116,17 +117,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||||
"""Unload AdGuard Home config entry."""
|
"""Unload AdGuard Home config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok:
|
loaded_entries = [
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
entry
|
||||||
if not hass.data[DOMAIN]:
|
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||||
|
if entry.state == ConfigEntryState.LOADED
|
||||||
|
]
|
||||||
|
if len(loaded_entries) == 1:
|
||||||
|
# This is the last loaded instance of AdGuard, deregister any services
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
|
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
||||||
del hass.data[DOMAIN]
|
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@@ -4,11 +4,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from adguardhome import AdGuardHomeError
|
from adguardhome import AdGuardHomeError
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
|
from homeassistant.config_entries import SOURCE_HASSIO
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import AdGuardData
|
from . import AdGuardConfigEntry, AdGuardData
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class AdGuardHomeEntity(Entity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: AdGuardData,
|
data: AdGuardData,
|
||||||
entry: ConfigEntry,
|
entry: AdGuardConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the AdGuard Home entity."""
|
"""Initialize the AdGuard Home entity."""
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
|
@@ -10,12 +10,11 @@ from typing import Any
|
|||||||
from adguardhome import AdGuardHome
|
from adguardhome import AdGuardHome
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import PERCENTAGE, UnitOfTime
|
from homeassistant.const import PERCENTAGE, UnitOfTime
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AdGuardData
|
from . import AdGuardConfigEntry, AdGuardData
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import AdGuardHomeEntity
|
from .entity import AdGuardHomeEntity
|
||||||
|
|
||||||
@@ -85,11 +84,11 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: AdGuardConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdGuard Home sensor based on a config entry."""
|
"""Set up AdGuard Home sensor based on a config entry."""
|
||||||
data: AdGuardData = hass.data[DOMAIN][entry.entry_id]
|
data = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[AdGuardHomeSensor(data, entry, description) for description in SENSORS],
|
[AdGuardHomeSensor(data, entry, description) for description in SENSORS],
|
||||||
@@ -105,7 +104,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: AdGuardData,
|
data: AdGuardData,
|
||||||
entry: ConfigEntry,
|
entry: AdGuardConfigEntry,
|
||||||
description: AdGuardHomeEntityDescription,
|
description: AdGuardHomeEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize AdGuard Home sensor."""
|
"""Initialize AdGuard Home sensor."""
|
||||||
|
@@ -10,11 +10,10 @@ from typing import Any
|
|||||||
from adguardhome import AdGuardHome, AdGuardHomeError
|
from adguardhome import AdGuardHome, AdGuardHomeError
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AdGuardData
|
from . import AdGuardConfigEntry, AdGuardData
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
from .entity import AdGuardHomeEntity
|
from .entity import AdGuardHomeEntity
|
||||||
|
|
||||||
@@ -79,11 +78,11 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: AdGuardConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdGuard Home switch based on a config entry."""
|
"""Set up AdGuard Home switch based on a config entry."""
|
||||||
data: AdGuardData = hass.data[DOMAIN][entry.entry_id]
|
data = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES],
|
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES],
|
||||||
@@ -99,7 +98,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: AdGuardData,
|
data: AdGuardData,
|
||||||
entry: ConfigEntry,
|
entry: AdGuardConfigEntry,
|
||||||
description: AdGuardHomeSwitchEntityDescription,
|
description: AdGuardHomeSwitchEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize AdGuard Home switch."""
|
"""Initialize AdGuard Home switch."""
|
||||||
|
@@ -18,6 +18,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||||||
from homeassistant.helpers.typing import ConfigType, StateType
|
from homeassistant.helpers.typing import ConfigType, StateType
|
||||||
|
|
||||||
from . import group as group_pre_import # noqa: F401
|
from . import group as group_pre_import # noqa: F401
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -33,8 +34,6 @@ ATTR_PM_10: Final = "particulate_matter_10"
|
|||||||
ATTR_PM_2_5: Final = "particulate_matter_2_5"
|
ATTR_PM_2_5: Final = "particulate_matter_2_5"
|
||||||
ATTR_SO2: Final = "sulphur_dioxide"
|
ATTR_SO2: Final = "sulphur_dioxide"
|
||||||
|
|
||||||
DOMAIN: Final = "air_quality"
|
|
||||||
|
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
|
|
||||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||||
|
5
homeassistant/components/air_quality/const.py
Normal file
5
homeassistant/components/air_quality/const.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for the air_quality entity platform."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DOMAIN: Final = "air_quality"
|
@@ -7,10 +7,12 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.components.group import GroupIntegrationRegistry
|
from homeassistant.components.group import GroupIntegrationRegistry
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_describe_on_off_states(
|
def async_describe_on_off_states(
|
||||||
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
|
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Describe group on off states."""
|
"""Describe group on off states."""
|
||||||
registry.exclude_domain()
|
registry.exclude_domain(DOMAIN)
|
||||||
|
@@ -55,7 +55,7 @@ def set_update_interval(instances_count: int, requests_remaining: int) -> timede
|
|||||||
return interval
|
return interval
|
||||||
|
|
||||||
|
|
||||||
class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
|
||||||
"""Define an object to hold Airly data."""
|
"""Define an object to hold Airly data."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@@ -225,7 +225,7 @@ class AirthingsSensor(
|
|||||||
manufacturer=airthings_device.manufacturer,
|
manufacturer=airthings_device.manufacturer,
|
||||||
hw_version=airthings_device.hw_version,
|
hw_version=airthings_device.hw_version,
|
||||||
sw_version=airthings_device.sw_version,
|
sw_version=airthings_device.sw_version,
|
||||||
model=airthings_device.model.name,
|
model=airthings_device.model.product_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -10,9 +10,12 @@ from homeassistant.const import (
|
|||||||
STATE_ALARM_ARMED_VACATION,
|
STATE_ALARM_ARMED_VACATION,
|
||||||
STATE_ALARM_TRIGGERED,
|
STATE_ALARM_TRIGGERED,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.components.group import GroupIntegrationRegistry
|
from homeassistant.components.group import GroupIntegrationRegistry
|
||||||
|
|
||||||
@@ -23,7 +26,9 @@ def async_describe_on_off_states(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Describe group on off states."""
|
"""Describe group on off states."""
|
||||||
registry.on_off_states(
|
registry.on_off_states(
|
||||||
|
DOMAIN,
|
||||||
{
|
{
|
||||||
|
STATE_ON,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
@@ -31,5 +36,6 @@ def async_describe_on_off_states(
|
|||||||
STATE_ALARM_ARMED_VACATION,
|
STATE_ALARM_ARMED_VACATION,
|
||||||
STATE_ALARM_TRIGGERED,
|
STATE_ALARM_TRIGGERED,
|
||||||
},
|
},
|
||||||
|
STATE_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
)
|
)
|
||||||
|
@@ -15,10 +15,11 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN
|
from .const import CONF_TRACKED_INTEGRATIONS
|
||||||
from .coordinator import HomeassistantAnalyticsDataUpdateCoordinator
|
from .coordinator import HomeassistantAnalyticsDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
AnalyticsInsightsConfigEntry = ConfigEntry["AnalyticsInsightsData"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -29,7 +30,9 @@ class AnalyticsInsightsData:
|
|||||||
names: dict[str, str]
|
names: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Set up Homeassistant Analytics from a config entry."""
|
"""Set up Homeassistant Analytics from a config entry."""
|
||||||
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data[DOMAIN] = AnalyticsInsightsData(coordinator=coordinator, names=names)
|
entry.runtime_data = AnalyticsInsightsData(coordinator=coordinator, names=names)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
@@ -57,14 +60,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data.pop(DOMAIN)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def update_listener(
|
||||||
|
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Handle options update."""
|
"""Handle options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from python_homeassistant_analytics import (
|
from python_homeassistant_analytics import (
|
||||||
CustomIntegration,
|
CustomIntegration,
|
||||||
@@ -12,7 +13,6 @@ from python_homeassistant_analytics import (
|
|||||||
HomeassistantAnalyticsNotModifiedError,
|
HomeassistantAnalyticsNotModifiedError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@@ -23,6 +23,9 @@ from .const import (
|
|||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import AnalyticsInsightsConfigEntry
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AnalyticsData:
|
class AnalyticsData:
|
||||||
@@ -35,7 +38,7 @@ class AnalyticsData:
|
|||||||
class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[AnalyticsData]):
|
class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[AnalyticsData]):
|
||||||
"""A Homeassistant Analytics Data Update Coordinator."""
|
"""A Homeassistant Analytics Data Update Coordinator."""
|
||||||
|
|
||||||
config_entry: ConfigEntry
|
config_entry: AnalyticsInsightsConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, client: HomeassistantAnalyticsClient
|
self, hass: HomeAssistant, client: HomeassistantAnalyticsClient
|
||||||
|
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
@@ -18,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AnalyticsInsightsData
|
from . import AnalyticsInsightsConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AnalyticsData, HomeassistantAnalyticsDataUpdateCoordinator
|
from .coordinator import AnalyticsData, HomeassistantAnalyticsDataUpdateCoordinator
|
||||||
|
|
||||||
@@ -60,12 +59,12 @@ def get_custom_integration_entity_description(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: AnalyticsInsightsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entries."""
|
"""Initialize the entries."""
|
||||||
|
|
||||||
analytics_data: AnalyticsInsightsData = hass.data[DOMAIN]
|
analytics_data = entry.runtime_data
|
||||||
coordinator: HomeassistantAnalyticsDataUpdateCoordinator = (
|
coordinator: HomeassistantAnalyticsDataUpdateCoordinator = (
|
||||||
analytics_data.coordinator
|
analytics_data.coordinator
|
||||||
)
|
)
|
||||||
|
@@ -42,6 +42,7 @@ UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
AsekoBinarySensorEntityDescription(
|
AsekoBinarySensorEntityDescription(
|
||||||
key="has_error",
|
key="has_error",
|
||||||
|
translation_key="error",
|
||||||
value_fn=lambda unit: unit.has_error,
|
value_fn=lambda unit: unit.has_error,
|
||||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||||
),
|
),
|
||||||
@@ -78,7 +79,6 @@ class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
|||||||
"""Initialize the unit binary sensor."""
|
"""Initialize the unit binary sensor."""
|
||||||
super().__init__(unit, coordinator)
|
super().__init__(unit, coordinator)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_name = f"{self._device_name} {entity_description.name}"
|
|
||||||
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
|
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -66,10 +66,12 @@ _ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]
|
|||||||
|
|
||||||
def handle_errors_and_zip(
|
def handle_errors_and_zip(
|
||||||
exceptions: type[Exception] | tuple[type[Exception], ...], keys: list[str] | None
|
exceptions: type[Exception] | tuple[type[Exception], ...], keys: list[str] | None
|
||||||
) -> Callable[[_FuncType], _ReturnFuncType]:
|
) -> Callable[[_FuncType[_AsusWrtBridgeT]], _ReturnFuncType[_AsusWrtBridgeT]]:
|
||||||
"""Run library methods and zip results or manage exceptions."""
|
"""Run library methods and zip results or manage exceptions."""
|
||||||
|
|
||||||
def _handle_errors_and_zip(func: _FuncType) -> _ReturnFuncType:
|
def _handle_errors_and_zip(
|
||||||
|
func: _FuncType[_AsusWrtBridgeT],
|
||||||
|
) -> _ReturnFuncType[_AsusWrtBridgeT]:
|
||||||
"""Run library methods and zip results or manage exceptions."""
|
"""Run library methods and zip results or manage exceptions."""
|
||||||
|
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
|
@@ -28,5 +28,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==3.0.1", "yalexs-ble==2.4.2"]
|
"requirements": ["yalexs==3.1.0", "yalexs-ble==2.4.2"]
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,9 @@ class AugustSubscriberMixin:
|
|||||||
@callback
|
@callback
|
||||||
def _async_scheduled_refresh(self, now: datetime) -> None:
|
def _async_scheduled_refresh(self, now: datetime) -> None:
|
||||||
"""Call the refresh method."""
|
"""Call the refresh method."""
|
||||||
self._hass.async_create_task(self._async_refresh(now), eager_start=True)
|
self._hass.async_create_background_task(
|
||||||
|
self._async_refresh(now), name=f"{self} schedule refresh", eager_start=True
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
|
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
|
||||||
|
@@ -363,9 +363,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
|||||||
def is_volume_muted(self) -> bool | None:
|
def is_volume_muted(self) -> bool | None:
|
||||||
"""Boolean if volume is currently muted."""
|
"""Boolean if volume is currently muted."""
|
||||||
if self._volume.muted and self._volume.muted.muted:
|
if self._volume.muted and self._volume.muted.muted:
|
||||||
# The any return here is side effect of pydantic v2 compatibility
|
return self._volume.muted.muted
|
||||||
# This will be fixed in the future.
|
|
||||||
return self._volume.muted.muted # type: ignore[no-any-return]
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -152,6 +152,7 @@ async def _async_start_adapter_discovery(
|
|||||||
cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
||||||
immediate=False,
|
immediate=False,
|
||||||
function=_async_rediscover_adapters,
|
function=_async_rediscover_adapters,
|
||||||
|
background=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"bleak==0.21.1",
|
"bleak==0.21.1",
|
||||||
"bleak-retry-connector==3.5.0",
|
"bleak-retry-connector==3.5.0",
|
||||||
"bluetooth-adapters==0.19.0",
|
"bluetooth-adapters==0.19.1",
|
||||||
"bluetooth-auto-recovery==1.4.2",
|
"bluetooth-auto-recovery==1.4.2",
|
||||||
"bluetooth-data-tools==1.19.0",
|
"bluetooth-data-tools==1.19.0",
|
||||||
"dbus-fast==2.21.1",
|
"dbus-fast==2.21.1",
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["bimmer_connected"],
|
"loggers": ["bimmer_connected"],
|
||||||
"requirements": ["bimmer-connected[china]==0.14.6"]
|
"requirements": ["bimmer-connected[china]==0.15.2"]
|
||||||
}
|
}
|
||||||
|
@@ -128,7 +128,9 @@ class BondEntity(Entity):
|
|||||||
_FALLBACK_SCAN_INTERVAL,
|
_FALLBACK_SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self.hass.async_create_task(self._async_update(), eager_start=True)
|
self.hass.async_create_background_task(
|
||||||
|
self._async_update(), f"{DOMAIN} {self.name} update", eager_start=True
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_update(self) -> None:
|
async def _async_update(self) -> None:
|
||||||
"""Fetch via the API."""
|
"""Fetch via the API."""
|
||||||
|
@@ -325,16 +325,24 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
|
|
||||||
# Convert the supported features to ClimateEntityFeature.
|
# Convert the supported features to ClimateEntityFeature.
|
||||||
# Remove this compatibility shim in 2025.1 or later.
|
# Remove this compatibility shim in 2025.1 or later.
|
||||||
_supported_features = super().__getattribute__(__name)
|
_supported_features: ClimateEntityFeature = super().__getattribute__(
|
||||||
|
"supported_features"
|
||||||
|
)
|
||||||
|
_mod_supported_features: ClimateEntityFeature = super().__getattribute__(
|
||||||
|
"_ClimateEntity__mod_supported_features"
|
||||||
|
)
|
||||||
if type(_supported_features) is int: # noqa: E721
|
if type(_supported_features) is int: # noqa: E721
|
||||||
new_features = ClimateEntityFeature(_supported_features)
|
_features = ClimateEntityFeature(_supported_features)
|
||||||
self._report_deprecated_supported_features_values(new_features)
|
self._report_deprecated_supported_features_values(_features)
|
||||||
|
else:
|
||||||
|
_features = _supported_features
|
||||||
|
|
||||||
|
if not _mod_supported_features:
|
||||||
|
return _features
|
||||||
|
|
||||||
# Add automatically calculated ClimateEntityFeature.TURN_OFF/TURN_ON to
|
# Add automatically calculated ClimateEntityFeature.TURN_OFF/TURN_ON to
|
||||||
# supported features and return it
|
# supported features and return it
|
||||||
return _supported_features | super().__getattribute__(
|
return _features | _mod_supported_features
|
||||||
"_ClimateEntity__mod_supported_features"
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def add_to_platform_start(
|
def add_to_platform_start(
|
||||||
@@ -375,7 +383,8 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
# Return if integration has migrated already
|
# Return if integration has migrated already
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.supported_features & ClimateEntityFeature.TURN_OFF and (
|
supported_features = self.supported_features
|
||||||
|
if not supported_features & ClimateEntityFeature.TURN_OFF and (
|
||||||
type(self).async_turn_off is not ClimateEntity.async_turn_off
|
type(self).async_turn_off is not ClimateEntity.async_turn_off
|
||||||
or type(self).turn_off is not ClimateEntity.turn_off
|
or type(self).turn_off is not ClimateEntity.turn_off
|
||||||
):
|
):
|
||||||
@@ -385,7 +394,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
ClimateEntityFeature.TURN_OFF
|
ClimateEntityFeature.TURN_OFF
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.supported_features & ClimateEntityFeature.TURN_ON and (
|
if not supported_features & ClimateEntityFeature.TURN_ON and (
|
||||||
type(self).async_turn_on is not ClimateEntity.async_turn_on
|
type(self).async_turn_on is not ClimateEntity.async_turn_on
|
||||||
or type(self).turn_on is not ClimateEntity.turn_on
|
or type(self).turn_on is not ClimateEntity.turn_on
|
||||||
):
|
):
|
||||||
@@ -398,7 +407,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
if (modes := self.hvac_modes) and len(modes) >= 2 and HVACMode.OFF in modes:
|
if (modes := self.hvac_modes) and len(modes) >= 2 and HVACMode.OFF in modes:
|
||||||
# turn_on/off implicitly supported by including more modes than 1 and one of these
|
# turn_on/off implicitly supported by including more modes than 1 and one of these
|
||||||
# are HVACMode.OFF
|
# are HVACMode.OFF
|
||||||
_modes = [_mode for _mode in self.hvac_modes if _mode is not None]
|
_modes = [_mode for _mode in modes if _mode is not None]
|
||||||
_report_turn_on_off(", ".join(_modes or []), "turn_on/turn_off")
|
_report_turn_on_off(", ".join(_modes or []), "turn_on/turn_off")
|
||||||
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
|
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
|
||||||
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.const import STATE_OFF
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import HVAC_MODES, HVACMode
|
from .const import DOMAIN, HVACMode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.components.group import GroupIntegrationRegistry
|
from homeassistant.components.group import GroupIntegrationRegistry
|
||||||
@@ -17,6 +17,15 @@ def async_describe_on_off_states(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Describe group on off states."""
|
"""Describe group on off states."""
|
||||||
registry.on_off_states(
|
registry.on_off_states(
|
||||||
set(HVAC_MODES) - {HVACMode.OFF},
|
DOMAIN,
|
||||||
|
{
|
||||||
|
STATE_ON,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.COOL,
|
||||||
|
HVACMode.HEAT_COOL,
|
||||||
|
HVACMode.AUTO,
|
||||||
|
HVACMode.FAN_ONLY,
|
||||||
|
},
|
||||||
|
STATE_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
)
|
)
|
||||||
|
@@ -365,13 +365,16 @@ class CloudPreferences:
|
|||||||
@property
|
@property
|
||||||
def strict_connection(self) -> http.const.StrictConnectionMode:
|
def strict_connection(self) -> http.const.StrictConnectionMode:
|
||||||
"""Return the strict connection mode."""
|
"""Return the strict connection mode."""
|
||||||
mode = self._prefs.get(
|
mode = self._prefs.get(PREF_STRICT_CONNECTION)
|
||||||
PREF_STRICT_CONNECTION, http.const.StrictConnectionMode.DISABLED
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(mode, http.const.StrictConnectionMode):
|
if mode is None:
|
||||||
|
# Set to default value
|
||||||
|
# We store None in the store as the default value to detect if the user has changed the
|
||||||
|
# value or not.
|
||||||
|
mode = http.const.StrictConnectionMode.DISABLED
|
||||||
|
elif not isinstance(mode, http.const.StrictConnectionMode):
|
||||||
mode = http.const.StrictConnectionMode(mode)
|
mode = http.const.StrictConnectionMode(mode)
|
||||||
return mode # type: ignore[no-any-return]
|
return mode
|
||||||
|
|
||||||
async def get_cloud_user(self) -> str:
|
async def get_cloud_user(self) -> str:
|
||||||
"""Return ID of Home Assistant Cloud system user."""
|
"""Return ID of Home Assistant Cloud system user."""
|
||||||
@@ -430,5 +433,5 @@ class CloudPreferences:
|
|||||||
PREF_REMOTE_DOMAIN: None,
|
PREF_REMOTE_DOMAIN: None,
|
||||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE: True,
|
PREF_REMOTE_ALLOW_REMOTE_ENABLE: True,
|
||||||
PREF_USERNAME: username,
|
PREF_USERNAME: username,
|
||||||
PREF_STRICT_CONNECTION: http.const.StrictConnectionMode.DISABLED,
|
PREF_STRICT_CONNECTION: None,
|
||||||
}
|
}
|
||||||
|
@@ -191,12 +191,12 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if await self._switch(self._command_on) and not self._command_state:
|
if await self._switch(self._command_on) and not self._command_state:
|
||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
self.async_schedule_update_ha_state()
|
self.async_write_ha_state()
|
||||||
await self._update_entity_state()
|
await self._update_entity_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
if await self._switch(self._command_off) and not self._command_state:
|
if await self._switch(self._command_off) and not self._command_state:
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
self.async_schedule_update_ha_state()
|
self.async_write_ha_state()
|
||||||
await self._update_entity_state()
|
await self._update_entity_state()
|
||||||
|
@@ -46,10 +46,10 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
from . import group as group_pre_import # noqa: F401
|
from . import group as group_pre_import # noqa: F401
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "cover"
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
3
homeassistant/components/cover/const.py
Normal file
3
homeassistant/components/cover/const.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""Constants for cover entity platform."""
|
||||||
|
|
||||||
|
DOMAIN = "cover"
|
@@ -5,6 +5,8 @@ from typing import TYPE_CHECKING
|
|||||||
from homeassistant.const import STATE_CLOSED, STATE_OPEN
|
from homeassistant.const import STATE_CLOSED, STATE_OPEN
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.components.group import GroupIntegrationRegistry
|
from homeassistant.components.group import GroupIntegrationRegistry
|
||||||
|
|
||||||
@@ -15,4 +17,4 @@ def async_describe_on_off_states(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Describe group on off states."""
|
"""Describe group on off states."""
|
||||||
# On means open, Off means closed
|
# On means open, Off means closed
|
||||||
registry.on_off_states({STATE_OPEN}, STATE_CLOSED)
|
registry.on_off_states(DOMAIN, {STATE_OPEN}, STATE_OPEN, STATE_CLOSED)
|
||||||
|
@@ -5,6 +5,8 @@ from typing import TYPE_CHECKING
|
|||||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.components.group import GroupIntegrationRegistry
|
from homeassistant.components.group import GroupIntegrationRegistry
|
||||||
|
|
||||||
@@ -14,4 +16,4 @@ def async_describe_on_off_states(
|
|||||||
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
|
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Describe group on off states."""
|
"""Describe group on off states."""
|
||||||
registry.on_off_states({STATE_HOME}, STATE_NOT_HOME)
|
registry.on_off_states(DOMAIN, {STATE_HOME}, STATE_HOME, STATE_NOT_HOME)
|
||||||
|
@@ -11,7 +11,7 @@ from .coordinator import DwdWeatherWarningsCoordinator
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
coordinator = DwdWeatherWarningsCoordinator(hass, entry)
|
coordinator = DwdWeatherWarningsCoordinator(hass)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
|
@@ -26,7 +26,7 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
|
|||||||
config_entry: ConfigEntry
|
config_entry: ConfigEntry
|
||||||
api: DwdWeatherWarningsAPI
|
api: DwdWeatherWarningsAPI
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the dwd_weather_warnings coordinator."""
|
"""Initialize the dwd_weather_warnings coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL
|
hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL
|
||||||
|
@@ -37,6 +37,7 @@ PLATFORMS = [
|
|||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
Platform.VACUUM,
|
Platform.VACUUM,
|
||||||
]
|
]
|
||||||
|
EcovacsConfigEntry = ConfigEntry[EcovacsController]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
@@ -50,21 +51,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: EcovacsConfigEntry) -> bool:
|
||||||
"""Set up this integration using UI."""
|
"""Set up this integration using UI."""
|
||||||
controller = EcovacsController(hass, entry.data)
|
controller = EcovacsController(hass, entry.data)
|
||||||
await controller.initialize()
|
await controller.initialize()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = controller
|
async def on_unload() -> None:
|
||||||
|
await controller.teardown()
|
||||||
|
|
||||||
|
entry.async_on_unload(on_unload)
|
||||||
|
entry.runtime_data = controller
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: EcovacsConfigEntry) -> bool:
|
||||||
"""Unload config entry."""
|
"""Unload config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
await hass.data[DOMAIN][entry.entry_id].teardown()
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
if not hass.data[DOMAIN]:
|
|
||||||
hass.data.pop(DOMAIN)
|
|
||||||
return unload_ok
|
|
||||||
|
@@ -11,13 +11,11 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CapabilityDevice,
|
CapabilityDevice,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
@@ -52,13 +50,14 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsBinarySensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
get_supported_entitites(controller, EcovacsBinarySensor, ENTITY_DESCRIPTIONS)
|
get_supported_entitites(
|
||||||
|
config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -11,13 +11,12 @@ from deebot_client.capabilities import (
|
|||||||
from deebot_client.events import LifeSpan
|
from deebot_client.events import LifeSpan
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, SUPPORTED_LIFESPANS
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
from .const import SUPPORTED_LIFESPANS
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CapabilityDevice,
|
CapabilityDevice,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
@@ -66,11 +65,11 @@ LIFESPAN_ENTITY_DESCRIPTIONS = tuple(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||||
controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS
|
controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS
|
||||||
)
|
)
|
||||||
|
@@ -42,7 +42,7 @@ class EcovacsController:
|
|||||||
"""Initialize controller."""
|
"""Initialize controller."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._devices: list[Device] = []
|
self._devices: list[Device] = []
|
||||||
self.legacy_devices: list[VacBot] = []
|
self._legacy_devices: list[VacBot] = []
|
||||||
rest_url = config.get(CONF_OVERRIDE_REST_URL)
|
rest_url = config.get(CONF_OVERRIDE_REST_URL)
|
||||||
self._device_id = get_client_device_id(hass, rest_url is not None)
|
self._device_id = get_client_device_id(hass, rest_url is not None)
|
||||||
country = config[CONF_COUNTRY]
|
country = config[CONF_COUNTRY]
|
||||||
@@ -101,7 +101,7 @@ class EcovacsController:
|
|||||||
self._continent,
|
self._continent,
|
||||||
monitor=True,
|
monitor=True,
|
||||||
)
|
)
|
||||||
self.legacy_devices.append(bot)
|
self._legacy_devices.append(bot)
|
||||||
except InvalidAuthenticationError as ex:
|
except InvalidAuthenticationError as ex:
|
||||||
raise ConfigEntryError("Invalid credentials") from ex
|
raise ConfigEntryError("Invalid credentials") from ex
|
||||||
except DeebotError as ex:
|
except DeebotError as ex:
|
||||||
@@ -113,7 +113,7 @@ class EcovacsController:
|
|||||||
"""Disconnect controller."""
|
"""Disconnect controller."""
|
||||||
for device in self._devices:
|
for device in self._devices:
|
||||||
await device.teardown()
|
await device.teardown()
|
||||||
for legacy_device in self.legacy_devices:
|
for legacy_device in self._legacy_devices:
|
||||||
await self._hass.async_add_executor_job(legacy_device.disconnect)
|
await self._hass.async_add_executor_job(legacy_device.disconnect)
|
||||||
await self._mqtt.disconnect()
|
await self._mqtt.disconnect()
|
||||||
await self._authenticator.teardown()
|
await self._authenticator.teardown()
|
||||||
@@ -124,3 +124,8 @@ class EcovacsController:
|
|||||||
for device in self._devices:
|
for device in self._devices:
|
||||||
if isinstance(device.capabilities, capability):
|
if isinstance(device.capabilities, capability):
|
||||||
yield device
|
yield device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy_devices(self) -> list[VacBot]:
|
||||||
|
"""Return legacy devices."""
|
||||||
|
return self._legacy_devices
|
||||||
|
@@ -7,12 +7,11 @@ from typing import Any
|
|||||||
from deebot_client.capabilities import Capabilities
|
from deebot_client.capabilities import Capabilities
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import CONF_OVERRIDE_MQTT_URL, CONF_OVERRIDE_REST_URL, DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
from .const import CONF_OVERRIDE_MQTT_URL, CONF_OVERRIDE_REST_URL
|
||||||
|
|
||||||
REDACT_CONFIG = {
|
REDACT_CONFIG = {
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
@@ -25,10 +24,10 @@ REDACT_DEVICE = {"did", CONF_NAME, "homeId"}
|
|||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: EcovacsConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
diag: dict[str, Any] = {
|
diag: dict[str, Any] = {
|
||||||
"config": async_redact_data(config_entry.as_dict(), REDACT_CONFIG)
|
"config": async_redact_data(config_entry.as_dict(), REDACT_CONFIG)
|
||||||
}
|
}
|
||||||
|
@@ -5,24 +5,22 @@ from deebot_client.device import Device
|
|||||||
from deebot_client.events import CleanJobStatus, ReportStatsEvent
|
from deebot_client.events import CleanJobStatus, ReportStatsEvent
|
||||||
|
|
||||||
from homeassistant.components.event import EventEntity, EventEntityDescription
|
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import EcovacsEntity
|
from .entity import EcovacsEntity
|
||||||
from .util import get_name_key
|
from .util import get_name_key
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
EcovacsLastJobEventEntity(device) for device in controller.devices(Capabilities)
|
EcovacsLastJobEventEntity(device) for device in controller.devices(Capabilities)
|
||||||
)
|
)
|
||||||
|
@@ -5,23 +5,21 @@ from deebot_client.device import Device
|
|||||||
from deebot_client.events.map import CachedMapInfoEvent, MapChangedEvent
|
from deebot_client.events.map import CachedMapInfoEvent, MapChangedEvent
|
||||||
|
|
||||||
from homeassistant.components.image import ImageEntity
|
from homeassistant.components.image import ImageEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import EcovacsEntity
|
from .entity import EcovacsEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
entities = []
|
entities = []
|
||||||
for device in controller.devices(VacuumCapabilities):
|
for device in controller.devices(VacuumCapabilities):
|
||||||
capabilities: VacuumCapabilities = device.capabilities
|
capabilities: VacuumCapabilities = device.capabilities
|
||||||
|
@@ -15,12 +15,10 @@ from homeassistant.components.lawn_mower import (
|
|||||||
LawnMowerEntityEntityDescription,
|
LawnMowerEntityEntityDescription,
|
||||||
LawnMowerEntityFeature,
|
LawnMowerEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import EcovacsEntity
|
from .entity import EcovacsEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -38,11 +36,11 @@ _STATE_TO_MOWER_STATE = {
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Ecovacs mowers."""
|
"""Set up the Ecovacs mowers."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
mowers: list[EcovacsMower] = [
|
mowers: list[EcovacsMower] = [
|
||||||
EcovacsMower(device) for device in controller.devices(MowerCapabilities)
|
EcovacsMower(device) for device in controller.devices(MowerCapabilities)
|
||||||
]
|
]
|
||||||
|
@@ -10,13 +10,11 @@ from deebot_client.capabilities import Capabilities, CapabilitySet, VacuumCapabi
|
|||||||
from deebot_client.events import CleanCountEvent, VolumeEvent
|
from deebot_client.events import CleanCountEvent, VolumeEvent
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CapabilityDevice,
|
CapabilityDevice,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
@@ -70,11 +68,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||||
controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS
|
controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS
|
||||||
)
|
)
|
||||||
|
@@ -9,13 +9,11 @@ from deebot_client.device import Device
|
|||||||
from deebot_client.events import WaterInfoEvent, WorkModeEvent
|
from deebot_client.events import WaterInfoEvent, WorkModeEvent
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CapabilityDevice,
|
CapabilityDevice,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
@@ -62,11 +60,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
entities = get_supported_entitites(
|
entities = get_supported_entitites(
|
||||||
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
|
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
|
||||||
)
|
)
|
||||||
|
@@ -24,7 +24,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
AREA_SQUARE_METERS,
|
AREA_SQUARE_METERS,
|
||||||
ATTR_BATTERY_LEVEL,
|
ATTR_BATTERY_LEVEL,
|
||||||
@@ -37,8 +36,8 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import DOMAIN, SUPPORTED_LIFESPANS
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
from .const import SUPPORTED_LIFESPANS
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CapabilityDevice,
|
CapabilityDevice,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
@@ -171,11 +170,11 @@ LIFESPAN_ENTITY_DESCRIPTIONS = tuple(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||||
controller, EcovacsSensor, ENTITY_DESCRIPTIONS
|
controller, EcovacsSensor, ENTITY_DESCRIPTIONS
|
||||||
|
@@ -11,13 +11,11 @@ from deebot_client.capabilities import (
|
|||||||
from deebot_client.events import EnableEvent
|
from deebot_client.events import EnableEvent
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import EcovacsConfigEntry
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CapabilityDevice,
|
CapabilityDevice,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
@@ -121,11 +119,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSwitchEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add entities for passed config_entry in HA."""
|
"""Add entities for passed config_entry in HA."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||||
controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS
|
controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS
|
||||||
)
|
)
|
||||||
|
@@ -23,15 +23,14 @@ from homeassistant.components.vacuum import (
|
|||||||
StateVacuumEntityDescription,
|
StateVacuumEntityDescription,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.icon import icon_for_battery_level
|
from homeassistant.helpers.icon import icon_for_battery_level
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from . import EcovacsConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .controller import EcovacsController
|
|
||||||
from .entity import EcovacsEntity
|
from .entity import EcovacsEntity
|
||||||
from .util import get_name_key
|
from .util import get_name_key
|
||||||
|
|
||||||
@@ -43,11 +42,11 @@ ATTR_COMPONENT_PREFIX = "component_"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: EcovacsConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Ecovacs vacuums."""
|
"""Set up the Ecovacs vacuums."""
|
||||||
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
|
controller = config_entry.runtime_data
|
||||||
vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [
|
vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [
|
||||||
EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities)
|
EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities)
|
||||||
]
|
]
|
||||||
|
@@ -15,5 +15,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["elkm1_lib"],
|
"loggers": ["elkm1_lib"],
|
||||||
"requirements": ["elkm1-lib==2.2.6"]
|
"requirements": ["elkm1-lib==2.2.7"]
|
||||||
}
|
}
|
||||||
|
@@ -12,11 +12,10 @@ from homeassistant.components.sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import UnitOfPower
|
from homeassistant.const import UnitOfPower
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
@@ -63,7 +62,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
|
class EmonitorPowerSensor(CoordinatorEntity[EmonitorStatus], SensorEntity):
|
||||||
"""Representation of an Emonitor power sensor entity."""
|
"""Representation of an Emonitor power sensor entity."""
|
||||||
|
|
||||||
_attr_device_class = SensorDeviceClass.POWER
|
_attr_device_class = SensorDeviceClass.POWER
|
||||||
@@ -81,7 +80,8 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
|
|||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self.channel_number = channel_number
|
self.channel_number = channel_number
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
mac_address = self.emonitor_status.network.mac_address
|
emonitor_status = self.coordinator.data
|
||||||
|
mac_address = emonitor_status.network.mac_address
|
||||||
device_name = name_short_mac(mac_address[-6:])
|
device_name = name_short_mac(mac_address[-6:])
|
||||||
label = self.channel_data.label or str(channel_number)
|
label = self.channel_data.label or str(channel_number)
|
||||||
if description.translation_key is not None:
|
if description.translation_key is not None:
|
||||||
@@ -94,13 +94,15 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
|
|||||||
connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
|
connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
|
||||||
manufacturer="Powerhouse Dynamics, Inc.",
|
manufacturer="Powerhouse Dynamics, Inc.",
|
||||||
name=device_name,
|
name=device_name,
|
||||||
sw_version=self.emonitor_status.hardware.firmware_version,
|
sw_version=emonitor_status.hardware.firmware_version,
|
||||||
)
|
)
|
||||||
|
self._attr_extra_state_attributes = {"channel": channel_number}
|
||||||
|
self._attr_native_value = self._paired_attr(self.entity_description.key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channels(self) -> dict[int, EmonitorChannel]:
|
def channels(self) -> dict[int, EmonitorChannel]:
|
||||||
"""Return the channels dict."""
|
"""Return the channels dict."""
|
||||||
channels: dict[int, EmonitorChannel] = self.emonitor_status.channels
|
channels: dict[int, EmonitorChannel] = self.coordinator.data.channels
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -108,11 +110,6 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
|
|||||||
"""Return the channel data."""
|
"""Return the channel data."""
|
||||||
return self.channels[self.channel_number]
|
return self.channels[self.channel_number]
|
||||||
|
|
||||||
@property
|
|
||||||
def emonitor_status(self) -> EmonitorStatus:
|
|
||||||
"""Return the EmonitorStatus."""
|
|
||||||
return self.coordinator.data
|
|
||||||
|
|
||||||
def _paired_attr(self, attr_name: str) -> float:
|
def _paired_attr(self, attr_name: str) -> float:
|
||||||
"""Cumulative attributes for channel and paired channel."""
|
"""Cumulative attributes for channel and paired channel."""
|
||||||
channel_data = self.channels[self.channel_number]
|
channel_data = self.channels[self.channel_number]
|
||||||
@@ -121,12 +118,8 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
|
|||||||
attr_val += getattr(self.channels[paired_channel], attr_name)
|
attr_val += getattr(self.channels[paired_channel], attr_name)
|
||||||
return attr_val
|
return attr_val
|
||||||
|
|
||||||
@property
|
@callback
|
||||||
def native_value(self) -> StateType:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""State of the sensor."""
|
"""Handle updated data from the coordinator."""
|
||||||
return self._paired_attr(self.entity_description.key)
|
self._attr_native_value = self._paired_attr(self.entity_description.key)
|
||||||
|
return super()._handle_coordinator_update()
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, int]:
|
|
||||||
"""Return the device specific state attributes."""
|
|
||||||
return {"channel": self.channel_number}
|
|
||||||
|
@@ -20,7 +20,7 @@ UPDATE_INTERVAL: Final = datetime.timedelta(minutes=30)
|
|||||||
TIMEOUT = 10
|
TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
class FitbitDeviceCoordinator(DataUpdateCoordinator):
|
class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, FitbitDevice]]):
|
||||||
"""Coordinator for fetching fitbit devices from the API."""
|
"""Coordinator for fetching fitbit devices from the API."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, api: FitbitApi) -> None:
|
def __init__(self, hass: HomeAssistant, api: FitbitApi) -> None:
|
||||||
|
@@ -443,7 +443,10 @@ class FritzBoxTools(
|
|||||||
)
|
)
|
||||||
except Exception as ex: # pylint: disable=[broad-except]
|
except Exception as ex: # pylint: disable=[broad-except]
|
||||||
if not self.hass.is_stopping:
|
if not self.hass.is_stopping:
|
||||||
raise HomeAssistantError("Error refreshing hosts info") from ex
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="error_refresh_hosts_info",
|
||||||
|
) from ex
|
||||||
|
|
||||||
hosts: dict[str, Device] = {}
|
hosts: dict[str, Device] = {}
|
||||||
if hosts_attributes:
|
if hosts_attributes:
|
||||||
@@ -730,7 +733,9 @@ class FritzBoxTools(
|
|||||||
_LOGGER.debug("FRITZ!Box service: %s", service_call.service)
|
_LOGGER.debug("FRITZ!Box service: %s", service_call.service)
|
||||||
|
|
||||||
if not self.connection:
|
if not self.connection:
|
||||||
raise HomeAssistantError("Unable to establish a connection")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="unable_to_connect"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if service_call.service == SERVICE_REBOOT:
|
if service_call.service == SERVICE_REBOOT:
|
||||||
@@ -765,9 +770,13 @@ class FritzBoxTools(
|
|||||||
return
|
return
|
||||||
|
|
||||||
except (FritzServiceError, FritzActionError) as ex:
|
except (FritzServiceError, FritzActionError) as ex:
|
||||||
raise HomeAssistantError("Service or parameter unknown") from ex
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="service_parameter_unknown"
|
||||||
|
) from ex
|
||||||
except FritzConnectionException as ex:
|
except FritzConnectionException as ex:
|
||||||
raise HomeAssistantError("Service not supported") from ex
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="service_not_supported"
|
||||||
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
class AvmWrapper(FritzBoxTools): # pylint: disable=hass-enforce-coordinator-module
|
class AvmWrapper(FritzBoxTools): # pylint: disable=hass-enforce-coordinator-module
|
||||||
|
@@ -55,8 +55,9 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Failed to call service '{service_call.service}'. Config entry for"
|
translation_domain=DOMAIN,
|
||||||
" target not found"
|
translation_key="config_entry_not_found",
|
||||||
|
translation_placeholders={"service": service_call.service},
|
||||||
)
|
)
|
||||||
|
|
||||||
for entry_id in fritzbox_entry_ids:
|
for entry_id in fritzbox_entry_ids:
|
||||||
|
@@ -192,5 +192,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"config_entry_not_found": {
|
||||||
|
"message": "Failed to call service \"{service}\". Config entry for target not found"
|
||||||
|
},
|
||||||
|
"service_parameter_unknown": { "message": "Service or parameter unknown" },
|
||||||
|
"service_not_supported": { "message": "Service not supported" },
|
||||||
|
"error_refresh_hosts_info": {
|
||||||
|
"message": "Error refreshing hosts info"
|
||||||
|
},
|
||||||
|
"unable_to_connect": {
|
||||||
|
"message": "Unable to establish a connection"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,52 +4,23 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
|
from pyfritzhome import FritzhomeDevice
|
||||||
from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase
|
from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase
|
||||||
from requests.exceptions import ConnectionError as RequestConnectionError
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfTemperature
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_HOST,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_USERNAME,
|
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
UnitOfTemperature,
|
|
||||||
)
|
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS
|
from .const import DOMAIN, LOGGER, PLATFORMS
|
||||||
from .coordinator import FritzboxDataUpdateCoordinator
|
from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> bool:
|
||||||
"""Set up the AVM FRITZ!SmartHome platforms."""
|
"""Set up the AVM FRITZ!SmartHome platforms."""
|
||||||
fritz = Fritzhome(
|
|
||||||
host=entry.data[CONF_HOST],
|
|
||||||
user=entry.data[CONF_USERNAME],
|
|
||||||
password=entry.data[CONF_PASSWORD],
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await hass.async_add_executor_job(fritz.login)
|
|
||||||
except RequestConnectionError as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
except LoginError as err:
|
|
||||||
raise ConfigEntryAuthFailed from err
|
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
|
||||||
CONF_CONNECTIONS: fritz,
|
|
||||||
}
|
|
||||||
|
|
||||||
has_templates = await hass.async_add_executor_job(fritz.has_templates)
|
|
||||||
LOGGER.debug("enable smarthome templates: %s", has_templates)
|
|
||||||
|
|
||||||
def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None:
|
def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None:
|
||||||
"""Update unique ID of entity entry."""
|
"""Update unique ID of entity entry."""
|
||||||
@@ -73,15 +44,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
await async_migrate_entries(hass, entry.entry_id, _update_unique_id)
|
await async_migrate_entries(hass, entry.entry_id, _update_unique_id)
|
||||||
|
|
||||||
coordinator = FritzboxDataUpdateCoordinator(hass, entry.entry_id, has_templates)
|
coordinator = FritzboxDataUpdateCoordinator(hass, entry.entry_id)
|
||||||
await coordinator.async_setup()
|
await coordinator.async_setup()
|
||||||
hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
def logout_fritzbox(event: Event) -> None:
|
def logout_fritzbox(event: Event) -> None:
|
||||||
"""Close connections to this fritzbox."""
|
"""Close connections to this fritzbox."""
|
||||||
fritz.logout()
|
coordinator.fritz.logout()
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox)
|
||||||
@@ -90,25 +62,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> bool:
|
||||||
"""Unloading the AVM FRITZ!SmartHome platforms."""
|
"""Unloading the AVM FRITZ!SmartHome platforms."""
|
||||||
fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS]
|
await hass.async_add_executor_job(entry.runtime_data.fritz.logout)
|
||||||
await hass.async_add_executor_job(fritz.logout)
|
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def async_remove_config_entry_device(
|
async def async_remove_config_entry_device(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
|
hass: HomeAssistant, entry: FritzboxConfigEntry, device: DeviceEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Remove Fritzbox config entry from a device."""
|
"""Remove Fritzbox config entry from a device."""
|
||||||
coordinator: FritzboxDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
coordinator = entry.runtime_data
|
||||||
CONF_COORDINATOR
|
|
||||||
]
|
|
||||||
|
|
||||||
for identifier in device.identifiers:
|
for identifier in device.identifiers:
|
||||||
if identifier[0] == DOMAIN and (
|
if identifier[0] == DOMAIN and (
|
||||||
|
@@ -13,13 +13,12 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FritzBoxDeviceEntity
|
from . import FritzBoxDeviceEntity
|
||||||
from .common import get_coordinator
|
from .coordinator import FritzboxConfigEntry
|
||||||
from .model import FritzEntityDescriptionMixinBase
|
from .model import FritzEntityDescriptionMixinBase
|
||||||
|
|
||||||
|
|
||||||
@@ -65,10 +64,12 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome binary sensor from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome binary sensor from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(devices: set[str] | None = None) -> None:
|
def _add_entities(devices: set[str] | None = None) -> None:
|
||||||
|
@@ -3,21 +3,22 @@
|
|||||||
from pyfritzhome.devicetypes import FritzhomeTemplate
|
from pyfritzhome.devicetypes import FritzhomeTemplate
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity
|
from homeassistant.components.button import ButtonEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FritzBoxEntity
|
from . import FritzBoxEntity
|
||||||
from .common import get_coordinator
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .coordinator import FritzboxConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome template from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome template from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(templates: set[str] | None = None) -> None:
|
def _add_entities(templates: set[str] | None = None) -> None:
|
||||||
|
@@ -12,7 +12,6 @@ from homeassistant.components.climate import (
|
|||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_BATTERY_LEVEL,
|
ATTR_BATTERY_LEVEL,
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
@@ -23,7 +22,6 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FritzBoxDeviceEntity
|
from . import FritzBoxDeviceEntity
|
||||||
from .common import get_coordinator
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_STATE_BATTERY_LOW,
|
ATTR_STATE_BATTERY_LOW,
|
||||||
ATTR_STATE_HOLIDAY_MODE,
|
ATTR_STATE_HOLIDAY_MODE,
|
||||||
@@ -31,6 +29,7 @@ from .const import (
|
|||||||
ATTR_STATE_WINDOW_OPEN,
|
ATTR_STATE_WINDOW_OPEN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
|
from .coordinator import FritzboxConfigEntry
|
||||||
from .model import ClimateExtraAttributes
|
from .model import ClimateExtraAttributes
|
||||||
|
|
||||||
OPERATION_LIST = [HVACMode.HEAT, HVACMode.OFF]
|
OPERATION_LIST = [HVACMode.HEAT, HVACMode.OFF]
|
||||||
@@ -48,10 +47,12 @@ OFF_REPORT_SET_TEMPERATURE = 0.0
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome thermostat from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome thermostat from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(devices: set[str] | None = None) -> None:
|
def _add_entities(devices: set[str] | None = None) -> None:
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
"""Common functions for fritzbox integration."""
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import CONF_COORDINATOR, DOMAIN
|
|
||||||
from .coordinator import FritzboxDataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
def get_coordinator(
|
|
||||||
hass: HomeAssistant, config_entry_id: str
|
|
||||||
) -> FritzboxDataUpdateCoordinator:
|
|
||||||
"""Get coordinator for given config entry id."""
|
|
||||||
coordinator: FritzboxDataUpdateCoordinator = hass.data[DOMAIN][config_entry_id][
|
|
||||||
CONF_COORDINATOR
|
|
||||||
]
|
|
||||||
return coordinator
|
|
@@ -15,9 +15,6 @@ ATTR_STATE_WINDOW_OPEN: Final = "window_open"
|
|||||||
COLOR_MODE: Final = "1"
|
COLOR_MODE: Final = "1"
|
||||||
COLOR_TEMP_MODE: Final = "4"
|
COLOR_TEMP_MODE: Final = "4"
|
||||||
|
|
||||||
CONF_CONNECTIONS: Final = "connections"
|
|
||||||
CONF_COORDINATOR: Final = "coordinator"
|
|
||||||
|
|
||||||
DEFAULT_HOST: Final = "fritz.box"
|
DEFAULT_HOST: Final = "fritz.box"
|
||||||
DEFAULT_USERNAME: Final = "admin"
|
DEFAULT_USERNAME: Final = "admin"
|
||||||
|
|
||||||
|
@@ -10,12 +10,15 @@ from pyfritzhome.devicetypes import FritzhomeTemplate
|
|||||||
from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
|
from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import CONF_CONNECTIONS, DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
FritzboxConfigEntry = ConfigEntry["FritzboxDataUpdateCoordinator"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -29,10 +32,12 @@ class FritzboxCoordinatorData:
|
|||||||
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
|
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
|
||||||
"""Fritzbox Smarthome device data update coordinator."""
|
"""Fritzbox Smarthome device data update coordinator."""
|
||||||
|
|
||||||
config_entry: ConfigEntry
|
config_entry: FritzboxConfigEntry
|
||||||
configuration_url: str
|
configuration_url: str
|
||||||
|
fritz: Fritzhome
|
||||||
|
has_templates: bool
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, name: str, has_templates: bool) -> None:
|
def __init__(self, hass: HomeAssistant, name: str) -> None:
|
||||||
"""Initialize the Fritzbox Smarthome device coordinator."""
|
"""Initialize the Fritzbox Smarthome device coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@@ -41,11 +46,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
|||||||
update_interval=timedelta(seconds=30),
|
update_interval=timedelta(seconds=30),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fritz: Fritzhome = hass.data[DOMAIN][self.config_entry.entry_id][
|
|
||||||
CONF_CONNECTIONS
|
|
||||||
]
|
|
||||||
self.configuration_url = self.fritz.get_prefixed_host()
|
|
||||||
self.has_templates = has_templates
|
|
||||||
self.new_devices: set[str] = set()
|
self.new_devices: set[str] = set()
|
||||||
self.new_templates: set[str] = set()
|
self.new_templates: set[str] = set()
|
||||||
|
|
||||||
@@ -53,6 +53,27 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
|||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Set up the coordinator."""
|
"""Set up the coordinator."""
|
||||||
|
|
||||||
|
self.fritz = Fritzhome(
|
||||||
|
host=self.config_entry.data[CONF_HOST],
|
||||||
|
user=self.config_entry.data[CONF_USERNAME],
|
||||||
|
password=self.config_entry.data[CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(self.fritz.login)
|
||||||
|
except RequestConnectionError as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
except LoginError as err:
|
||||||
|
raise ConfigEntryAuthFailed from err
|
||||||
|
|
||||||
|
self.has_templates = await self.hass.async_add_executor_job(
|
||||||
|
self.fritz.has_templates
|
||||||
|
)
|
||||||
|
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
|
||||||
|
|
||||||
|
self.configuration_url = self.fritz.get_prefixed_host()
|
||||||
|
|
||||||
await self.async_config_entry_first_refresh()
|
await self.async_config_entry_first_refresh()
|
||||||
self.cleanup_removed_devices(
|
self.cleanup_removed_devices(
|
||||||
list(self.data.devices) + list(self.data.templates)
|
list(self.data.devices) + list(self.data.templates)
|
||||||
|
@@ -10,19 +10,20 @@ from homeassistant.components.cover import (
|
|||||||
CoverEntity,
|
CoverEntity,
|
||||||
CoverEntityFeature,
|
CoverEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FritzBoxDeviceEntity
|
from . import FritzBoxDeviceEntity
|
||||||
from .common import get_coordinator
|
from .coordinator import FritzboxConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome cover from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome cover from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(devices: set[str] | None = None) -> None:
|
def _add_entities(devices: set[str] | None = None) -> None:
|
||||||
|
@@ -5,22 +5,19 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import CONF_COORDINATOR, DOMAIN
|
from .coordinator import FritzboxConfigEntry
|
||||||
from .coordinator import FritzboxDataUpdateCoordinator
|
|
||||||
|
|
||||||
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
|
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, entry: ConfigEntry
|
hass: HomeAssistant, entry: FritzboxConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
data: dict = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
coordinator: FritzboxDataUpdateCoordinator = data[CONF_COORDINATOR]
|
|
||||||
|
|
||||||
diag_data = {
|
diag_data = {
|
||||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||||
|
@@ -13,22 +13,23 @@ from homeassistant.components.light import (
|
|||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity
|
from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity
|
||||||
from .common import get_coordinator
|
|
||||||
from .const import COLOR_MODE, COLOR_TEMP_MODE, LOGGER
|
from .const import COLOR_MODE, COLOR_TEMP_MODE, LOGGER
|
||||||
|
from .coordinator import FritzboxConfigEntry
|
||||||
|
|
||||||
SUPPORTED_COLOR_MODES = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
SUPPORTED_COLOR_MODES = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome light from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome light from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(devices: set[str] | None = None) -> None:
|
def _add_entities(devices: set[str] | None = None) -> None:
|
||||||
|
@@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
@@ -32,7 +31,7 @@ from homeassistant.helpers.typing import StateType
|
|||||||
from homeassistant.util.dt import utc_from_timestamp
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
from . import FritzBoxDeviceEntity
|
from . import FritzBoxDeviceEntity
|
||||||
from .common import get_coordinator
|
from .coordinator import FritzboxConfigEntry
|
||||||
from .model import FritzEntityDescriptionMixinBase
|
from .model import FritzEntityDescriptionMixinBase
|
||||||
|
|
||||||
|
|
||||||
@@ -210,10 +209,12 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome sensor from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome sensor from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(devices: set[str] | None = None) -> None:
|
def _add_entities(devices: set[str] | None = None) -> None:
|
||||||
|
@@ -5,19 +5,20 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import FritzBoxDeviceEntity
|
from . import FritzBoxDeviceEntity
|
||||||
from .common import get_coordinator
|
from .coordinator import FritzboxConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: FritzboxConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the FRITZ!SmartHome switch from ConfigEntry."""
|
"""Set up the FRITZ!SmartHome switch from ConfigEntry."""
|
||||||
coordinator = get_coordinator(hass, entry.entry_id)
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _add_entities(devices: set[str] | None = None) -> None:
|
def _add_entities(devices: set[str] | None = None) -> None:
|
||||||
|
@@ -11,19 +11,16 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .base import FritzBoxPhonebook
|
from .base import FritzBoxPhonebook
|
||||||
from .const import (
|
from .const import CONF_PHONEBOOK, CONF_PREFIXES, PLATFORMS
|
||||||
CONF_PHONEBOOK,
|
|
||||||
CONF_PREFIXES,
|
|
||||||
DOMAIN,
|
|
||||||
FRITZBOX_PHONEBOOK,
|
|
||||||
PLATFORMS,
|
|
||||||
UNDO_UPDATE_LISTENER,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
FritzBoxCallMonitorConfigEntry = ConfigEntry[FritzBoxPhonebook]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, config_entry: FritzBoxCallMonitorConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Set up the fritzbox_callmonitor platforms."""
|
"""Set up the fritzbox_callmonitor platforms."""
|
||||||
fritzbox_phonebook = FritzBoxPhonebook(
|
fritzbox_phonebook = FritzBoxPhonebook(
|
||||||
host=config_entry.data[CONF_HOST],
|
host=config_entry.data[CONF_HOST],
|
||||||
@@ -51,34 +48,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
_LOGGER.error("Unable to connect to AVM FRITZ!Box call monitor: %s", ex)
|
_LOGGER.error("Unable to connect to AVM FRITZ!Box call monitor: %s", ex)
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
undo_listener = config_entry.add_update_listener(update_listener)
|
config_entry.runtime_data = fritzbox_phonebook
|
||||||
|
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
|
||||||
FRITZBOX_PHONEBOOK: fritzbox_phonebook,
|
|
||||||
UNDO_UPDATE_LISTENER: undo_listener,
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, config_entry: FritzBoxCallMonitorConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unloading the fritzbox_callmonitor platforms."""
|
"""Unloading the fritzbox_callmonitor platforms."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
|
||||||
config_entry, PLATFORMS
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
async def update_listener(
|
||||||
|
hass: HomeAssistant, config_entry: FritzBoxCallMonitorConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Update listener to reload after option has changed."""
|
"""Update listener to reload after option has changed."""
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
@@ -38,5 +38,3 @@ DOMAIN: Final = "fritzbox_callmonitor"
|
|||||||
MANUFACTURER: Final = "AVM"
|
MANUFACTURER: Final = "AVM"
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
|
|
||||||
FRITZBOX_PHONEBOOK: Final = "fritzbox_phonebook"
|
|
||||||
|
@@ -14,19 +14,18 @@ from typing import Any, cast
|
|||||||
from fritzconnection.core.fritzmonitor import FritzMonitor
|
from fritzconnection.core.fritzmonitor import FritzMonitor
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import FritzBoxCallMonitorConfigEntry
|
||||||
from .base import FritzBoxPhonebook
|
from .base import FritzBoxPhonebook
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_PREFIXES,
|
ATTR_PREFIXES,
|
||||||
CONF_PHONEBOOK,
|
CONF_PHONEBOOK,
|
||||||
CONF_PREFIXES,
|
CONF_PREFIXES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FRITZBOX_PHONEBOOK,
|
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
SERIAL_NUMBER,
|
SERIAL_NUMBER,
|
||||||
FritzState,
|
FritzState,
|
||||||
@@ -48,13 +47,11 @@ class CallState(StrEnum):
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: FritzBoxCallMonitorConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the fritzbox_callmonitor sensor from config_entry."""
|
"""Set up the fritzbox_callmonitor sensor from config_entry."""
|
||||||
fritzbox_phonebook: FritzBoxPhonebook = hass.data[DOMAIN][config_entry.entry_id][
|
fritzbox_phonebook = config_entry.runtime_data
|
||||||
FRITZBOX_PHONEBOOK
|
|
||||||
]
|
|
||||||
|
|
||||||
phonebook_id: int = config_entry.data[CONF_PHONEBOOK]
|
phonebook_id: int = config_entry.data[CONF_PHONEBOOK]
|
||||||
prefixes: list[str] | None = config_entry.options.get(CONF_PREFIXES)
|
prefixes: list[str] | None = config_entry.options.get(CONF_PREFIXES)
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20240424.1"]
|
"requirements": ["home-assistant-frontend==20240501.0"]
|
||||||
}
|
}
|
||||||
|
@@ -2,15 +2,23 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from fyta_cli.fyta_connector import FytaConnector
|
from fyta_cli.fyta_connector import FytaConnector
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_USERNAME,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import CONF_EXPIRATION, DOMAIN
|
||||||
from .coordinator import FytaCoordinator
|
from .coordinator import FytaCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -22,11 +30,16 @@ PLATFORMS = [
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up the Fyta integration."""
|
"""Set up the Fyta integration."""
|
||||||
|
tz: str = hass.config.time_zone
|
||||||
|
|
||||||
username = entry.data[CONF_USERNAME]
|
username = entry.data[CONF_USERNAME]
|
||||||
password = entry.data[CONF_PASSWORD]
|
password = entry.data[CONF_PASSWORD]
|
||||||
|
access_token: str = entry.data[CONF_ACCESS_TOKEN]
|
||||||
|
expiration: datetime = datetime.fromisoformat(
|
||||||
|
entry.data[CONF_EXPIRATION]
|
||||||
|
).astimezone(ZoneInfo(tz))
|
||||||
|
|
||||||
fyta = FytaConnector(username, password)
|
fyta = FytaConnector(username, password, access_token, expiration, tz)
|
||||||
|
|
||||||
coordinator = FytaCoordinator(hass, fyta)
|
coordinator = FytaCoordinator(hass, fyta)
|
||||||
|
|
||||||
@@ -47,3 +60,36 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Migrate old entry."""
|
||||||
|
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||||
|
|
||||||
|
if config_entry.version > 1:
|
||||||
|
# This means the user has downgraded from a future version
|
||||||
|
return False
|
||||||
|
|
||||||
|
if config_entry.version == 1:
|
||||||
|
if config_entry.minor_version < 2:
|
||||||
|
new = {**config_entry.data}
|
||||||
|
fyta = FytaConnector(
|
||||||
|
config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]
|
||||||
|
)
|
||||||
|
credentials: dict[str, Any] = await fyta.login()
|
||||||
|
await fyta.client.close()
|
||||||
|
|
||||||
|
new[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN]
|
||||||
|
new[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat()
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
config_entry, data=new, minor_version=2, version=1
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migration to version %s.%s successful",
|
||||||
|
config_entry.version,
|
||||||
|
config_entry.minor_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@@ -17,7 +17,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import CONF_EXPIRATION, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -31,14 +31,19 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Fyta."""
|
"""Handle a config flow for Fyta."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
_entry: ConfigEntry | None = None
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize FytaConfigFlow."""
|
||||||
|
self.credentials: dict[str, Any] = {}
|
||||||
|
self._entry: ConfigEntry | None = None
|
||||||
|
|
||||||
async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
|
async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
|
||||||
"""Reusable Auth Helper."""
|
"""Reusable Auth Helper."""
|
||||||
fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await fyta.login()
|
self.credentials = await fyta.login()
|
||||||
except FytaConnectionError:
|
except FytaConnectionError:
|
||||||
return {"base": "cannot_connect"}
|
return {"base": "cannot_connect"}
|
||||||
except FytaAuthentificationError:
|
except FytaAuthentificationError:
|
||||||
@@ -51,6 +56,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
finally:
|
finally:
|
||||||
await fyta.client.close()
|
await fyta.client.close()
|
||||||
|
|
||||||
|
self.credentials[CONF_EXPIRATION] = self.credentials[
|
||||||
|
CONF_EXPIRATION
|
||||||
|
].isoformat()
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
@@ -62,6 +71,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
|
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
|
||||||
|
|
||||||
if not (errors := await self.async_auth(user_input)):
|
if not (errors := await self.async_auth(user_input)):
|
||||||
|
user_input |= self.credentials
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_USERNAME], data=user_input
|
title=user_input[CONF_USERNAME], data=user_input
|
||||||
)
|
)
|
||||||
@@ -85,6 +95,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
assert self._entry is not None
|
assert self._entry is not None
|
||||||
|
|
||||||
if user_input and not (errors := await self.async_auth(user_input)):
|
if user_input and not (errors := await self.async_auth(user_input)):
|
||||||
|
user_input |= self.credentials
|
||||||
return self.async_update_reload_and_abort(
|
return self.async_update_reload_and_abort(
|
||||||
self._entry, data={**self._entry.data, **user_input}
|
self._entry, data={**self._entry.data, **user_input}
|
||||||
)
|
)
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
"""Const for fyta integration."""
|
"""Const for fyta integration."""
|
||||||
|
|
||||||
DOMAIN = "fyta"
|
DOMAIN = "fyta"
|
||||||
|
CONF_EXPIRATION = "expiration"
|
||||||
|
@@ -12,10 +12,13 @@ from fyta_cli.fyta_exceptions import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import CONF_EXPIRATION
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -39,17 +42,33 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
|
|||||||
) -> dict[int, dict[str, Any]]:
|
) -> dict[int, dict[str, Any]]:
|
||||||
"""Fetch data from API endpoint."""
|
"""Fetch data from API endpoint."""
|
||||||
|
|
||||||
if self.fyta.expiration is None or self.fyta.expiration < datetime.now():
|
if (
|
||||||
|
self.fyta.expiration is None
|
||||||
|
or self.fyta.expiration.timestamp() < datetime.now().timestamp()
|
||||||
|
):
|
||||||
await self.renew_authentication()
|
await self.renew_authentication()
|
||||||
|
|
||||||
return await self.fyta.update_all_plants()
|
return await self.fyta.update_all_plants()
|
||||||
|
|
||||||
async def renew_authentication(self) -> None:
|
async def renew_authentication(self) -> bool:
|
||||||
"""Renew access token for FYTA API."""
|
"""Renew access token for FYTA API."""
|
||||||
|
credentials: dict[str, Any] = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.fyta.login()
|
credentials = await self.fyta.login()
|
||||||
except FytaConnectionError as ex:
|
except FytaConnectionError as ex:
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
except (FytaAuthentificationError, FytaPasswordError) as ex:
|
except (FytaAuthentificationError, FytaPasswordError) as ex:
|
||||||
raise ConfigEntryAuthFailed from ex
|
raise ConfigEntryAuthFailed from ex
|
||||||
|
|
||||||
|
new_config_entry = {**self.config_entry.data}
|
||||||
|
new_config_entry[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN]
|
||||||
|
new_config_entry[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat()
|
||||||
|
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self.config_entry, data=new_config_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Credentials successfully updated")
|
||||||
|
|
||||||
|
return True
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/fyta",
|
"documentation": "https://www.home-assistant.io/integrations/fyta",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["fyta_cli==0.3.5"]
|
"requirements": ["fyta_cli==0.4.1"]
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from fyta_cli.fyta_connector import PLANT_STATUS
|
from fyta_cli.fyta_connector import PLANT_MEASUREMENT_STATUS, PLANT_STATUS
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@@ -34,7 +34,15 @@ class FytaSensorEntityDescription(SensorEntityDescription):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
PLANT_STATUS_LIST: list[str] = ["too_low", "low", "perfect", "high", "too_high"]
|
PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"]
|
||||||
|
PLANT_MEASUREMENT_STATUS_LIST: list[str] = [
|
||||||
|
"no_data",
|
||||||
|
"too_low",
|
||||||
|
"low",
|
||||||
|
"perfect",
|
||||||
|
"high",
|
||||||
|
"too_high",
|
||||||
|
]
|
||||||
|
|
||||||
SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
||||||
FytaSensorEntityDescription(
|
FytaSensorEntityDescription(
|
||||||
@@ -52,29 +60,29 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
|||||||
key="temperature_status",
|
key="temperature_status",
|
||||||
translation_key="temperature_status",
|
translation_key="temperature_status",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=PLANT_STATUS_LIST,
|
options=PLANT_MEASUREMENT_STATUS_LIST,
|
||||||
value_fn=PLANT_STATUS.get,
|
value_fn=PLANT_MEASUREMENT_STATUS.get,
|
||||||
),
|
),
|
||||||
FytaSensorEntityDescription(
|
FytaSensorEntityDescription(
|
||||||
key="light_status",
|
key="light_status",
|
||||||
translation_key="light_status",
|
translation_key="light_status",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=PLANT_STATUS_LIST,
|
options=PLANT_MEASUREMENT_STATUS_LIST,
|
||||||
value_fn=PLANT_STATUS.get,
|
value_fn=PLANT_MEASUREMENT_STATUS.get,
|
||||||
),
|
),
|
||||||
FytaSensorEntityDescription(
|
FytaSensorEntityDescription(
|
||||||
key="moisture_status",
|
key="moisture_status",
|
||||||
translation_key="moisture_status",
|
translation_key="moisture_status",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=PLANT_STATUS_LIST,
|
options=PLANT_MEASUREMENT_STATUS_LIST,
|
||||||
value_fn=PLANT_STATUS.get,
|
value_fn=PLANT_MEASUREMENT_STATUS.get,
|
||||||
),
|
),
|
||||||
FytaSensorEntityDescription(
|
FytaSensorEntityDescription(
|
||||||
key="salinity_status",
|
key="salinity_status",
|
||||||
translation_key="salinity_status",
|
translation_key="salinity_status",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=PLANT_STATUS_LIST,
|
options=PLANT_MEASUREMENT_STATUS_LIST,
|
||||||
value_fn=PLANT_STATUS.get,
|
value_fn=PLANT_MEASUREMENT_STATUS.get,
|
||||||
),
|
),
|
||||||
FytaSensorEntityDescription(
|
FytaSensorEntityDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
|
@@ -36,6 +36,16 @@
|
|||||||
"plant_status": {
|
"plant_status": {
|
||||||
"name": "Plant state",
|
"name": "Plant state",
|
||||||
"state": {
|
"state": {
|
||||||
|
"deleted": "Deleted",
|
||||||
|
"doing_great": "Doing great",
|
||||||
|
"need_attention": "Needs attention",
|
||||||
|
"no_sensor": "No sensor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temperature_status": {
|
||||||
|
"name": "Temperature state",
|
||||||
|
"state": {
|
||||||
|
"no_data": "No data",
|
||||||
"too_low": "Too low",
|
"too_low": "Too low",
|
||||||
"low": "Low",
|
"low": "Low",
|
||||||
"perfect": "Perfect",
|
"perfect": "Perfect",
|
||||||
@@ -43,44 +53,37 @@
|
|||||||
"too_high": "Too high"
|
"too_high": "Too high"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"temperature_status": {
|
|
||||||
"name": "Temperature state",
|
|
||||||
"state": {
|
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
|
|
||||||
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
|
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
|
|
||||||
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
|
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"light_status": {
|
"light_status": {
|
||||||
"name": "Light state",
|
"name": "Light state",
|
||||||
"state": {
|
"state": {
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
|
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
|
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||||
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moisture_status": {
|
"moisture_status": {
|
||||||
"name": "Moisture state",
|
"name": "Moisture state",
|
||||||
"state": {
|
"state": {
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
|
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
|
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||||
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"salinity_status": {
|
"salinity_status": {
|
||||||
"name": "Salinity state",
|
"name": "Salinity state",
|
||||||
"state": {
|
"state": {
|
||||||
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
|
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||||
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
|
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||||
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
|
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||||
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
|
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||||
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
|
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||||
|
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"light": {
|
"light": {
|
||||||
|
@@ -412,7 +412,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity):
|
|||||||
else:
|
else:
|
||||||
self._attr_action = HumidifierAction.IDLE
|
self._attr_action = HumidifierAction.IDLE
|
||||||
|
|
||||||
self.async_schedule_update_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def _async_update_humidity(self, humidity: str) -> None:
|
async def _async_update_humidity(self, humidity: str) -> None:
|
||||||
"""Update hygrostat with latest state from sensor."""
|
"""Update hygrostat with latest state from sensor."""
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
@@ -25,8 +26,17 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
|
GiosConfigEntry = ConfigEntry["GiosData"]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
|
@dataclass
|
||||||
|
class GiosData:
|
||||||
|
"""Data for GIOS integration."""
|
||||||
|
|
||||||
|
coordinator: GiosDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: GiosConfigEntry) -> bool:
|
||||||
"""Set up GIOS as config entry."""
|
"""Set up GIOS as config entry."""
|
||||||
station_id: int = entry.data[CONF_STATION_ID]
|
station_id: int = entry.data[CONF_STATION_ID]
|
||||||
_LOGGER.debug("Using station_id: %d", station_id)
|
_LOGGER.debug("Using station_id: %d", station_id)
|
||||||
@@ -48,8 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
coordinator = GiosDataUpdateCoordinator(hass, websession, station_id)
|
coordinator = GiosDataUpdateCoordinator(hass, websession, station_id)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
entry.runtime_data = GiosData(coordinator)
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@@ -65,14 +74,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: GiosConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]): # pylint: disable=hass-enforce-coordinator-module
|
class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]): # pylint: disable=hass-enforce-coordinator-module
|
||||||
|
@@ -5,18 +5,16 @@ from __future__ import annotations
|
|||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import GiosDataUpdateCoordinator
|
from . import GiosConfigEntry
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: GiosConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
coordinator: GiosDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator = config_entry.runtime_data.coordinator
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"config_entry": config_entry.as_dict(),
|
"config_entry": config_entry.as_dict(),
|
||||||
|
@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME
|
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@@ -24,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import GiosDataUpdateCoordinator
|
from . import GiosConfigEntry, GiosDataUpdateCoordinator
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_AQI,
|
ATTR_AQI,
|
||||||
ATTR_C6H6,
|
ATTR_C6H6,
|
||||||
@@ -159,13 +158,12 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: GiosConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a GIOS entities from a config_entry."""
|
"""Add a GIOS entities from a config_entry."""
|
||||||
name = entry.data[CONF_NAME]
|
name = entry.data[CONF_NAME]
|
||||||
|
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data.coordinator
|
||||||
|
|
||||||
# Due to the change of the attribute name of one sensor, it is necessary to migrate
|
# Due to the change of the attribute name of one sensor, it is necessary to migrate
|
||||||
# the unique_id to the new name.
|
# the unique_id to the new name.
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from contextvars import ContextVar
|
from typing import TYPE_CHECKING, Protocol
|
||||||
from typing import Protocol
|
|
||||||
|
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@@ -13,12 +12,13 @@ from homeassistant.helpers.integration_platform import (
|
|||||||
|
|
||||||
from .const import DOMAIN, REG_KEY
|
from .const import DOMAIN, REG_KEY
|
||||||
|
|
||||||
current_domain: ContextVar[str] = ContextVar("current_domain")
|
if TYPE_CHECKING:
|
||||||
|
from .entity import Group
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant) -> None:
|
async def async_setup(hass: HomeAssistant) -> None:
|
||||||
"""Set up the Group integration registry of integration platforms."""
|
"""Set up the Group integration registry of integration platforms."""
|
||||||
hass.data[REG_KEY] = GroupIntegrationRegistry()
|
hass.data[REG_KEY] = GroupIntegrationRegistry(hass)
|
||||||
|
|
||||||
await async_process_integration_platforms(
|
await async_process_integration_platforms(
|
||||||
hass, DOMAIN, _process_group_platform, wait_for_platforms=True
|
hass, DOMAIN, _process_group_platform, wait_for_platforms=True
|
||||||
@@ -39,7 +39,6 @@ def _process_group_platform(
|
|||||||
hass: HomeAssistant, domain: str, platform: GroupProtocol
|
hass: HomeAssistant, domain: str, platform: GroupProtocol
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Process a group platform."""
|
"""Process a group platform."""
|
||||||
current_domain.set(domain)
|
|
||||||
registry: GroupIntegrationRegistry = hass.data[REG_KEY]
|
registry: GroupIntegrationRegistry = hass.data[REG_KEY]
|
||||||
platform.async_describe_on_off_states(hass, registry)
|
platform.async_describe_on_off_states(hass, registry)
|
||||||
|
|
||||||
@@ -47,24 +46,31 @@ def _process_group_platform(
|
|||||||
class GroupIntegrationRegistry:
|
class GroupIntegrationRegistry:
|
||||||
"""Class to hold a registry of integrations."""
|
"""Class to hold a registry of integrations."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Imitialize registry."""
|
"""Imitialize registry."""
|
||||||
|
self.hass = hass
|
||||||
self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF}
|
self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF}
|
||||||
self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON}
|
self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON}
|
||||||
self.on_states_by_domain: dict[str, set[str]] = {}
|
self.on_states_by_domain: dict[str, set[str]] = {}
|
||||||
self.exclude_domains: set[str] = set()
|
self.exclude_domains: set[str] = set()
|
||||||
|
self.state_group_mapping: dict[str, tuple[str, str]] = {}
|
||||||
|
self.group_entities: set[Group] = set()
|
||||||
|
|
||||||
def exclude_domain(self) -> None:
|
@callback
|
||||||
|
def exclude_domain(self, domain: str) -> None:
|
||||||
"""Exclude the current domain."""
|
"""Exclude the current domain."""
|
||||||
self.exclude_domains.add(current_domain.get())
|
self.exclude_domains.add(domain)
|
||||||
|
|
||||||
def on_off_states(self, on_states: set, off_state: str) -> None:
|
@callback
|
||||||
|
def on_off_states(
|
||||||
|
self, domain: str, on_states: set[str], default_on_state: str, off_state: str
|
||||||
|
) -> None:
|
||||||
"""Register on and off states for the current domain."""
|
"""Register on and off states for the current domain."""
|
||||||
for on_state in on_states:
|
for on_state in on_states:
|
||||||
if on_state not in self.on_off_mapping:
|
if on_state not in self.on_off_mapping:
|
||||||
self.on_off_mapping[on_state] = off_state
|
self.on_off_mapping[on_state] = off_state
|
||||||
|
|
||||||
if len(on_states) == 1 and off_state not in self.off_on_mapping:
|
if len(on_states) == 1 and off_state not in self.off_on_mapping:
|
||||||
self.off_on_mapping[off_state] = list(on_states)[0]
|
self.off_on_mapping[off_state] = default_on_state
|
||||||
|
|
||||||
self.on_states_by_domain[current_domain.get()] = set(on_states)
|
self.on_states_by_domain[domain] = on_states
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
|
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["growattServer"],
|
"loggers": ["growattServer"],
|
||||||
"requirements": ["growattServer==1.3.0"]
|
"requirements": ["growattServer==1.5.0"]
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import SensorEntity
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle, dt as dt_util
|
from homeassistant.util import Throttle, dt as dt_util
|
||||||
@@ -46,8 +47,7 @@ def get_device_list(api, config):
|
|||||||
not login_response["success"]
|
not login_response["success"]
|
||||||
and login_response["msg"] == LOGIN_INVALID_AUTH_CODE
|
and login_response["msg"] == LOGIN_INVALID_AUTH_CODE
|
||||||
):
|
):
|
||||||
_LOGGER.error("Username, Password or URL may be incorrect!")
|
raise ConfigEntryError("Username, Password or URL may be incorrect!")
|
||||||
return
|
|
||||||
user_id = login_response["user"]["id"]
|
user_id = login_response["user"]["id"]
|
||||||
if plant_id == DEFAULT_PLANT_ID:
|
if plant_id == DEFAULT_PLANT_ID:
|
||||||
plant_info = api.plant_list(user_id)
|
plant_info = api.plant_list(user_id)
|
||||||
|
@@ -30,10 +30,11 @@ from .const import (
|
|||||||
EVENT_API_CALL_SUCCESS,
|
EVENT_API_CALL_SUCCESS,
|
||||||
SERVICE_API_CALL,
|
SERVICE_API_CALL,
|
||||||
)
|
)
|
||||||
from .sensor import SENSORS_TYPES
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"]
|
||||||
|
|
||||||
INSTANCE_SCHEMA = vol.All(
|
INSTANCE_SCHEMA = vol.All(
|
||||||
cv.deprecated(CONF_SENSORS),
|
cv.deprecated(CONF_SENSORS),
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
|
@@ -10,9 +10,10 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
from homeassistant.config_entries import ConfigFlow
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
|
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
|
|
||||||
from .const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
from .const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
||||||
|
|
||||||
@@ -79,6 +80,20 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_import(self, import_data):
|
async def async_step_import(self, import_data):
|
||||||
"""Import habitica config from configuration.yaml."""
|
"""Import habitica config from configuration.yaml."""
|
||||||
|
|
||||||
|
async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
HOMEASSISTANT_DOMAIN,
|
||||||
|
f"deprecated_yaml_{DOMAIN}",
|
||||||
|
is_fixable=False,
|
||||||
|
breaks_in_ha_version="2024.11.0",
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml",
|
||||||
|
translation_placeholders={
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"integration_title": "Habitica",
|
||||||
|
},
|
||||||
|
)
|
||||||
return await self.async_step_user(import_data)
|
return await self.async_step_user(import_data)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -15,3 +15,6 @@ ATTR_ARGS = "args"
|
|||||||
# event constants
|
# event constants
|
||||||
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
|
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
|
||||||
ATTR_DATA = "data"
|
ATTR_DATA = "data"
|
||||||
|
|
||||||
|
MANUFACTURER = "HabitRPG, Inc."
|
||||||
|
NAME = "Habitica"
|
||||||
|
@@ -1,4 +1,50 @@
|
|||||||
{
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"display_name": {
|
||||||
|
"default": "mdi:account-circle"
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"default": "mdi:heart",
|
||||||
|
"state": {
|
||||||
|
"0": "mdi:skull-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"health_max": {
|
||||||
|
"default": "mdi:heart"
|
||||||
|
},
|
||||||
|
"mana": {
|
||||||
|
"default": "mdi:flask",
|
||||||
|
"state": {
|
||||||
|
"0": "mdi:flask-empty-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mana_max": {
|
||||||
|
"default": "mdi:flask"
|
||||||
|
},
|
||||||
|
"experience": {
|
||||||
|
"default": "mdi:star-four-points"
|
||||||
|
},
|
||||||
|
"experience_max": {
|
||||||
|
"default": "mdi:star-four-points"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"default": "mdi:crown-circle"
|
||||||
|
},
|
||||||
|
"gold": {
|
||||||
|
"default": "mdi:sack"
|
||||||
|
},
|
||||||
|
"class": {
|
||||||
|
"default": "mdi:sword",
|
||||||
|
"state": {
|
||||||
|
"warrior": "mdi:sword",
|
||||||
|
"healer": "mdi:shield",
|
||||||
|
"wizard": "mdi:wizard-hat",
|
||||||
|
"rogue": "mdi:ninja"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"api_call": "mdi:console"
|
"api_call": "mdi:console"
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "habitica",
|
"domain": "habitica",
|
||||||
"name": "Habitica",
|
"name": "Habitica",
|
||||||
"codeowners": ["@ASMfreaK", "@leikoilja"],
|
"codeowners": ["@ASMfreaK", "@leikoilja", "@tr4nt0r"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
|
@@ -3,42 +3,123 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from enum import StrEnum
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME, CONF_URL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, MANUFACTURER, NAME
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||||
|
|
||||||
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
|
|
||||||
|
|
||||||
SENSORS_TYPES = {
|
@dataclass(kw_only=True, frozen=True)
|
||||||
"name": SensorType("Name", None, None, ["profile", "name"]),
|
class HabitipySensorEntityDescription(SensorEntityDescription):
|
||||||
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]),
|
"""Habitipy Sensor Description."""
|
||||||
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
|
|
||||||
"mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
|
value_path: list[str]
|
||||||
"maxMP": SensorType("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]),
|
|
||||||
"exp": SensorType("EXP", "mdi:star", "EXP", ["stats", "exp"]),
|
|
||||||
"toNextLevel": SensorType("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]),
|
class HabitipySensorEntity(StrEnum):
|
||||||
"lvl": SensorType(
|
"""Habitipy Entities."""
|
||||||
"Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]
|
|
||||||
|
DISPLAY_NAME = "display_name"
|
||||||
|
HEALTH = "health"
|
||||||
|
HEALTH_MAX = "health_max"
|
||||||
|
MANA = "mana"
|
||||||
|
MANA_MAX = "mana_max"
|
||||||
|
EXPERIENCE = "experience"
|
||||||
|
EXPERIENCE_MAX = "experience_max"
|
||||||
|
LEVEL = "level"
|
||||||
|
GOLD = "gold"
|
||||||
|
CLASS = "class"
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTIONS: dict[str, HabitipySensorEntityDescription] = {
|
||||||
|
HabitipySensorEntity.DISPLAY_NAME: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.DISPLAY_NAME,
|
||||||
|
translation_key=HabitipySensorEntity.DISPLAY_NAME,
|
||||||
|
value_path=["profile", "name"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.HEALTH: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.HEALTH,
|
||||||
|
translation_key=HabitipySensorEntity.HEALTH,
|
||||||
|
native_unit_of_measurement="HP",
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_path=["stats", "hp"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.HEALTH_MAX: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.HEALTH_MAX,
|
||||||
|
translation_key=HabitipySensorEntity.HEALTH_MAX,
|
||||||
|
native_unit_of_measurement="HP",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_path=["stats", "maxHealth"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.MANA: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.MANA,
|
||||||
|
translation_key=HabitipySensorEntity.MANA,
|
||||||
|
native_unit_of_measurement="MP",
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_path=["stats", "mp"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.MANA_MAX: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.MANA_MAX,
|
||||||
|
translation_key=HabitipySensorEntity.MANA_MAX,
|
||||||
|
native_unit_of_measurement="MP",
|
||||||
|
value_path=["stats", "maxMP"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.EXPERIENCE: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.EXPERIENCE,
|
||||||
|
translation_key=HabitipySensorEntity.EXPERIENCE,
|
||||||
|
native_unit_of_measurement="XP",
|
||||||
|
value_path=["stats", "exp"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.EXPERIENCE_MAX: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.EXPERIENCE_MAX,
|
||||||
|
translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
|
||||||
|
native_unit_of_measurement="XP",
|
||||||
|
value_path=["stats", "toNextLevel"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.LEVEL: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.LEVEL,
|
||||||
|
translation_key=HabitipySensorEntity.LEVEL,
|
||||||
|
value_path=["stats", "lvl"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.GOLD: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.GOLD,
|
||||||
|
translation_key=HabitipySensorEntity.GOLD,
|
||||||
|
native_unit_of_measurement="GP",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
value_path=["stats", "gp"],
|
||||||
|
),
|
||||||
|
HabitipySensorEntity.CLASS: HabitipySensorEntityDescription(
|
||||||
|
key=HabitipySensorEntity.CLASS,
|
||||||
|
translation_key=HabitipySensorEntity.CLASS,
|
||||||
|
value_path=["stats", "class"],
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=["warrior", "healer", "wizard", "rogue"],
|
||||||
),
|
),
|
||||||
"gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]),
|
|
||||||
"class": SensorType("Class", "mdi:sword", None, ["stats", "class"]),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
|
||||||
TASKS_TYPES = {
|
TASKS_TYPES = {
|
||||||
"habits": SensorType(
|
"habits": SensorType(
|
||||||
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]
|
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]
|
||||||
@@ -92,10 +173,12 @@ async def async_setup_entry(
|
|||||||
await sensor_data.update()
|
await sensor_data.update()
|
||||||
|
|
||||||
entities: list[SensorEntity] = [
|
entities: list[SensorEntity] = [
|
||||||
HabitipySensor(name, sensor_type, sensor_data) for sensor_type in SENSORS_TYPES
|
HabitipySensor(sensor_data, description, config_entry)
|
||||||
|
for description in SENSOR_DESCRIPTIONS.values()
|
||||||
]
|
]
|
||||||
entities.extend(
|
entities.extend(
|
||||||
HabitipyTaskSensor(name, task_type, sensor_data) for task_type in TASKS_TYPES
|
HabitipyTaskSensor(name, task_type, sensor_data, config_entry)
|
||||||
|
for task_type in TASKS_TYPES
|
||||||
)
|
)
|
||||||
async_add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
@@ -103,7 +186,9 @@ async def async_setup_entry(
|
|||||||
class HabitipyData:
|
class HabitipyData:
|
||||||
"""Habitica API user data cache."""
|
"""Habitica API user data cache."""
|
||||||
|
|
||||||
def __init__(self, api):
|
tasks: dict[str, Any]
|
||||||
|
|
||||||
|
def __init__(self, api) -> None:
|
||||||
"""Habitica API user data cache."""
|
"""Habitica API user data cache."""
|
||||||
self.api = api
|
self.api = api
|
||||||
self.data = None
|
self.data = None
|
||||||
@@ -153,53 +238,59 @@ class HabitipyData:
|
|||||||
class HabitipySensor(SensorEntity):
|
class HabitipySensor(SensorEntity):
|
||||||
"""A generic Habitica sensor."""
|
"""A generic Habitica sensor."""
|
||||||
|
|
||||||
def __init__(self, name, sensor_name, updater):
|
_attr_has_entity_name = True
|
||||||
|
entity_description: HabitipySensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator,
|
||||||
|
entity_description: HabitipySensorEntityDescription,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
"""Initialize a generic Habitica sensor."""
|
"""Initialize a generic Habitica sensor."""
|
||||||
self._name = name
|
super().__init__()
|
||||||
self._sensor_name = sensor_name
|
if TYPE_CHECKING:
|
||||||
self._sensor_type = SENSORS_TYPES[sensor_name]
|
assert entry.unique_id
|
||||||
self._state = None
|
self.coordinator = coordinator
|
||||||
self._updater = updater
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = f"{entry.unique_id}_{entity_description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
model=NAME,
|
||||||
|
name=entry.data[CONF_NAME],
|
||||||
|
configuration_url=entry.data[CONF_URL],
|
||||||
|
identifiers={(DOMAIN, entry.unique_id)},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update Condition and Forecast."""
|
"""Update Sensor state."""
|
||||||
await self._updater.update()
|
await self.coordinator.update()
|
||||||
data = self._updater.data
|
data = self.coordinator.data
|
||||||
for element in self._sensor_type.path:
|
for element in self.entity_description.value_path:
|
||||||
data = data[element]
|
data = data[element]
|
||||||
self._state = data
|
self._attr_native_value = data
|
||||||
|
|
||||||
@property
|
|
||||||
def icon(self):
|
|
||||||
"""Return the icon to use in the frontend, if any."""
|
|
||||||
return self._sensor_type.icon
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Return the name of the sensor."""
|
|
||||||
return f"{DOMAIN}_{self._name}_{self._sensor_name}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self):
|
|
||||||
"""Return the state of the device."""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_unit_of_measurement(self):
|
|
||||||
"""Return the unit the value is expressed in."""
|
|
||||||
return self._sensor_type.unit
|
|
||||||
|
|
||||||
|
|
||||||
class HabitipyTaskSensor(SensorEntity):
|
class HabitipyTaskSensor(SensorEntity):
|
||||||
"""A Habitica task sensor."""
|
"""A Habitica task sensor."""
|
||||||
|
|
||||||
def __init__(self, name, task_name, updater):
|
def __init__(self, name, task_name, updater, entry):
|
||||||
"""Initialize a generic Habitica task."""
|
"""Initialize a generic Habitica task."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._task_name = task_name
|
self._task_name = task_name
|
||||||
self._task_type = TASKS_TYPES[task_name]
|
self._task_type = TASKS_TYPES[task_name]
|
||||||
self._state = None
|
self._state = None
|
||||||
self._updater = updater
|
self._updater = updater
|
||||||
|
self._attr_unique_id = f"{entry.unique_id}_{task_name}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
model=NAME,
|
||||||
|
name=entry.data[CONF_NAME],
|
||||||
|
configuration_url=entry.data[CONF_URL],
|
||||||
|
identifiers={(DOMAIN, entry.unique_id)},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update Condition and Forecast."""
|
"""Update Condition and Forecast."""
|
||||||
|
@@ -19,6 +19,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"display_name": {
|
||||||
|
"name": "Display name"
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"name": "Health"
|
||||||
|
},
|
||||||
|
"health_max": {
|
||||||
|
"name": "Max. health"
|
||||||
|
},
|
||||||
|
"mana": {
|
||||||
|
"name": "Mana"
|
||||||
|
},
|
||||||
|
"mana_max": {
|
||||||
|
"name": "Max. mana"
|
||||||
|
},
|
||||||
|
"experience": {
|
||||||
|
"name": "Experience"
|
||||||
|
},
|
||||||
|
"experience_max": {
|
||||||
|
"name": "Next level"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "Level"
|
||||||
|
},
|
||||||
|
"gold": {
|
||||||
|
"name": "Gold"
|
||||||
|
},
|
||||||
|
"class": {
|
||||||
|
"name": "Class",
|
||||||
|
"state": {
|
||||||
|
"warrior": "Warrior",
|
||||||
|
"healer": "Healer",
|
||||||
|
"wizard": "Mage",
|
||||||
|
"rogue": "Rogue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"api_call": {
|
"api_call": {
|
||||||
"name": "API name",
|
"name": "API name",
|
||||||
|
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "harmony",
|
"domain": "harmony",
|
||||||
"name": "Logitech Harmony Hub",
|
"name": "Logitech Harmony Hub",
|
||||||
"codeowners": [
|
"codeowners": ["@ehendrix23", "@bdraco", "@mkeesey", "@Aohzan"],
|
||||||
"@ehendrix23",
|
|
||||||
"@bramkragten",
|
|
||||||
"@bdraco",
|
|
||||||
"@mkeesey",
|
|
||||||
"@Aohzan"
|
|
||||||
],
|
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["remote", "switch"],
|
"dependencies": ["remote", "switch"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/harmony",
|
"documentation": "https://www.home-assistant.io/integrations/harmony",
|
||||||
|
@@ -67,15 +67,15 @@ class HassIOIngress(HomeAssistantView):
|
|||||||
"""Initialize a Hass.io ingress view."""
|
"""Initialize a Hass.io ingress view."""
|
||||||
self._host = host
|
self._host = host
|
||||||
self._websession = websession
|
self._websession = websession
|
||||||
|
self._url = URL(f"http://{host}")
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def _create_url(self, token: str, path: str) -> URL:
|
def _create_url(self, token: str, path: str) -> URL:
|
||||||
"""Create URL to service."""
|
"""Create URL to service."""
|
||||||
base_path = f"/ingress/{token}/"
|
base_path = f"/ingress/{token}/"
|
||||||
url = f"http://{self._host}{base_path}{quote(path)}"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target_url = URL(url)
|
target_url = self._url.join(URL(f"{base_path}{quote(path)}"))
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise HTTPBadRequest from err
|
raise HTTPBadRequest from err
|
||||||
|
|
||||||
@@ -177,11 +177,13 @@ class HassIOIngress(HomeAssistantView):
|
|||||||
if maybe_content_type := result.headers.get(hdrs.CONTENT_TYPE):
|
if maybe_content_type := result.headers.get(hdrs.CONTENT_TYPE):
|
||||||
content_type: str = (maybe_content_type.partition(";"))[0].strip()
|
content_type: str = (maybe_content_type.partition(";"))[0].strip()
|
||||||
else:
|
else:
|
||||||
content_type = result.content_type
|
# default value according to RFC 2616
|
||||||
|
content_type = "application/octet-stream"
|
||||||
|
|
||||||
# Simple request
|
# Simple request
|
||||||
if result.status in (204, 304) or (
|
if result.status in (204, 304) or (
|
||||||
content_length is not UNDEFINED
|
content_length is not UNDEFINED
|
||||||
and (content_length_int := int(content_length or 0))
|
and (content_length_int := int(content_length))
|
||||||
<= MAX_SIMPLE_RESPONSE_SIZE
|
<= MAX_SIMPLE_RESPONSE_SIZE
|
||||||
):
|
):
|
||||||
# Return Response
|
# Return Response
|
||||||
@@ -194,17 +196,17 @@ class HassIOIngress(HomeAssistantView):
|
|||||||
zlib_executor_size=32768,
|
zlib_executor_size=32768,
|
||||||
)
|
)
|
||||||
if content_length_int > MIN_COMPRESSED_SIZE and should_compress(
|
if content_length_int > MIN_COMPRESSED_SIZE and should_compress(
|
||||||
content_type or simple_response.content_type
|
content_type
|
||||||
):
|
):
|
||||||
simple_response.enable_compression()
|
simple_response.enable_compression()
|
||||||
return simple_response
|
return simple_response
|
||||||
|
|
||||||
# Stream response
|
# Stream response
|
||||||
response = web.StreamResponse(status=result.status, headers=headers)
|
response = web.StreamResponse(status=result.status, headers=headers)
|
||||||
response.content_type = result.content_type
|
response.content_type = content_type
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if should_compress(response.content_type):
|
if should_compress(content_type):
|
||||||
response.enable_compression()
|
response.enable_compression()
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
# In testing iter_chunked, iter_any, and iter_chunks:
|
# In testing iter_chunked, iter_any, and iter_chunks:
|
||||||
|
@@ -87,7 +87,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
if not coordinator.last_update_success:
|
if not coordinator.last_update_success:
|
||||||
return
|
return
|
||||||
|
|
||||||
hass.async_create_task(async_update_alerts(), eager_start=True)
|
hass.async_create_background_task(
|
||||||
|
async_update_alerts(), "homeassistant_alerts update", eager_start=True
|
||||||
|
)
|
||||||
|
|
||||||
coordinator = AlertUpdateCoordinator(hass)
|
coordinator = AlertUpdateCoordinator(hass)
|
||||||
coordinator.async_add_listener(async_schedule_update_alerts)
|
coordinator.async_add_listener(async_schedule_update_alerts)
|
||||||
@@ -99,6 +101,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
cooldown=COMPONENT_LOADED_COOLDOWN,
|
cooldown=COMPONENT_LOADED_COOLDOWN,
|
||||||
immediate=False,
|
immediate=False,
|
||||||
function=coordinator.async_refresh,
|
function=coordinator.async_refresh,
|
||||||
|
background=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@@ -597,6 +597,21 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
|
|||||||
"""Return the name of the hardware."""
|
"""Return the name of the hardware."""
|
||||||
return self._hw_variant.full_name
|
return self._hw_variant.full_name
|
||||||
|
|
||||||
|
async def async_step_flashing_complete(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Finish flashing and update the config entry."""
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
entry=self.config_entry,
|
||||||
|
data={
|
||||||
|
**self.config_entry.data,
|
||||||
|
"firmware": ApplicationType.EZSP.value,
|
||||||
|
},
|
||||||
|
options=self.config_entry.options,
|
||||||
|
)
|
||||||
|
|
||||||
|
return await super().async_step_flashing_complete(user_input)
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantSkyConnectOptionsFlowHandler(
|
class HomeAssistantSkyConnectOptionsFlowHandler(
|
||||||
BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry
|
BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user