Merge branch 'dev' into pglab

This commit is contained in:
pglab-electronics
2024-05-02 12:37:03 +02:00
committed by GitHub
429 changed files with 10032 additions and 3924 deletions

View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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"

View File

@@ -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.*

View File

@@ -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

View File

@@ -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

View File

@@ -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/

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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)

View File

@@ -0,0 +1,5 @@
"""Constants for the air_quality entity platform."""
from typing import Final
DOMAIN: Final = "air_quality"

View File

@@ -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)

View File

@@ -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__(

View File

@@ -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

View File

@@ -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,
) )

View File

@@ -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)

View File

@@ -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

View File

@@ -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
) )

View File

@@ -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

View File

@@ -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)

View File

@@ -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"]
} }

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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"]
} }

View File

@@ -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."""

View File

@@ -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

View File

@@ -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,
) )

View File

@@ -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,
} }

View File

@@ -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()

View File

@@ -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 + ".{}"

View File

@@ -0,0 +1,3 @@
"""Constants for cover entity platform."""
DOMAIN = "cover"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
)
) )

View File

@@ -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
) )

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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)
) )

View File

@@ -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

View File

@@ -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)
] ]

View File

@@ -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
) )

View File

@@ -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
) )

View File

@@ -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

View File

@@ -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
) )

View File

@@ -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)
] ]

View File

@@ -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"]
} }

View File

@@ -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}

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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"
}
} }
} }

View File

@@ -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 (

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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:

View File

@@ -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),

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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"]
} }

View File

@@ -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

View File

@@ -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}
) )

View File

@@ -1,3 +1,4 @@
"""Const for fyta integration.""" """Const for fyta integration."""
DOMAIN = "fyta" DOMAIN = "fyta"
CONF_EXPIRATION = "expiration"

View File

@@ -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

View File

@@ -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"]
} }

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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."""

View File

@@ -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

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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

View File

@@ -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"]
} }

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"
} }

View File

@@ -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",

View File

@@ -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."""

View File

@@ -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",

View File

@@ -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",

View File

@@ -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:

View File

@@ -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

View File

@@ -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