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
- name: Install Cosign
uses: sigstore/cosign-installer@v3.4.0
uses: sigstore/cosign-installer@v3.5.0
with:
cosign-release: "v2.2.3"

View File

@@ -92,8 +92,10 @@ jobs:
uses: actions/checkout@v4.1.4
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: >-
echo "key=venv-${{ env.CACHE_VERSION }}-${{
run: |
# 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.txt') }}-${{
hashFiles('requirements_all.txt') }}-${{
@@ -1104,7 +1106,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v4.3.0
uses: codecov/codecov-action@v4.3.1
with:
fail_ci_if_error: true
flags: full-suite
@@ -1238,7 +1240,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v4.3.0
uses: codecov/codecov-action@v4.3.1
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -211,7 +211,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt"
@@ -226,7 +226,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
@@ -240,7 +240,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
@@ -254,7 +254,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"

View File

@@ -244,6 +244,7 @@ homeassistant.components.image.*
homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.input_text.*

View File

@@ -550,14 +550,14 @@ build.json @home-assistant/supervisor
/tests/components/group/ @home-assistant/core
/homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @ASMfreaK @leikoilja
/tests/components/habitica/ @ASMfreaK @leikoilja
/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
/homeassistant/components/hardkernel/ @home-assistant/core
/tests/components/hardkernel/ @home-assistant/core
/homeassistant/components/hardware/ @home-assistant/core
/tests/components/hardware/ @home-assistant/core
/homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
/tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
/homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
/homeassistant/components/hassio/ @home-assistant/supervisor
/tests/components/hassio/ @home-assistant/supervisor
/homeassistant/components/hdmi_cec/ @inytar
@@ -650,6 +650,8 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @zxdavb

View File

@@ -12,7 +12,7 @@ ENV \
ARG QEMU_CPU
# Install uv
RUN pip3 install uv==0.1.35
RUN pip3 install uv==0.1.39
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/>`__,
`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|
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
:target: https://demo.home-assistant.io
.. |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 .setup import (
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_notify_setup_error,
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
# loaded the sooner we can start loading the rest of the integrations.
futures = {
domain: hass.async_create_task(
domain: hass.async_create_task_internal(
async_setup_component(hass, domain, config),
f"setup component {domain}",
eager_start=True,
@@ -913,9 +917,7 @@ async def _async_set_up_integrations(
hass: core.HomeAssistant, config: dict[str, Any]
) -> None:
"""Set up all the integrations."""
setup_started: dict[tuple[str, str | None], float] = {}
hass.data[DATA_SETUP_STARTED] = setup_started
watcher = _WatchPendingSetups(hass, setup_started)
watcher = _WatchPendingSetups(hass, _setup_started(hass))
watcher.async_start()
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
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@@ -43,6 +43,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
AdGuardConfigEntry = ConfigEntry["AdGuardData"]
@dataclass
@@ -53,7 +54,7 @@ class AdGuardData:
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."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
adguard = AdGuardHome(
@@ -71,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except AdGuardHomeConnectionError as 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)
@@ -116,17 +117,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
loaded_entries = [
entry
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_REMOVE_URL)
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
del hass.data[DOMAIN]
return unload_ok

View File

@@ -4,11 +4,11 @@ from __future__ import annotations
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.entity import Entity
from . import AdGuardData
from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN, LOGGER
@@ -21,7 +21,7 @@ class AdGuardHomeEntity(Entity):
def __init__(
self,
data: AdGuardData,
entry: ConfigEntry,
entry: AdGuardConfigEntry,
) -> None:
"""Initialize the AdGuard Home entity."""
self._entry = entry

View File

@@ -10,12 +10,11 @@ from typing import Any
from adguardhome import AdGuardHome
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardData
from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN
from .entity import AdGuardHomeEntity
@@ -85,11 +84,11 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AdGuardConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
[AdGuardHomeSensor(data, entry, description) for description in SENSORS],
@@ -105,7 +104,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
def __init__(
self,
data: AdGuardData,
entry: ConfigEntry,
entry: AdGuardConfigEntry,
description: AdGuardHomeEntityDescription,
) -> None:
"""Initialize AdGuard Home sensor."""

View File

@@ -10,11 +10,10 @@ from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardData
from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN, LOGGER
from .entity import AdGuardHomeEntity
@@ -79,11 +78,11 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AdGuardConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES],
@@ -99,7 +98,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
def __init__(
self,
data: AdGuardData,
entry: ConfigEntry,
entry: AdGuardConfigEntry,
description: AdGuardHomeSwitchEntityDescription,
) -> None:
"""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 . import group as group_pre_import # noqa: F401
from .const import DOMAIN
_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_SO2: Final = "sulphur_dioxide"
DOMAIN: Final = "air_quality"
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
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:
from homeassistant.components.group import GroupIntegrationRegistry
from .const import DOMAIN
@callback
def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
) -> None:
"""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
class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
"""Define an object to hold Airly data."""
def __init__(

View File

@@ -225,7 +225,7 @@ class AirthingsSensor(
manufacturer=airthings_device.manufacturer,
hw_version=airthings_device.hw_version,
sw_version=airthings_device.sw_version,
model=airthings_device.model.name,
model=airthings_device.model.product_name,
)
@property

View File

@@ -10,9 +10,12 @@ from homeassistant.const import (
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_TRIGGERED,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry
@@ -23,7 +26,9 @@ def async_describe_on_off_states(
) -> None:
"""Describe group on off states."""
registry.on_off_states(
DOMAIN,
{
STATE_ON,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
@@ -31,5 +36,6 @@ def async_describe_on_off_states(
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_TRIGGERED,
},
STATE_ON,
STATE_OFF,
)

View File

@@ -15,10 +15,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
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
PLATFORMS: list[Platform] = [Platform.SENSOR]
AnalyticsInsightsConfigEntry = ConfigEntry["AnalyticsInsightsData"]
@dataclass(frozen=True)
@@ -29,7 +30,9 @@ class AnalyticsInsightsData:
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."""
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()
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)
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
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data.pop(DOMAIN)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def update_listener(
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING
from python_homeassistant_analytics import (
CustomIntegration,
@@ -12,7 +13,6 @@ from python_homeassistant_analytics import (
HomeassistantAnalyticsNotModifiedError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -23,6 +23,9 @@ from .const import (
LOGGER,
)
if TYPE_CHECKING:
from . import AnalyticsInsightsConfigEntry
@dataclass(frozen=True)
class AnalyticsData:
@@ -35,7 +38,7 @@ class AnalyticsData:
class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[AnalyticsData]):
"""A Homeassistant Analytics Data Update Coordinator."""
config_entry: ConfigEntry
config_entry: AnalyticsInsightsConfigEntry
def __init__(
self, hass: HomeAssistant, client: HomeassistantAnalyticsClient

View File

@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
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.update_coordinator import CoordinatorEntity
from . import AnalyticsInsightsData
from . import AnalyticsInsightsConfigEntry
from .const import DOMAIN
from .coordinator import AnalyticsData, HomeassistantAnalyticsDataUpdateCoordinator
@@ -60,12 +59,12 @@ def get_custom_integration_entity_description(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AnalyticsInsightsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize the entries."""
analytics_data: AnalyticsInsightsData = hass.data[DOMAIN]
analytics_data = entry.runtime_data
coordinator: HomeassistantAnalyticsDataUpdateCoordinator = (
analytics_data.coordinator
)

View File

@@ -42,6 +42,7 @@ UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
),
AsekoBinarySensorEntityDescription(
key="has_error",
translation_key="error",
value_fn=lambda unit: unit.has_error,
device_class=BinarySensorDeviceClass.PROBLEM,
),
@@ -78,7 +79,6 @@ class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
"""Initialize the unit binary sensor."""
super().__init__(unit, coordinator)
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}"
@property

View File

@@ -66,10 +66,12 @@ _ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]
def handle_errors_and_zip(
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."""
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."""
@functools.wraps(func)

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"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
def _async_scheduled_refresh(self, now: datetime) -> None:
"""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
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:
"""Boolean if volume is currently muted."""
if self._volume.muted and self._volume.muted.muted:
# The any return here is side effect of pydantic v2 compatibility
# This will be fixed in the future.
return self._volume.muted.muted # type: ignore[no-any-return]
return self._volume.muted.muted
return None
@property

View File

@@ -152,6 +152,7 @@ async def _async_start_adapter_discovery(
cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
immediate=False,
function=_async_rediscover_adapters,
background=True,
)
@hass_callback

View File

@@ -16,7 +16,7 @@
"requirements": [
"bleak==0.21.1",
"bleak-retry-connector==3.5.0",
"bluetooth-adapters==0.19.0",
"bluetooth-adapters==0.19.1",
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.19.0",
"dbus-fast==2.21.1",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"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,
)
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:
"""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.
# 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
new_features = ClimateEntityFeature(_supported_features)
self._report_deprecated_supported_features_values(new_features)
_features = ClimateEntityFeature(_supported_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
# supported features and return it
return _supported_features | super().__getattribute__(
"_ClimateEntity__mod_supported_features"
)
return _features | _mod_supported_features
@callback
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 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
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
)
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
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:
# turn_on/off implicitly supported by including more modes than 1 and one of these
# 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")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF

View File

@@ -2,10 +2,10 @@
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 .const import HVAC_MODES, HVACMode
from .const import DOMAIN, HVACMode
if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry
@@ -17,6 +17,15 @@ def async_describe_on_off_states(
) -> None:
"""Describe group 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,
)

View File

@@ -365,13 +365,16 @@ class CloudPreferences:
@property
def strict_connection(self) -> http.const.StrictConnectionMode:
"""Return the strict connection mode."""
mode = self._prefs.get(
PREF_STRICT_CONNECTION, http.const.StrictConnectionMode.DISABLED
)
mode = self._prefs.get(PREF_STRICT_CONNECTION)
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)
return mode # type: ignore[no-any-return]
return mode
async def get_cloud_user(self) -> str:
"""Return ID of Home Assistant Cloud system user."""
@@ -430,5 +433,5 @@ class CloudPreferences:
PREF_REMOTE_DOMAIN: None,
PREF_REMOTE_ALLOW_REMOTE_ENABLE: True,
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."""
if await self._switch(self._command_on) and not self._command_state:
self._attr_is_on = True
self.async_schedule_update_ha_state()
self.async_write_ha_state()
await self._update_entity_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if await self._switch(self._command_off) and not self._command_state:
self._attr_is_on = False
self.async_schedule_update_ha_state()
self.async_write_ha_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 . import group as group_pre_import # noqa: F401
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DOMAIN = "cover"
SCAN_INTERVAL = timedelta(seconds=15)
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.core import HomeAssistant, callback
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry
@@ -15,4 +17,4 @@ def async_describe_on_off_states(
) -> None:
"""Describe group on off states."""
# 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.core import HomeAssistant, callback
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry
@@ -14,4 +16,4 @@ def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
) -> None:
"""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:
"""Set up a config entry."""
coordinator = DwdWeatherWarningsCoordinator(hass, entry)
coordinator = DwdWeatherWarningsCoordinator(hass)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

View File

@@ -26,7 +26,7 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
config_entry: ConfigEntry
api: DwdWeatherWarningsAPI
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the dwd_weather_warnings coordinator."""
super().__init__(
hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL

View File

@@ -37,6 +37,7 @@ PLATFORMS = [
Platform.SWITCH,
Platform.VACUUM,
]
EcovacsConfigEntry = ConfigEntry[EcovacsController]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -50,21 +51,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
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."""
controller = EcovacsController(hass, entry.data)
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)
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."""
if unload_ok := 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
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -11,13 +11,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import (
CapabilityDevice,
EcovacsCapabilityEntityDescription,
@@ -52,13 +50,14 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsBinarySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add entities for passed config_entry in HA."""
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
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 homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, SUPPORTED_LIFESPANS
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .const import SUPPORTED_LIFESPANS
from .entity import (
CapabilityDevice,
EcovacsCapabilityEntityDescription,
@@ -66,11 +65,11 @@ LIFESPAN_ENTITY_DESCRIPTIONS = tuple(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS
)

View File

@@ -42,7 +42,7 @@ class EcovacsController:
"""Initialize controller."""
self._hass = hass
self._devices: list[Device] = []
self.legacy_devices: list[VacBot] = []
self._legacy_devices: list[VacBot] = []
rest_url = config.get(CONF_OVERRIDE_REST_URL)
self._device_id = get_client_device_id(hass, rest_url is not None)
country = config[CONF_COUNTRY]
@@ -101,7 +101,7 @@ class EcovacsController:
self._continent,
monitor=True,
)
self.legacy_devices.append(bot)
self._legacy_devices.append(bot)
except InvalidAuthenticationError as ex:
raise ConfigEntryError("Invalid credentials") from ex
except DeebotError as ex:
@@ -113,7 +113,7 @@ class EcovacsController:
"""Disconnect controller."""
for device in self._devices:
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._mqtt.disconnect()
await self._authenticator.teardown()
@@ -124,3 +124,8 @@ class EcovacsController:
for device in self._devices:
if isinstance(device.capabilities, capability):
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 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.core import HomeAssistant
from .const import CONF_OVERRIDE_MQTT_URL, CONF_OVERRIDE_REST_URL, DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .const import CONF_OVERRIDE_MQTT_URL, CONF_OVERRIDE_REST_URL
REDACT_CONFIG = {
CONF_USERNAME,
@@ -25,10 +24,10 @@ REDACT_DEVICE = {"did", CONF_NAME, "homeId"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: EcovacsConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
controller = config_entry.runtime_data
diag: dict[str, Any] = {
"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 homeassistant.components.event import EventEntity, EventEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import EcovacsEntity
from .util import get_name_key
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
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 homeassistant.components.image import ImageEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import EcovacsEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add entities for passed config_entry in HA."""
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
controller = config_entry.runtime_data
entities = []
for device in controller.devices(VacuumCapabilities):
capabilities: VacuumCapabilities = device.capabilities

View File

@@ -15,12 +15,10 @@ from homeassistant.components.lawn_mower import (
LawnMowerEntityEntityDescription,
LawnMowerEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import EcovacsEntity
_LOGGER = logging.getLogger(__name__)
@@ -38,11 +36,11 @@ _STATE_TO_MOWER_STATE = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Ecovacs mowers."""
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
controller = config_entry.runtime_data
mowers: list[EcovacsMower] = [
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 homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import (
CapabilityDevice,
EcovacsCapabilityEntityDescription,
@@ -70,11 +68,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS
)

View File

@@ -9,13 +9,11 @@ from deebot_client.device import Device
from deebot_client.events import WaterInfoEvent, WorkModeEvent
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import (
CapabilityDevice,
EcovacsCapabilityEntityDescription,
@@ -62,11 +60,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
)

View File

@@ -24,7 +24,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_BATTERY_LEVEL,
@@ -37,8 +36,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN, SUPPORTED_LIFESPANS
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .const import SUPPORTED_LIFESPANS
from .entity import (
CapabilityDevice,
EcovacsCapabilityEntityDescription,
@@ -171,11 +170,11 @@ LIFESPAN_ENTITY_DESCRIPTIONS = tuple(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
controller, EcovacsSensor, ENTITY_DESCRIPTIONS

View File

@@ -11,13 +11,11 @@ from deebot_client.capabilities import (
from deebot_client.events import EnableEvent
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from . import EcovacsConfigEntry
from .entity import (
CapabilityDevice,
EcovacsCapabilityEntityDescription,
@@ -121,11 +119,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS
)

View File

@@ -23,15 +23,14 @@ from homeassistant.components.vacuum import (
StateVacuumEntityDescription,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util import slugify
from . import EcovacsConfigEntry
from .const import DOMAIN
from .controller import EcovacsController
from .entity import EcovacsEntity
from .util import get_name_key
@@ -43,11 +42,11 @@ ATTR_COMPONENT_PREFIX = "component_"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: EcovacsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Ecovacs vacuums."""
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
controller = config_entry.runtime_data
vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [
EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities)
]

View File

@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push",
"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.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.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -63,7 +62,7 @@ async def async_setup_entry(
async_add_entities(entities)
class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
class EmonitorPowerSensor(CoordinatorEntity[EmonitorStatus], SensorEntity):
"""Representation of an Emonitor power sensor entity."""
_attr_device_class = SensorDeviceClass.POWER
@@ -81,7 +80,8 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
self.entity_description = description
self.channel_number = channel_number
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:])
label = self.channel_data.label or str(channel_number)
if description.translation_key is not None:
@@ -94,13 +94,15 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
manufacturer="Powerhouse Dynamics, Inc.",
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
def channels(self) -> dict[int, EmonitorChannel]:
"""Return the channels dict."""
channels: dict[int, EmonitorChannel] = self.emonitor_status.channels
channels: dict[int, EmonitorChannel] = self.coordinator.data.channels
return channels
@property
@@ -108,11 +110,6 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
"""Return the channel data."""
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:
"""Cumulative attributes for channel and paired channel."""
channel_data = self.channels[self.channel_number]
@@ -121,12 +118,8 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity):
attr_val += getattr(self.channels[paired_channel], attr_name)
return attr_val
@property
def native_value(self) -> StateType:
"""State of the sensor."""
return self._paired_attr(self.entity_description.key)
@property
def extra_state_attributes(self) -> dict[str, int]:
"""Return the device specific state attributes."""
return {"channel": self.channel_number}
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_native_value = self._paired_attr(self.entity_description.key)
return super()._handle_coordinator_update()

View File

@@ -20,7 +20,7 @@ UPDATE_INTERVAL: Final = datetime.timedelta(minutes=30)
TIMEOUT = 10
class FitbitDeviceCoordinator(DataUpdateCoordinator):
class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, FitbitDevice]]):
"""Coordinator for fetching fitbit devices from the API."""
def __init__(self, hass: HomeAssistant, api: FitbitApi) -> None:

View File

@@ -443,7 +443,10 @@ class FritzBoxTools(
)
except Exception as ex: # pylint: disable=[broad-except]
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] = {}
if hosts_attributes:
@@ -730,7 +733,9 @@ class FritzBoxTools(
_LOGGER.debug("FRITZ!Box service: %s", service_call.service)
if not self.connection:
raise HomeAssistantError("Unable to establish a connection")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unable_to_connect"
)
try:
if service_call.service == SERVICE_REBOOT:
@@ -765,9 +770,13 @@ class FritzBoxTools(
return
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:
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

View File

@@ -55,8 +55,9 @@ async def async_setup_services(hass: HomeAssistant) -> None:
)
):
raise HomeAssistantError(
f"Failed to call service '{service_call.service}'. Config entry for"
" target not found"
translation_domain=DOMAIN,
translation_key="config_entry_not_found",
translation_placeholders={"service": service_call.service},
)
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 pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
from pyfritzhome import FritzhomeDevice
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.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
UnitOfTemperature,
)
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfTemperature
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS
from .coordinator import FritzboxDataUpdateCoordinator
from .const import DOMAIN, LOGGER, PLATFORMS
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."""
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:
"""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)
coordinator = FritzboxDataUpdateCoordinator(hass, entry.entry_id, has_templates)
coordinator = FritzboxDataUpdateCoordinator(hass, entry.entry_id)
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)
def logout_fritzbox(event: Event) -> None:
"""Close connections to this fritzbox."""
fritz.logout()
coordinator.fritz.logout()
entry.async_on_unload(
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
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."""
fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS]
await hass.async_add_executor_job(fritz.logout)
await hass.async_add_executor_job(entry.runtime_data.fritz.logout)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_config_entry_device(
hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
hass: HomeAssistant, entry: FritzboxConfigEntry, device: DeviceEntry
) -> bool:
"""Remove Fritzbox config entry from a device."""
coordinator: FritzboxDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
CONF_COORDINATOR
]
coordinator = entry.runtime_data
for identifier in device.identifiers:
if identifier[0] == DOMAIN and (

View File

@@ -13,13 +13,12 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
from .common import get_coordinator
from .coordinator import FritzboxConfigEntry
from .model import FritzEntityDescriptionMixinBase
@@ -65,10 +64,12 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome binary sensor from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
def _add_entities(devices: set[str] | None = None) -> None:

View File

@@ -3,21 +3,22 @@
from pyfritzhome.devicetypes import FritzhomeTemplate
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from .common import get_coordinator
from .const import DOMAIN
from .coordinator import FritzboxConfigEntry
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome template from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
def _add_entities(templates: set[str] | None = None) -> None:

View File

@@ -12,7 +12,6 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE,
@@ -23,7 +22,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
from .common import get_coordinator
from .const import (
ATTR_STATE_BATTERY_LOW,
ATTR_STATE_HOLIDAY_MODE,
@@ -31,6 +29,7 @@ from .const import (
ATTR_STATE_WINDOW_OPEN,
LOGGER,
)
from .coordinator import FritzboxConfigEntry
from .model import ClimateExtraAttributes
OPERATION_LIST = [HVACMode.HEAT, HVACMode.OFF]
@@ -48,10 +47,12 @@ OFF_REPORT_SET_TEMPERATURE = 0.0
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome thermostat from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
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_TEMP_MODE: Final = "4"
CONF_CONNECTIONS: Final = "connections"
CONF_COORDINATOR: Final = "coordinator"
DEFAULT_HOST: Final = "fritz.box"
DEFAULT_USERNAME: Final = "admin"

View File

@@ -10,12 +10,15 @@ from pyfritzhome.devicetypes import FritzhomeTemplate
from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
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.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_CONNECTIONS, DOMAIN, LOGGER
from .const import DOMAIN, LOGGER
FritzboxConfigEntry = ConfigEntry["FritzboxDataUpdateCoordinator"]
@dataclass
@@ -29,10 +32,12 @@ class FritzboxCoordinatorData:
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
"""Fritzbox Smarthome device data update coordinator."""
config_entry: ConfigEntry
config_entry: FritzboxConfigEntry
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."""
super().__init__(
hass,
@@ -41,11 +46,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
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_templates: set[str] = set()
@@ -53,6 +53,27 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
async def async_setup(self) -> None:
"""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()
self.cleanup_removed_devices(
list(self.data.devices) + list(self.data.templates)

View File

@@ -10,19 +10,20 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
from .common import get_coordinator
from .coordinator import FritzboxConfigEntry
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome cover from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
def _add_entities(devices: set[str] | None = None) -> None:

View File

@@ -5,22 +5,19 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import CONF_COORDINATOR, DOMAIN
from .coordinator import FritzboxDataUpdateCoordinator
from .coordinator import FritzboxConfigEntry
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: FritzboxConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data: dict = hass.data[DOMAIN][entry.entry_id]
coordinator: FritzboxDataUpdateCoordinator = data[CONF_COORDINATOR]
coordinator = entry.runtime_data
diag_data = {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),

View File

@@ -13,22 +13,23 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity
from .common import get_coordinator
from .const import COLOR_MODE, COLOR_TEMP_MODE, LOGGER
from .coordinator import FritzboxConfigEntry
SUPPORTED_COLOR_MODES = {ColorMode.COLOR_TEMP, ColorMode.HS}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome light from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
def _add_entities(devices: set[str] | None = None) -> None:

View File

@@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
@@ -32,7 +31,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utc_from_timestamp
from . import FritzBoxDeviceEntity
from .common import get_coordinator
from .coordinator import FritzboxConfigEntry
from .model import FritzEntityDescriptionMixinBase
@@ -210,10 +209,12 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome sensor from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
def _add_entities(devices: set[str] | None = None) -> None:

View File

@@ -5,19 +5,20 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
from .common import get_coordinator
from .coordinator import FritzboxConfigEntry
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: FritzboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the FRITZ!SmartHome switch from ConfigEntry."""
coordinator = get_coordinator(hass, entry.entry_id)
coordinator = entry.runtime_data
@callback
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 .base import FritzBoxPhonebook
from .const import (
CONF_PHONEBOOK,
CONF_PREFIXES,
DOMAIN,
FRITZBOX_PHONEBOOK,
PLATFORMS,
UNDO_UPDATE_LISTENER,
)
from .const import CONF_PHONEBOOK, CONF_PREFIXES, PLATFORMS
_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."""
fritzbox_phonebook = FritzBoxPhonebook(
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)
raise ConfigEntryNotReady from ex
undo_listener = 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,
}
config_entry.runtime_data = fritzbox_phonebook
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
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."""
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
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
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."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@@ -38,5 +38,3 @@ DOMAIN: Final = "fritzbox_callmonitor"
MANUFACTURER: Final = "AVM"
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 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.core import Event, HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxCallMonitorConfigEntry
from .base import FritzBoxPhonebook
from .const import (
ATTR_PREFIXES,
CONF_PHONEBOOK,
CONF_PREFIXES,
DOMAIN,
FRITZBOX_PHONEBOOK,
MANUFACTURER,
SERIAL_NUMBER,
FritzState,
@@ -48,13 +47,11 @@ class CallState(StrEnum):
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: FritzBoxCallMonitorConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the fritzbox_callmonitor sensor from config_entry."""
fritzbox_phonebook: FritzBoxPhonebook = hass.data[DOMAIN][config_entry.entry_id][
FRITZBOX_PHONEBOOK
]
fritzbox_phonebook = config_entry.runtime_data
phonebook_id: int = config_entry.data[CONF_PHONEBOOK]
prefixes: list[str] | None = config_entry.options.get(CONF_PREFIXES)

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"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 datetime import datetime
import logging
from typing import Any
from zoneinfo import ZoneInfo
from fyta_cli.fyta_connector import FytaConnector
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 .const import DOMAIN
from .const import CONF_EXPIRATION, DOMAIN
from .coordinator import FytaCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -22,11 +30,16 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Fyta integration."""
tz: str = hass.config.time_zone
username = entry.data[CONF_USERNAME]
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)
@@ -47,3 +60,36 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
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.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN
from .const import CONF_EXPIRATION, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -31,14 +31,19 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fyta."""
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]:
"""Reusable Auth Helper."""
fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
try:
await fyta.login()
self.credentials = await fyta.login()
except FytaConnectionError:
return {"base": "cannot_connect"}
except FytaAuthentificationError:
@@ -51,6 +56,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
finally:
await fyta.client.close()
self.credentials[CONF_EXPIRATION] = self.credentials[
CONF_EXPIRATION
].isoformat()
return {}
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]})
if not (errors := await self.async_auth(user_input)):
user_input |= self.credentials
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
@@ -85,6 +95,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
assert self._entry is not None
if user_input and not (errors := await self.async_auth(user_input)):
user_input |= self.credentials
return self.async_update_reload_and_abort(
self._entry, data={**self._entry.data, **user_input}
)

View File

@@ -1,3 +1,4 @@
"""Const for fyta integration."""
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.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_EXPIRATION
_LOGGER = logging.getLogger(__name__)
@@ -39,17 +42,33 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
) -> dict[int, dict[str, Any]]:
"""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()
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."""
credentials: dict[str, Any] = {}
try:
await self.fyta.login()
credentials = await self.fyta.login()
except FytaConnectionError as ex:
raise ConfigEntryNotReady from ex
except (FytaAuthentificationError, FytaPasswordError) as 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",
"integration_type": "hub",
"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 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 (
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]] = [
FytaSensorEntityDescription(
@@ -52,29 +60,29 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
key="temperature_status",
translation_key="temperature_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=PLANT_STATUS.get,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
),
FytaSensorEntityDescription(
key="light_status",
translation_key="light_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=PLANT_STATUS.get,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
),
FytaSensorEntityDescription(
key="moisture_status",
translation_key="moisture_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=PLANT_STATUS.get,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
),
FytaSensorEntityDescription(
key="salinity_status",
translation_key="salinity_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=PLANT_STATUS.get,
options=PLANT_MEASUREMENT_STATUS_LIST,
value_fn=PLANT_MEASUREMENT_STATUS.get,
),
FytaSensorEntityDescription(
key="temperature",

View File

@@ -36,6 +36,16 @@
"plant_status": {
"name": "Plant 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",
"low": "Low",
"perfect": "Perfect",
@@ -43,44 +53,37 @@
"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": {
"name": "Light 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%]"
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
}
},
"moisture_status": {
"name": "Moisture 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%]"
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
}
},
"salinity_status": {
"name": "Salinity 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%]"
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
}
},
"light": {

View File

@@ -412,7 +412,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity):
else:
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:
"""Update hygrostat with latest state from sensor."""

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
import logging
from aiohttp import ClientSession
@@ -25,8 +26,17 @@ _LOGGER = logging.getLogger(__name__)
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."""
station_id: int = entry.data[CONF_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)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = GiosData(coordinator)
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
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
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 typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import GiosDataUpdateCoordinator
from .const import DOMAIN
from . import GiosConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: GiosConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: GiosDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data.coordinator
return {
"config_entry": config_entry.as_dict(),

View File

@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME
from homeassistant.core import HomeAssistant
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.update_coordinator import CoordinatorEntity
from . import GiosDataUpdateCoordinator
from . import GiosConfigEntry, GiosDataUpdateCoordinator
from .const import (
ATTR_AQI,
ATTR_C6H6,
@@ -159,13 +158,12 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant, entry: GiosConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add a GIOS entities from a config_entry."""
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
# the unique_id to the new name.
entity_registry = er.async_get(hass)

View File

@@ -2,8 +2,7 @@
from __future__ import annotations
from contextvars import ContextVar
from typing import Protocol
from typing import TYPE_CHECKING, Protocol
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback
@@ -13,12 +12,13 @@ from homeassistant.helpers.integration_platform import (
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:
"""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(
hass, DOMAIN, _process_group_platform, wait_for_platforms=True
@@ -39,7 +39,6 @@ def _process_group_platform(
hass: HomeAssistant, domain: str, platform: GroupProtocol
) -> None:
"""Process a group platform."""
current_domain.set(domain)
registry: GroupIntegrationRegistry = hass.data[REG_KEY]
platform.async_describe_on_off_states(hass, registry)
@@ -47,24 +46,31 @@ def _process_group_platform(
class GroupIntegrationRegistry:
"""Class to hold a registry of integrations."""
def __init__(self) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Imitialize registry."""
self.hass = hass
self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF}
self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON}
self.on_states_by_domain: dict[str, set[str]] = {}
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."""
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."""
for on_state in on_states:
if on_state not in self.on_off_mapping:
self.on_off_mapping[on_state] = off_state
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",
"iot_class": "cloud_polling",
"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.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle, dt as dt_util
@@ -46,8 +47,7 @@ def get_device_list(api, config):
not login_response["success"]
and login_response["msg"] == LOGIN_INVALID_AUTH_CODE
):
_LOGGER.error("Username, Password or URL may be incorrect!")
return
raise ConfigEntryError("Username, Password or URL may be incorrect!")
user_id = login_response["user"]["id"]
if plant_id == DEFAULT_PLANT_ID:
plant_info = api.plant_list(user_id)

View File

@@ -30,10 +30,11 @@ from .const import (
EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL,
)
from .sensor import SENSORS_TYPES
_LOGGER = logging.getLogger(__name__)
SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"]
INSTANCE_SCHEMA = vol.All(
cv.deprecated(CONF_SENSORS),
vol.Schema(

View File

@@ -10,9 +10,10 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
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.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
@@ -79,6 +80,20 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_data):
"""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)

View File

@@ -15,3 +15,6 @@ ATTR_ARGS = "args"
# event constants
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
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": {
"api_call": "mdi:console"
}

View File

@@ -1,7 +1,7 @@
{
"domain": "habitica",
"name": "Habitica",
"codeowners": ["@ASMfreaK", "@leikoilja"],
"codeowners": ["@ASMfreaK", "@leikoilja", "@tr4nt0r"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/habitica",
"iot_class": "cloud_polling",

View File

@@ -3,42 +3,123 @@
from __future__ import annotations
from collections import namedtuple
from dataclasses import dataclass
from datetime import timedelta
from enum import StrEnum
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING, Any
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.const import CONF_NAME
from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle
from .const import DOMAIN
from .const import DOMAIN, MANUFACTURER, NAME
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
SENSORS_TYPES = {
"name": SensorType("Name", None, None, ["profile", "name"]),
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]),
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
"mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
"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"]),
"lvl": SensorType(
"Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]
@dataclass(kw_only=True, frozen=True)
class HabitipySensorEntityDescription(SensorEntityDescription):
"""Habitipy Sensor Description."""
value_path: list[str]
class HabitipySensorEntity(StrEnum):
"""Habitipy Entities."""
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 = {
"habits": SensorType(
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]
@@ -92,10 +173,12 @@ async def async_setup_entry(
await sensor_data.update()
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(
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)
@@ -103,7 +186,9 @@ async def async_setup_entry(
class HabitipyData:
"""Habitica API user data cache."""
def __init__(self, api):
tasks: dict[str, Any]
def __init__(self, api) -> None:
"""Habitica API user data cache."""
self.api = api
self.data = None
@@ -153,53 +238,59 @@ class HabitipyData:
class HabitipySensor(SensorEntity):
"""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."""
self._name = name
self._sensor_name = sensor_name
self._sensor_type = SENSORS_TYPES[sensor_name]
self._state = None
self._updater = updater
super().__init__()
if TYPE_CHECKING:
assert entry.unique_id
self.coordinator = coordinator
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:
"""Update Condition and Forecast."""
await self._updater.update()
data = self._updater.data
for element in self._sensor_type.path:
"""Update Sensor state."""
await self.coordinator.update()
data = self.coordinator.data
for element in self.entity_description.value_path:
data = data[element]
self._state = 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
self._attr_native_value = data
class HabitipyTaskSensor(SensorEntity):
"""A Habitica task sensor."""
def __init__(self, name, task_name, updater):
def __init__(self, name, task_name, updater, entry):
"""Initialize a generic Habitica task."""
self._name = name
self._task_name = task_name
self._task_type = TASKS_TYPES[task_name]
self._state = None
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:
"""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": {
"api_call": {
"name": "API name",

View File

@@ -1,13 +1,7 @@
{
"domain": "harmony",
"name": "Logitech Harmony Hub",
"codeowners": [
"@ehendrix23",
"@bramkragten",
"@bdraco",
"@mkeesey",
"@Aohzan"
],
"codeowners": ["@ehendrix23", "@bdraco", "@mkeesey", "@Aohzan"],
"config_flow": true,
"dependencies": ["remote", "switch"],
"documentation": "https://www.home-assistant.io/integrations/harmony",

View File

@@ -67,15 +67,15 @@ class HassIOIngress(HomeAssistantView):
"""Initialize a Hass.io ingress view."""
self._host = host
self._websession = websession
self._url = URL(f"http://{host}")
@lru_cache
def _create_url(self, token: str, path: str) -> URL:
"""Create URL to service."""
base_path = f"/ingress/{token}/"
url = f"http://{self._host}{base_path}{quote(path)}"
try:
target_url = URL(url)
target_url = self._url.join(URL(f"{base_path}{quote(path)}"))
except ValueError as err:
raise HTTPBadRequest from err
@@ -177,11 +177,13 @@ class HassIOIngress(HomeAssistantView):
if maybe_content_type := result.headers.get(hdrs.CONTENT_TYPE):
content_type: str = (maybe_content_type.partition(";"))[0].strip()
else:
content_type = result.content_type
# default value according to RFC 2616
content_type = "application/octet-stream"
# Simple request
if result.status in (204, 304) or (
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
):
# Return Response
@@ -194,17 +196,17 @@ class HassIOIngress(HomeAssistantView):
zlib_executor_size=32768,
)
if content_length_int > MIN_COMPRESSED_SIZE and should_compress(
content_type or simple_response.content_type
content_type
):
simple_response.enable_compression()
return simple_response
# Stream response
response = web.StreamResponse(status=result.status, headers=headers)
response.content_type = result.content_type
response.content_type = content_type
try:
if should_compress(response.content_type):
if should_compress(content_type):
response.enable_compression()
await response.prepare(request)
# 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:
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.async_add_listener(async_schedule_update_alerts)
@@ -99,6 +101,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
cooldown=COMPONENT_LOADED_COOLDOWN,
immediate=False,
function=coordinator.async_refresh,
background=True,
)
@callback

View File

@@ -597,6 +597,21 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
"""Return the name of the hardware."""
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(
BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry

Some files were not shown because too many files have changed in this diff Show More