mirror of
https://github.com/home-assistant/core.git
synced 2025-08-07 06:35:10 +02:00
Merge branch 'dev' into pglab
This commit is contained in:
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -323,7 +323,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.4
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.4.0
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -92,8 +92,10 @@ jobs:
|
||||
uses: actions/checkout@v4.1.4
|
||||
- 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 }}
|
||||
|
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@@ -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"
|
||||
|
@@ -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.*
|
||||
|
10
CODEOWNERS
10
CODEOWNERS
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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/
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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)
|
||||
|
5
homeassistant/components/air_quality/const.py
Normal file
5
homeassistant/components/air_quality/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants for the air_quality entity platform."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "air_quality"
|
@@ -7,10 +7,12 @@ from homeassistant.core import HomeAssistant, callback
|
||||
if TYPE_CHECKING:
|
||||
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)
|
||||
|
@@ -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__(
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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 + ".{}"
|
||||
|
3
homeassistant/components/cover/const.py
Normal file
3
homeassistant/components/cover/const.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Constants for cover entity platform."""
|
||||
|
||||
DOMAIN = "cover"
|
@@ -5,6 +5,8 @@ from typing import TYPE_CHECKING
|
||||
from homeassistant.const import STATE_CLOSED, STATE_OPEN
|
||||
from homeassistant.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)
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
]
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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)
|
||||
]
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -192,5 +192,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_found": {
|
||||
"message": "Failed to call service \"{service}\". Config entry for target not found"
|
||||
},
|
||||
"service_parameter_unknown": { "message": "Service or parameter unknown" },
|
||||
"service_not_supported": { "message": "Service not supported" },
|
||||
"error_refresh_hosts_info": {
|
||||
"message": "Error refreshing hosts info"
|
||||
},
|
||||
"unable_to_connect": {
|
||||
"message": "Unable to establish a connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,52 +4,23 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from 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 (
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -1,16 +0,0 @@
|
||||
"""Common functions for fritzbox integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_COORDINATOR, DOMAIN
|
||||
from .coordinator import FritzboxDataUpdateCoordinator
|
||||
|
||||
|
||||
def get_coordinator(
|
||||
hass: HomeAssistant, config_entry_id: str
|
||||
) -> FritzboxDataUpdateCoordinator:
|
||||
"""Get coordinator for given config entry id."""
|
||||
coordinator: FritzboxDataUpdateCoordinator = hass.data[DOMAIN][config_entry_id][
|
||||
CONF_COORDINATOR
|
||||
]
|
||||
return coordinator
|
@@ -15,9 +15,6 @@ ATTR_STATE_WINDOW_OPEN: Final = "window_open"
|
||||
COLOR_MODE: Final = "1"
|
||||
COLOR_TEMP_MODE: Final = "4"
|
||||
|
||||
CONF_CONNECTIONS: Final = "connections"
|
||||
CONF_COORDINATOR: Final = "coordinator"
|
||||
|
||||
DEFAULT_HOST: Final = "fritz.box"
|
||||
DEFAULT_USERNAME: Final = "admin"
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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),
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
|
@@ -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)
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
"""Const for fyta integration."""
|
||||
|
||||
DOMAIN = "fyta"
|
||||
CONF_EXPIRATION = "expiration"
|
||||
|
@@ -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
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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": {
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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(),
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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."""
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
Reference in New Issue
Block a user