Merge branch 'dev' into feature/starlink-device-tracker

This commit is contained in:
Jack Boswell
2023-08-19 15:26:06 +12:00
committed by GitHub
613 changed files with 12266 additions and 3469 deletions

View File

@@ -168,6 +168,10 @@ omit =
homeassistant/components/cmus/media_player.py homeassistant/components/cmus/media_player.py
homeassistant/components/coinbase/sensor.py homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comelit/__init__.py
homeassistant/components/comelit/const.py
homeassistant/components/comelit/coordinator.py
homeassistant/components/comelit/light.py
homeassistant/components/comfoconnect/fan.py homeassistant/components/comfoconnect/fan.py
homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/alarm_control_panel.py
homeassistant/components/concord232/binary_sensor.py homeassistant/components/concord232/binary_sensor.py
@@ -212,8 +216,9 @@ omit =
homeassistant/components/dominos/* homeassistant/components/dominos/*
homeassistant/components/doods/* homeassistant/components/doods/*
homeassistant/components/doorbird/__init__.py homeassistant/components/doorbird/__init__.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/camera.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/device.py
homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/entity.py
homeassistant/components/doorbird/util.py homeassistant/components/doorbird/util.py
homeassistant/components/dormakaba_dkey/__init__.py homeassistant/components/dormakaba_dkey/__init__.py
@@ -305,6 +310,8 @@ omit =
homeassistant/components/enphase_envoy/binary_sensor.py homeassistant/components/enphase_envoy/binary_sensor.py
homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/coordinator.py
homeassistant/components/enphase_envoy/entity.py homeassistant/components/enphase_envoy/entity.py
homeassistant/components/enphase_envoy/number.py
homeassistant/components/enphase_envoy/select.py
homeassistant/components/enphase_envoy/sensor.py homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/enphase_envoy/switch.py homeassistant/components/enphase_envoy/switch.py
homeassistant/components/entur_public_transport/* homeassistant/components/entur_public_transport/*
@@ -341,6 +348,7 @@ omit =
homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/entity.py
homeassistant/components/ezviz/select.py homeassistant/components/ezviz/select.py
homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/sensor.py
homeassistant/components/ezviz/siren.py
homeassistant/components/ezviz/switch.py homeassistant/components/ezviz/switch.py
homeassistant/components/ezviz/update.py homeassistant/components/ezviz/update.py
homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/__init__.py
@@ -1325,9 +1333,6 @@ omit =
homeassistant/components/tplink_omada/__init__.py homeassistant/components/tplink_omada/__init__.py
homeassistant/components/tplink_omada/binary_sensor.py homeassistant/components/tplink_omada/binary_sensor.py
homeassistant/components/tplink_omada/controller.py homeassistant/components/tplink_omada/controller.py
homeassistant/components/tplink_omada/coordinator.py
homeassistant/components/tplink_omada/entity.py
homeassistant/components/tplink_omada/switch.py
homeassistant/components/tplink_omada/update.py homeassistant/components/tplink_omada/update.py
homeassistant/components/traccar/device_tracker.py homeassistant/components/traccar/device_tracker.py
homeassistant/components/tractive/__init__.py homeassistant/components/tractive/__init__.py

View File

@@ -734,9 +734,14 @@ jobs:
- name: Run pytest (fully) - name: Run pytest (fully)
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
timeout-minutes: 60 timeout-minutes: 60
id: pytest-full
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
@@ -749,14 +754,19 @@ jobs:
--cov-report=xml \ --cov-report=xml \
-o console_output_style=count \ -o console_output_style=count \
-p no:sugar \ -p no:sugar \
tests tests \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Run pytest (partially) - name: Run pytest (partially)
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
timeout-minutes: 10 timeout-minutes: 10
id: pytest-partial
shell: bash shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
@@ -774,7 +784,14 @@ jobs:
--durations=0 \ --durations=0 \
--durations-min=1 \ --durations-min=1 \
-p no:sugar \ -p no:sugar \
tests/components/${{ matrix.group }} tests/components/${{ matrix.group }} \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure')
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
- name: Upload coverage artifact - name: Upload coverage artifact
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v3.1.2
with: with:
@@ -862,10 +879,15 @@ jobs:
python3 -m script.translations develop --all python3 -m script.translations develop --all
- name: Run pytest (partially) - name: Run pytest (partially)
timeout-minutes: 20 timeout-minutes: 20
id: pytest-partial
shell: bash shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g")
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
@@ -881,7 +903,14 @@ jobs:
tests/components/history \ tests/components/history \
tests/components/logbook \ tests/components/logbook \
tests/components/recorder \ tests/components/recorder \
tests/components/sensor tests/components/sensor \
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
- name: Upload coverage artifact - name: Upload coverage artifact
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v3.1.2
with: with:
@@ -969,10 +998,15 @@ jobs:
python3 -m script.translations develop --all python3 -m script.translations develop --all
- name: Run pytest (partially) - name: Run pytest (partially)
timeout-minutes: 20 timeout-minutes: 20
id: pytest-partial
shell: bash shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g")
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
@@ -989,7 +1023,14 @@ jobs:
tests/components/history \ tests/components/history \
tests/components/logbook \ tests/components/logbook \
tests/components/recorder \ tests/components/recorder \
tests/components/sensor tests/components/sensor \
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
- name: Upload coverage artifact - name: Upload coverage artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:

1
.gitignore vendored
View File

@@ -67,6 +67,7 @@ htmlcov/
test-reports/ test-reports/
test-results.xml test-results.xml
test-output.xml test-output.xml
pytest-*.txt
# Translations # Translations
*.mo *.mo

View File

@@ -209,6 +209,8 @@ build.json @home-assistant/supervisor
/tests/components/coinbase/ @tombrien /tests/components/coinbase/ @tombrien
/homeassistant/components/color_extractor/ @GenericStudent /homeassistant/components/color_extractor/ @GenericStudent
/tests/components/color_extractor/ @GenericStudent /tests/components/color_extractor/ @GenericStudent
/homeassistant/components/comelit/ @chemelli74
/tests/components/comelit/ @chemelli74
/homeassistant/components/comfoconnect/ @michaelarnauts /homeassistant/components/comfoconnect/ @michaelarnauts
/tests/components/comfoconnect/ @michaelarnauts /tests/components/comfoconnect/ @michaelarnauts
/homeassistant/components/command_line/ @gjohansson-ST /homeassistant/components/command_line/ @gjohansson-ST
@@ -606,8 +608,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard /homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard /tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iperf3/ @rohankapoorcom /homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes @abmantis /homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes @abmantis /tests/components/ipma/ @dgomes
/homeassistant/components/ipp/ @ctalkington /homeassistant/components/ipp/ @ctalkington
/tests/components/ipp/ @ctalkington /tests/components/ipp/ @ctalkington
/homeassistant/components/iqvia/ @bachya /homeassistant/components/iqvia/ @bachya

View File

@@ -110,8 +110,7 @@ async def async_setup_hass(
runtime_config: RuntimeConfig, runtime_config: RuntimeConfig,
) -> core.HomeAssistant | None: ) -> core.HomeAssistant | None:
"""Set up Home Assistant.""" """Set up Home Assistant."""
hass = core.HomeAssistant() hass = core.HomeAssistant(runtime_config.config_dir)
hass.config.config_dir = runtime_config.config_dir
async_enable_logging( async_enable_logging(
hass, hass,
@@ -134,6 +133,7 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", runtime_config.config_dir) _LOGGER.info("Config directory: %s", runtime_config.config_dir)
loader.async_setup(hass)
config_dict = None config_dict = None
basic_setup_success = False basic_setup_success = False
@@ -177,14 +177,15 @@ async def async_setup_hass(
old_config = hass.config old_config = hass.config
old_logging = hass.data.get(DATA_LOGGING) old_logging = hass.data.get(DATA_LOGGING)
hass = core.HomeAssistant() hass = core.HomeAssistant(old_config.config_dir)
if old_logging: if old_logging:
hass.data[DATA_LOGGING] = old_logging hass.data[DATA_LOGGING] = old_logging
hass.config.skip_pip = old_config.skip_pip hass.config.skip_pip = old_config.skip_pip
hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.skip_pip_packages = old_config.skip_pip_packages
hass.config.internal_url = old_config.internal_url hass.config.internal_url = old_config.internal_url
hass.config.external_url = old_config.external_url hass.config.external_url = old_config.external_url
hass.config.config_dir = old_config.config_dir # Setup loader cache after the config dir has been set
loader.async_setup(hass)
if safe_mode: if safe_mode:
_LOGGER.info("Starting in safe mode") _LOGGER.info("Starting in safe mode")

View File

@@ -1,6 +1,7 @@
"""The AccuWeather component.""" """The AccuWeather component."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
@@ -8,7 +9,6 @@ from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry

View File

@@ -2,12 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from typing import Any from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries

View File

@@ -50,3 +50,8 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_SUNNY: [1, 2, 5],
ATTR_CONDITION_WINDY: [32], ATTR_CONDITION_WINDY: [32],
} }
CONDITION_MAP = {
cond_code: cond_ha
for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes
}

View File

@@ -40,7 +40,7 @@ from .const import (
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
CONDITION_CLASSES, CONDITION_MAP,
DOMAIN, DOMAIN,
) )
@@ -80,14 +80,7 @@ class AccuWeatherEntity(
@property @property
def condition(self) -> str | None: def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
try: return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
return [
k
for k, v in CONDITION_CLASSES.items()
if self.coordinator.data["WeatherIcon"] in v
][0]
except IndexError:
return None
@property @property
def cloud_coverage(self) -> float: def cloud_coverage(self) -> float:
@@ -177,9 +170,7 @@ class AccuWeatherEntity(
], ],
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE], ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: [ ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
][0],
} }
for item in self.coordinator.data[ATTR_FORECAST] for item in self.coordinator.data[ATTR_FORECAST]
] ]

View File

@@ -2,11 +2,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from contextlib import suppress from contextlib import suppress
from typing import Any from typing import Any
import aiopulse import aiopulse
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@@ -43,7 +43,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
hubs: list[aiopulse.Hub] = [] hubs: list[aiopulse.Hub] = []
with suppress(asyncio.TimeoutError): with suppress(asyncio.TimeoutError):
async with async_timeout.timeout(5): async with timeout(5):
async for hub in aiopulse.Hub.discover(): async for hub in aiopulse.Hub.discover():
if hub.id not in already_configured: if hub.id not in already_configured:
hubs.append(hub) hubs.append(hub)

View File

@@ -1,12 +1,12 @@
"""Support for Automation Device Specification (ADS).""" """Support for Automation Device Specification (ADS)."""
import asyncio import asyncio
from asyncio import timeout
from collections import namedtuple from collections import namedtuple
import ctypes import ctypes
import logging import logging
import struct import struct
import threading import threading
import async_timeout
import pyads import pyads
import voluptuous as vol import voluptuous as vol
@@ -301,7 +301,7 @@ class AdsEntity(Entity):
self._ads_hub.add_device_notification, ads_var, plctype, update self._ads_hub.add_device_notification, ads_var, plctype, update
) )
try: try:
async with async_timeout.timeout(10): async with timeout(10):
await self._event.wait() await self._event.wait()
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.debug("Variable %s: Timeout during first update", ads_var) _LOGGER.debug("Variable %s: Timeout during first update", ads_var)

View File

@@ -1,4 +1,6 @@
"""Support for the AEMET OpenData service.""" """Support for the AEMET OpenData service."""
from typing import cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_PRECIPITATION,
@@ -8,7 +10,10 @@ from homeassistant.components.weather import (
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
DOMAIN as WEATHER_DOMAIN,
Forecast,
WeatherEntity, WeatherEntity,
WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -17,7 +22,8 @@ from homeassistant.const import (
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -79,10 +85,28 @@ async def async_setup_entry(
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
entities = [] entities = []
for mode in FORECAST_MODES: entity_registry = er.async_get(hass)
name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}" # Add daily + hourly entity for legacy config entries, only add daily for new
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) # config entries. This can be removed in HA Core 2024.3
if entity_registry.async_get_entity_id(
WEATHER_DOMAIN,
DOMAIN,
f"{config_entry.unique_id} {FORECAST_MODE_HOURLY}",
):
for mode in FORECAST_MODES:
name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}"
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
else:
entities.append(
AemetWeather(
domain_data[ENTRY_NAME],
config_entry.unique_id,
weather_coordinator,
FORECAST_MODE_DAILY,
)
)
async_add_entities(entities, False) async_add_entities(entities, False)
@@ -95,6 +119,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
_attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
)
def __init__( def __init__(
self, self,
@@ -112,20 +139,44 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
self._attr_name = name self._attr_name = name
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
super()._handle_coordinator_update()
assert self.platform.config_entry
self.platform.config_entry.async_create_task(
self.hass, self.async_update_listeners(("daily", "hourly"))
)
@property @property
def condition(self): def condition(self):
"""Return the current condition.""" """Return the current condition."""
return self.coordinator.data[ATTR_API_CONDITION] return self.coordinator.data[ATTR_API_CONDITION]
@property def _forecast(self, forecast_mode: str) -> list[Forecast]:
def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]]
forecast_map = FORECAST_MAP[self._forecast_mode] forecast_map = FORECAST_MAP[forecast_mode]
return [ return cast(
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} list[Forecast],
for forecast in forecasts [
] {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
for forecast in forecasts
],
)
@property
def forecast(self) -> list[Forecast]:
"""Return the forecast array."""
return self._forecast(self._forecast_mode)
async def async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast(FORECAST_MODE_DAILY)
async def async_forecast_hourly(self) -> list[Forecast]:
"""Return the hourly forecast in native units."""
return self._forecast(FORECAST_MODE_HOURLY)
@property @property
def humidity(self): def humidity(self):

View File

@@ -1,6 +1,7 @@
"""Weather data coordinator for the AEMET OpenData service.""" """Weather data coordinator for the AEMET OpenData service."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
import logging import logging
@@ -41,7 +42,6 @@ from aemet_opendata.helpers import (
get_forecast_hour_value, get_forecast_hour_value,
get_forecast_interval_value, get_forecast_interval_value,
) )
import async_timeout
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -139,7 +139,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self): async def _async_update_data(self):
data = {} data = {}
async with async_timeout.timeout(120): async with timeout(120):
weather_response = await self._get_aemet_weather() weather_response = await self._get_aemet_weather()
data = self._convert_weather_response(weather_response) data = self._convert_weather_response(weather_response)
return data return data

View File

@@ -1,6 +1,7 @@
"""The Airly integration.""" """The Airly integration."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from math import ceil from math import ceil
@@ -9,7 +10,6 @@ from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from airly import Airly from airly import Airly
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
import async_timeout
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
measurements = self.airly.create_measurements_session_point( measurements = self.airly.create_measurements_session_point(
self.latitude, self.longitude self.latitude, self.longitude
) )
async with async_timeout.timeout(20): async with timeout(20):
try: try:
await measurements.update() await measurements.update()
except (AirlyError, ClientConnectorError) as error: except (AirlyError, ClientConnectorError) as error:

View File

@@ -1,13 +1,13 @@
"""Adds config flow for Airly.""" """Adds config flow for Airly."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from aiohttp import ClientSession from aiohttp import ClientSession
from airly import Airly from airly import Airly
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@@ -105,7 +105,7 @@ async def test_location(
measurements = airly.create_measurements_session_point( measurements = airly.create_measurements_session_point(
latitude=latitude, longitude=longitude latitude=latitude, longitude=longitude
) )
async with async_timeout.timeout(10): async with timeout(10):
await measurements.update() await measurements.update()
current = measurements.current current = measurements.current

View File

@@ -1,13 +1,13 @@
"""The Airzone integration.""" """The Airzone integration."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from aioairzone.exceptions import AirzoneError from aioairzone.exceptions import AirzoneError
from aioairzone.localapi import AirzoneLocalApi from aioairzone.localapi import AirzoneLocalApi
import async_timeout
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): async with timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
try: try:
await self.airzone.update() await self.airzone.update()
except AirzoneError as error: except AirzoneError as error:

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.5"] "requirements": ["aioairzone==0.6.6"]
} }

View File

@@ -1,13 +1,13 @@
"""The Airzone Cloud integration coordinator.""" """The Airzone Cloud integration coordinator."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from aioairzone_cloud.cloudapi import AirzoneCloudApi from aioairzone_cloud.cloudapi import AirzoneCloudApi
from aioairzone_cloud.exceptions import AirzoneCloudError from aioairzone_cloud.exceptions import AirzoneCloudError
import async_timeout
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC): async with timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
try: try:
await self.airzone.update() await self.airzone.update()
except AirzoneCloudError as error: except AirzoneCloudError as error:

View File

@@ -1,5 +1,6 @@
"""Support for Alexa skill auth.""" """Support for Alexa skill auth."""
import asyncio import asyncio
from asyncio import timeout
from datetime import datetime, timedelta from datetime import datetime, timedelta
from http import HTTPStatus from http import HTTPStatus
import json import json
@@ -7,7 +8,6 @@ import logging
from typing import Any from typing import Any
import aiohttp import aiohttp
import async_timeout
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@@ -113,7 +113,7 @@ class Auth:
async def _async_request_new_token(self, lwa_params: dict[str, str]) -> str | None: async def _async_request_new_token(self, lwa_params: dict[str, str]) -> str | None:
try: try:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
async with async_timeout.timeout(10): async with timeout(10):
response = await session.post( response = await session.post(
LWA_TOKEN_URI, LWA_TOKEN_URI,
headers=LWA_HEADERS, headers=LWA_HEADERS,

View File

@@ -474,7 +474,24 @@ async def async_api_unlock(
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an unlock request.""" """Process an unlock request."""
if config.locale not in {"de-DE", "en-US", "ja-JP"}: if config.locale not in {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}:
msg = ( msg = (
"The unlock directive is not supported for the following locales:" "The unlock directive is not supported for the following locales:"
f" {config.locale}" f" {config.locale}"

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging import logging
@@ -10,7 +11,6 @@ from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4 from uuid import uuid4
import aiohttp import aiohttp
import async_timeout
from homeassistant.components import event from homeassistant.components import event
from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.const import MATCH_ALL, STATE_ON
@@ -364,7 +364,7 @@ async def async_send_changereport_message(
assert config.endpoint is not None assert config.endpoint is not None
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): async with timeout(DEFAULT_TIMEOUT):
response = await session.post( response = await session.post(
config.endpoint, config.endpoint,
headers=headers, headers=headers,
@@ -517,7 +517,7 @@ async def async_send_doorbell_event_message(
assert config.endpoint is not None assert config.endpoint is not None
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): async with timeout(DEFAULT_TIMEOUT):
response = await session.post( response = await session.post(
config.endpoint, config.endpoint,
headers=headers, headers=headers,

View File

@@ -2,13 +2,13 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from dataclasses import asdict as dataclass_asdict, dataclass from dataclasses import asdict as dataclass_asdict, dataclass
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import uuid import uuid
import aiohttp import aiohttp
import async_timeout
from homeassistant.components import hassio from homeassistant.components import hassio
from homeassistant.components.api import ATTR_INSTALLATION_TYPE from homeassistant.components.api import ATTR_INSTALLATION_TYPE
@@ -313,7 +313,7 @@ class Analytics:
) )
try: try:
async with async_timeout.timeout(30): async with timeout(30):
response = await self.session.post(self.endpoint, json=payload) response = await self.session.post(self.endpoint, json=payload)
if response.status == 200: if response.status == 200:
LOGGER.info( LOGGER.info(

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
import logging import logging
from androidtvremote2 import ( from androidtvremote2 import (
@@ -10,7 +11,6 @@ from androidtvremote2 import (
ConnectionClosed, ConnectionClosed,
InvalidAuth, InvalidAuth,
) )
import async_timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
@@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api.add_is_available_updated_callback(is_available_updated) api.add_is_available_updated_callback(is_available_updated)
try: try:
async with async_timeout.timeout(5.0): async with timeout(5.0):
await api.async_connect() await api.async_connect()
except InvalidAuth as exc: except InvalidAuth as exc:
# The Android TV is hard reset or the certificate and key files were deleted. # The Android TV is hard reset or the certificate and key files were deleted.

View File

@@ -1,9 +1,9 @@
"""Support for Anova Coordinators.""" """Support for Anova Coordinators."""
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate
import async_timeout
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@@ -47,7 +47,7 @@ class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]):
async def _async_update_data(self) -> APCUpdate: async def _async_update_data(self) -> APCUpdate:
try: try:
async with async_timeout.timeout(5): async with timeout(5):
return await self.anova_device.update() return await self.anova_device.update()
except AnovaOffline as err: except AnovaOffline as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err

View File

@@ -1,17 +1,17 @@
"""Rest API for Home Assistant.""" """Rest API for Home Assistant."""
import asyncio import asyncio
from asyncio import timeout
from functools import lru_cache from functools import lru_cache
from http import HTTPStatus from http import HTTPStatus
import logging import logging
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPBadRequest from aiohttp.web_exceptions import HTTPBadRequest
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.bootstrap import DATA_LOGGING from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView, require_admin
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
MATCH_ALL, MATCH_ALL,
@@ -110,10 +110,9 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM url = URL_API_STREAM
name = "api:stream" name = "api:stream"
@require_admin
async def get(self, request): async def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
if not request["hass_user"].is_admin:
raise Unauthorized()
hass = request.app["hass"] hass = request.app["hass"]
stop_obj = object() stop_obj = object()
to_write = asyncio.Queue() to_write = asyncio.Queue()
@@ -149,7 +148,7 @@ class APIEventStream(HomeAssistantView):
while True: while True:
try: try:
async with async_timeout.timeout(STREAM_PING_INTERVAL): async with timeout(STREAM_PING_INTERVAL):
payload = await to_write.get() payload = await to_write.get()
if payload is stop_obj: if payload is stop_obj:
@@ -278,10 +277,9 @@ class APIEventView(HomeAssistantView):
url = "/api/events/{event_type}" url = "/api/events/{event_type}"
name = "api:event" name = "api:event"
@require_admin
async def post(self, request, event_type): async def post(self, request, event_type):
"""Fire events.""" """Fire events."""
if not request["hass_user"].is_admin:
raise Unauthorized()
body = await request.text() body = await request.text()
try: try:
event_data = json_loads(body) if body else None event_data = json_loads(body) if body else None
@@ -385,10 +383,9 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = "api:template"
@require_admin
async def post(self, request): async def post(self, request):
"""Render a template.""" """Render a template."""
if not request["hass_user"].is_admin:
raise Unauthorized()
try: try:
data = await request.json() data = await request.json()
tpl = _cached_template(data["template"], request.app["hass"]) tpl = _cached_template(data["template"], request.app["hass"])
@@ -405,10 +402,9 @@ class APIErrorLog(HomeAssistantView):
url = URL_API_ERROR_LOG url = URL_API_ERROR_LOG
name = "api:error_log" name = "api:error_log"
@require_admin
async def get(self, request): async def get(self, request):
"""Retrieve API error log.""" """Retrieve API error log."""
if not request["hass_user"].is_admin:
raise Unauthorized()
return web.FileResponse(request.app["hass"].data[DATA_LOGGING]) return web.FileResponse(request.app["hass"].data[DATA_LOGGING])

View File

@@ -1,11 +1,11 @@
"""Arcam component.""" """Arcam component."""
import asyncio import asyncio
from asyncio import timeout
import logging import logging
from typing import Any from typing import Any
from arcam.fmj import ConnectionFailed from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client from arcam.fmj.client import Client
import async_timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.const import CONF_HOST, CONF_PORT, Platform
@@ -66,7 +66,7 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N
while True: while True:
try: try:
async with async_timeout.timeout(interval): async with timeout(interval):
await client.start() await client.start()
_LOGGER.debug("Client connected %s", client.host) _LOGGER.debug("Client connected %s", client.host)

View File

@@ -1,10 +1,11 @@
"""Arcam media player.""" """Arcam media player."""
from __future__ import annotations from __future__ import annotations
import functools
import logging import logging
from typing import Any from typing import Any
from arcam.fmj import SourceCodes from arcam.fmj import ConnectionFailed, SourceCodes
from arcam.fmj.state import State from arcam.fmj.state import State
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@@ -19,6 +20,7 @@ from homeassistant.components.media_player.errors import BrowseError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -57,6 +59,21 @@ async def async_setup_entry(
) )
def convert_exception(func):
"""Return decorator to convert a connection error into a home assistant error."""
@functools.wraps(func)
async def _convert_exception(*args, **kwargs):
try:
return await func(*args, **kwargs)
except ConnectionFailed as exception:
raise HomeAssistantError(
f"Connection failed to device during {func}"
) from exception
return _convert_exception
class ArcamFmj(MediaPlayerEntity): class ArcamFmj(MediaPlayerEntity):
"""Representation of a media device.""" """Representation of a media device."""
@@ -105,7 +122,10 @@ class ArcamFmj(MediaPlayerEntity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Once registered, add listener for events.""" """Once registered, add listener for events."""
await self._state.start() await self._state.start()
await self._state.update() try:
await self._state.update()
except ConnectionFailed as connection:
_LOGGER.debug("Connection lost during addition: %s", connection)
@callback @callback
def _data(host: str) -> None: def _data(host: str) -> None:
@@ -137,13 +157,18 @@ class ArcamFmj(MediaPlayerEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Force update of state.""" """Force update of state."""
_LOGGER.debug("Update state %s", self.name) _LOGGER.debug("Update state %s", self.name)
await self._state.update() try:
await self._state.update()
except ConnectionFailed as connection:
_LOGGER.debug("Connection lost during update: %s", connection)
@convert_exception
async def async_mute_volume(self, mute: bool) -> None: async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command.""" """Send mute command."""
await self._state.set_mute(mute) await self._state.set_mute(mute)
self.async_write_ha_state() self.async_write_ha_state()
@convert_exception
async def async_select_source(self, source: str) -> None: async def async_select_source(self, source: str) -> None:
"""Select a specific source.""" """Select a specific source."""
try: try:
@@ -155,31 +180,37 @@ class ArcamFmj(MediaPlayerEntity):
await self._state.set_source(value) await self._state.set_source(value)
self.async_write_ha_state() self.async_write_ha_state()
@convert_exception
async def async_select_sound_mode(self, sound_mode: str) -> None: async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Select a specific source.""" """Select a specific source."""
try: try:
await self._state.set_decode_mode(sound_mode) await self._state.set_decode_mode(sound_mode)
except (KeyError, ValueError): except (KeyError, ValueError) as exception:
_LOGGER.error("Unsupported sound_mode %s", sound_mode) raise HomeAssistantError(
return f"Unsupported sound_mode {sound_mode}"
) from exception
self.async_write_ha_state() self.async_write_ha_state()
@convert_exception
async def async_set_volume_level(self, volume: float) -> None: async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
await self._state.set_volume(round(volume * 99.0)) await self._state.set_volume(round(volume * 99.0))
self.async_write_ha_state() self.async_write_ha_state()
@convert_exception
async def async_volume_up(self) -> None: async def async_volume_up(self) -> None:
"""Turn volume up for media player.""" """Turn volume up for media player."""
await self._state.inc_volume() await self._state.inc_volume()
self.async_write_ha_state() self.async_write_ha_state()
@convert_exception
async def async_volume_down(self) -> None: async def async_volume_down(self) -> None:
"""Turn volume up for media player.""" """Turn volume up for media player."""
await self._state.dec_volume() await self._state.dec_volume()
self.async_write_ha_state() self.async_write_ha_state()
@convert_exception
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the media player on.""" """Turn the media player on."""
if self._state.get_power() is not None: if self._state.get_power() is not None:
@@ -189,6 +220,7 @@ class ArcamFmj(MediaPlayerEntity):
_LOGGER.debug("Firing event to turn on device") _LOGGER.debug("Firing event to turn on device")
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
@convert_exception
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Turn the media player off.""" """Turn the media player off."""
await self._state.set_power(False) await self._state.set_power(False)
@@ -230,6 +262,7 @@ class ArcamFmj(MediaPlayerEntity):
return root return root
@convert_exception
async def async_play_media( async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None: ) -> None:

View File

@@ -37,19 +37,18 @@ class AsekoBinarySensorEntityDescription(
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
AsekoBinarySensorEntityDescription( AsekoBinarySensorEntityDescription(
key="water_flow", key="water_flow",
name="Water Flow", translation_key="water_flow",
icon="mdi:waves-arrow-right", icon="mdi:waves-arrow-right",
value_fn=lambda unit: unit.water_flow, value_fn=lambda unit: unit.water_flow,
), ),
AsekoBinarySensorEntityDescription( AsekoBinarySensorEntityDescription(
key="has_alarm", key="has_alarm",
name="Alarm", translation_key="alarm",
value_fn=lambda unit: unit.has_alarm, value_fn=lambda unit: unit.has_alarm,
device_class=BinarySensorDeviceClass.SAFETY, device_class=BinarySensorDeviceClass.SAFETY,
), ),
AsekoBinarySensorEntityDescription( AsekoBinarySensorEntityDescription(
key="has_error", key="has_error",
name="Error",
value_fn=lambda unit: unit.has_error, value_fn=lambda unit: unit.has_error,
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
), ),

View File

@@ -11,6 +11,8 @@ from .coordinator import AsekoDataUpdateCoordinator
class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
"""Representation of an aseko entity.""" """Representation of an aseko entity."""
_attr_has_entity_name = True
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None: def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
"""Initialize the aseko entity.""" """Initialize the aseko entity."""
super().__init__(coordinator) super().__init__(coordinator)

View File

@@ -45,13 +45,16 @@ class VariableSensorEntity(AsekoEntity, SensorEntity):
super().__init__(unit, coordinator) super().__init__(unit, coordinator)
self._variable = variable self._variable = variable
variable_name = { translation_key = {
"Air temp.": "Air Temperature", "Air temp.": "air_temperature",
"Cl free": "Free Chlorine", "Cl free": "free_chlorine",
"Water temp.": "Water Temperature", "Water temp.": "water_temperature",
}.get(self._variable.name, self._variable.name) }.get(self._variable.name)
if translation_key is not None:
self._attr_translation_key = translation_key
else:
self._attr_name = self._variable.name
self._attr_name = f"{self._device_name} {variable_name}"
self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}" self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}"
self._attr_native_unit_of_measurement = self._variable.unit self._attr_native_unit_of_measurement = self._variable.unit

View File

@@ -16,5 +16,26 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]" "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
} }
},
"entity": {
"binary_sensor": {
"water_flow": {
"name": "Water flow"
},
"alarm": {
"name": "Alarm"
}
},
"sensor": {
"air_temperature": {
"name": "Air temperature"
},
"free_chlorine": {
"name": "Free chlorine"
},
"water_temperature": {
"name": "Water temperature"
}
}
} }
} }

View File

@@ -254,6 +254,8 @@ class PipelineEventType(StrEnum):
WAKE_WORD_START = "wake_word-start" WAKE_WORD_START = "wake_word-start"
WAKE_WORD_END = "wake_word-end" WAKE_WORD_END = "wake_word-end"
STT_START = "stt-start" STT_START = "stt-start"
STT_VAD_START = "stt-vad-start"
STT_VAD_END = "stt-vad-end"
STT_END = "stt-end" STT_END = "stt-end"
INTENT_START = "intent-start" INTENT_START = "intent-start"
INTENT_END = "intent-end" INTENT_END = "intent-end"
@@ -612,11 +614,31 @@ class PipelineRun:
stream: AsyncIterable[bytes], stream: AsyncIterable[bytes],
) -> AsyncGenerator[bytes, None]: ) -> AsyncGenerator[bytes, None]:
"""Stop stream when voice command is finished.""" """Stop stream when voice command is finished."""
sent_vad_start = False
timestamp_ms = 0
async for chunk in stream: async for chunk in stream:
if not segmenter.process(chunk): if not segmenter.process(chunk):
# Silence detected at the end of voice command
self.process_event(
PipelineEvent(
PipelineEventType.STT_VAD_END,
{"timestamp": timestamp_ms},
)
)
break break
if segmenter.in_command and (not sent_vad_start):
# Speech detected at start of voice command
self.process_event(
PipelineEvent(
PipelineEventType.STT_VAD_START,
{"timestamp": timestamp_ms},
)
)
sent_vad_start = True
yield chunk yield chunk
timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz
# Transcribe audio stream # Transcribe audio stream
result = await self.stt_provider.async_process_audio_stream( result = await self.stt_provider.async_process_audio_stream(

View File

@@ -7,7 +7,6 @@ from collections.abc import AsyncGenerator, Callable
import logging import logging
from typing import Any from typing import Any
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components import conversation, stt, tts, websocket_api from homeassistant.components import conversation, stt, tts, websocket_api
@@ -207,7 +206,7 @@ async def websocket_run(
try: try:
# Task contains a timeout # Task contains a timeout
async with async_timeout.timeout(timeout): async with asyncio.timeout(timeout):
await run_task await run_task
except asyncio.TimeoutError: except asyncio.TimeoutError:
pipeline_input.run.process_event( pipeline_input.run.process_event(

View File

@@ -1,8 +1,8 @@
"""The ATAG Integration.""" """The ATAG Integration."""
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
import async_timeout
from pyatag import AtagException, AtagOne from pyatag import AtagException, AtagOne
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_update_data(): async def _async_update_data():
"""Update data via library.""" """Update data via library."""
async with async_timeout.timeout(20): async with timeout(20):
try: try:
await atag.update() await atag.update()
except AtagException as err: except AtagException as err:

View File

@@ -109,10 +109,6 @@ def _native_datetime() -> datetime:
class AugustBinarySensorEntityDescription(BinarySensorEntityDescription): class AugustBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes August binary_sensor entity.""" """Describes August binary_sensor entity."""
# AugustBinarySensor does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
@dataclass @dataclass
class AugustDoorbellRequiredKeysMixin: class AugustDoorbellRequiredKeysMixin:
@@ -128,34 +124,28 @@ class AugustDoorbellBinarySensorEntityDescription(
): ):
"""Describes August binary_sensor entity.""" """Describes August binary_sensor entity."""
# AugustDoorbellBinarySensor does not support UNDEFINED or None,
# restrict the type to str.
name: str = ""
SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription( SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription(
key="door_open", key="open",
name="Open", device_class=BinarySensorDeviceClass.DOOR,
) )
SENSOR_TYPES_VIDEO_DOORBELL = ( SENSOR_TYPES_VIDEO_DOORBELL = (
AugustDoorbellBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_motion", key="motion",
name="Motion",
device_class=BinarySensorDeviceClass.MOTION, device_class=BinarySensorDeviceClass.MOTION,
value_fn=_retrieve_motion_state, value_fn=_retrieve_motion_state,
is_time_based=True, is_time_based=True,
), ),
AugustDoorbellBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_image_capture", key="image capture",
name="Image Capture", translation_key="image_capture",
icon="mdi:file-image", icon="mdi:file-image",
value_fn=_retrieve_image_capture_state, value_fn=_retrieve_image_capture_state,
is_time_based=True, is_time_based=True,
), ),
AugustDoorbellBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_online", key="online",
name="Online",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
value_fn=_retrieve_online_state, value_fn=_retrieve_online_state,
@@ -166,8 +156,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ( SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
AugustDoorbellBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_ding", key="ding",
name="Ding",
device_class=BinarySensorDeviceClass.OCCUPANCY, device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=_retrieve_ding_state, value_fn=_retrieve_ding_state,
is_time_based=True, is_time_based=True,
@@ -236,8 +225,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
self.entity_description = description self.entity_description = description
self._data = data self._data = data
self._device = device self._device = device
self._attr_name = f"{device.device_name} {description.name}" self._attr_unique_id = f"{self._device_id}_{description.key}"
self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
@callback @callback
def _update_from_data(self): def _update_from_data(self):
@@ -284,8 +272,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
self.entity_description = description self.entity_description = description
self._check_for_off_update_listener = None self._check_for_off_update_listener = None
self._data = data self._data = data
self._attr_name = f"{device.device_name} {description.name}" self._attr_unique_id = f"{self._device_id}_{description.key}"
self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
@callback @callback
def _update_from_data(self): def _update_from_data(self):

View File

@@ -24,10 +24,11 @@ async def async_setup_entry(
class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): class AugustWakeLockButton(AugustEntityMixin, ButtonEntity):
"""Representation of an August lock wake button.""" """Representation of an August lock wake button."""
_attr_translation_key = "wake"
def __init__(self, data: AugustData, device: Lock) -> None: def __init__(self, data: AugustData, device: Lock) -> None:
"""Initialize the lock wake button.""" """Initialize the lock wake button."""
super().__init__(data, device) super().__init__(data, device)
self._attr_name = f"{device.device_name} Wake"
self._attr_unique_id = f"{self._device_id}_wake" self._attr_unique_id = f"{self._device_id}_wake"
async def async_press(self) -> None: async def async_press(self) -> None:

View File

@@ -33,16 +33,17 @@ async def async_setup_entry(
class AugustCamera(AugustEntityMixin, Camera): class AugustCamera(AugustEntityMixin, Camera):
"""An implementation of a August security camera.""" """An implementation of an August security camera."""
_attr_translation_key = "camera"
def __init__(self, data, device, session, timeout): def __init__(self, data, device, session, timeout):
"""Initialize a August security camera.""" """Initialize an August security camera."""
super().__init__(data, device) super().__init__(data, device)
self._timeout = timeout self._timeout = timeout
self._session = session self._session = session
self._image_url = None self._image_url = None
self._image_content = None self._image_content = None
self._attr_name = f"{device.device_name} Camera"
self._attr_unique_id = f"{self._device_id:s}_camera" self._attr_unique_id = f"{self._device_id:s}_camera"
@property @property

View File

@@ -19,6 +19,7 @@ class AugustEntityMixin(Entity):
"""Base implementation for August device.""" """Base implementation for August device."""
_attr_should_poll = False _attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, data: AugustData, device: Doorbell | Lock) -> None: def __init__(self, data: AugustData, device: Doorbell | Lock) -> None:
"""Initialize an August device.""" """Initialize an August device."""

View File

@@ -37,11 +37,12 @@ async def async_setup_entry(
class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
"""Representation of an August lock.""" """Representation of an August lock."""
_attr_name = None
def __init__(self, data, device): def __init__(self, data, device):
"""Initialize the lock.""" """Initialize the lock."""
super().__init__(data, device) super().__init__(data, device)
self._lock_status = None self._lock_status = None
self._attr_name = device.device_name
self._attr_unique_id = f"{self._device_id:s}_lock" self._attr_unique_id = f"{self._device_id:s}_lock"
self._update_from_data() self._update_from_data()

View File

@@ -75,7 +75,6 @@ class AugustSensorEntityDescription(
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
key="device_battery", key="device_battery",
name="Battery",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=_retrieve_device_battery_state, value_fn=_retrieve_device_battery_state,
@@ -83,7 +82,6 @@ SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail](
key="linked_keypad_battery", key="linked_keypad_battery",
name="Battery",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=_retrieve_linked_keypad_battery_state, value_fn=_retrieve_linked_keypad_battery_state,
@@ -176,6 +174,8 @@ async def _async_migrate_old_unique_ids(hass, devices):
class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
"""Representation of an August lock operation sensor.""" """Representation of an August lock operation sensor."""
_attr_translation_key = "operator"
def __init__(self, data, device): def __init__(self, data, device):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(data, device) super().__init__(data, device)
@@ -188,11 +188,6 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
self._entity_picture = None self._entity_picture = None
self._update_from_data() self._update_from_data()
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._device.device_name} Operator"
@callback @callback
def _update_from_data(self): def _update_from_data(self):
"""Get the latest state of the sensor and update activity.""" """Get the latest state of the sensor and update activity."""
@@ -278,7 +273,6 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]):
super().__init__(data, device) super().__init__(data, device)
self.entity_description = description self.entity_description = description
self._old_device = old_device self._old_device = old_device
self._attr_name = f"{device.device_name} {description.name}"
self._attr_unique_id = f"{self._device_id}_{description.key}" self._attr_unique_id = f"{self._device_id}_{description.key}"
self._update_from_data() self._update_from_data()

View File

@@ -37,5 +37,27 @@
"title": "Reauthenticate an August account" "title": "Reauthenticate an August account"
} }
} }
},
"entity": {
"binary_sensor": {
"image_capture": {
"name": "Image capture"
}
},
"button": {
"wake": {
"name": "Wake"
}
},
"camera": {
"camera": {
"name": "[%key:component::camera::title%]"
}
},
"sensor": {
"operator": {
"name": "Operator"
}
}
} }
} }

View File

@@ -13,9 +13,12 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the binary_sensor platform.""" """Set up the binary_sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
name = f"{coordinator.name} Aurora Visibility Alert"
entity = AuroraSensor(coordinator=coordinator, name=name, icon="mdi:hazard-lights") entity = AuroraSensor(
coordinator=coordinator,
translation_key="visibility_alert",
icon="mdi:hazard-lights",
)
async_add_entries([entity]) async_add_entries([entity])

View File

@@ -19,14 +19,14 @@ class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]):
def __init__( def __init__(
self, self,
coordinator: AuroraDataUpdateCoordinator, coordinator: AuroraDataUpdateCoordinator,
name: str, translation_key: str,
icon: str, icon: str,
) -> None: ) -> None:
"""Initialize the Aurora Entity.""" """Initialize the Aurora Entity."""
super().__init__(coordinator=coordinator) super().__init__(coordinator=coordinator)
self._attr_name = name self._attr_translation_key = translation_key
self._attr_unique_id = f"{coordinator.latitude}_{coordinator.longitude}" self._attr_unique_id = f"{coordinator.latitude}_{coordinator.longitude}"
self._attr_icon = icon self._attr_icon = icon

View File

@@ -17,7 +17,7 @@ async def async_setup_entry(
entity = AuroraSensor( entity = AuroraSensor(
coordinator=coordinator, coordinator=coordinator,
name=f"{coordinator.name} Aurora Visibility %", translation_key="visibility",
icon="mdi:gauge", icon="mdi:gauge",
) )

View File

@@ -25,5 +25,17 @@
} }
} }
} }
},
"entity": {
"binary_sensor": {
"visibility_alert": {
"name": "Visibility alert"
}
},
"sensor": {
"visibility": {
"name": "Visibility"
}
}
} }
} }

View File

@@ -1,12 +1,11 @@
"""The awair component.""" """The awair component."""
from __future__ import annotations from __future__ import annotations
from asyncio import gather from asyncio import gather, timeout
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from aiohttp import ClientSession from aiohttp import ClientSession
from async_timeout import timeout
from python_awair import Awair, AwairLocal from python_awair import Awair, AwairLocal
from python_awair.air_data import AirData from python_awair.air_data import AirData
from python_awair.devices import AwairBaseDevice, AwairLocalDevice from python_awair.devices import AwairBaseDevice, AwairLocalDevice

View File

@@ -1,10 +1,10 @@
"""Axis network device abstraction.""" """Axis network device abstraction."""
import asyncio import asyncio
from asyncio import timeout
from types import MappingProxyType from types import MappingProxyType
from typing import Any from typing import Any
import async_timeout
import axis import axis
from axis.configuration import Configuration from axis.configuration import Configuration
from axis.errors import Unauthorized from axis.errors import Unauthorized
@@ -253,7 +253,7 @@ async def get_axis_device(
) )
try: try:
async with async_timeout.timeout(30): async with timeout(30):
await device.vapix.initialize() await device.vapix.initialize()
return device return device

View File

@@ -2,10 +2,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from aiobafi6 import Device, Service from aiobafi6 import Device, Service
from aiobafi6.discovery import PORT from aiobafi6.discovery import PORT
import async_timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, Platform from homeassistant.const import CONF_IP_ADDRESS, Platform
@@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
run_future = device.async_run() run_future = device.async_run()
try: try:
async with async_timeout.timeout(RUN_TIMEOUT): async with timeout(RUN_TIMEOUT):
await device.async_wait_available() await device.async_wait_available()
except asyncio.TimeoutError as ex: except asyncio.TimeoutError as ex:
run_future.cancel() run_future.cancel()

View File

@@ -2,12 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
import logging import logging
from typing import Any from typing import Any
from aiobafi6 import Device, Service from aiobafi6 import Device, Service
from aiobafi6.discovery import PORT from aiobafi6.discovery import PORT
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@@ -27,7 +27,7 @@ async def async_try_connect(ip_address: str) -> Device:
device = Device(Service(ip_addresses=[ip_address], port=PORT)) device = Device(Service(ip_addresses=[ip_address], port=PORT))
run_future = device.async_run() run_future = device.async_run()
try: try:
async with async_timeout.timeout(RUN_TIMEOUT): async with timeout(RUN_TIMEOUT):
await device.async_wait_available() await device.async_wait_available()
except asyncio.TimeoutError as ex: except asyncio.TimeoutError as ex:
raise CannotConnect from ex raise CannotConnect from ex

View File

@@ -1,9 +1,9 @@
"""Websocket API for blueprint.""" """Websocket API for blueprint."""
from __future__ import annotations from __future__ import annotations
import asyncio
from typing import Any, cast from typing import Any, cast
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
@@ -72,7 +72,7 @@ async def ws_import_blueprint(
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Import a blueprint.""" """Import a blueprint."""
async with async_timeout.timeout(10): async with asyncio.timeout(10):
imported_blueprint = await importer.fetch_blueprint_from_url(hass, msg["url"]) imported_blueprint = await importer.fetch_blueprint_from_url(hass, msg["url"])
if imported_blueprint is None: if imported_blueprint is None:

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import CancelledError from asyncio import CancelledError, timeout
from datetime import timedelta from datetime import timedelta
from http import HTTPStatus from http import HTTPStatus
import logging import logging
@@ -12,7 +12,6 @@ from urllib import parse
import aiohttp import aiohttp
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
from aiohttp.hdrs import CONNECTION, KEEP_ALIVE from aiohttp.hdrs import CONNECTION, KEEP_ALIVE
import async_timeout
import voluptuous as vol import voluptuous as vol
import xmltodict import xmltodict
@@ -355,7 +354,7 @@ class BluesoundPlayer(MediaPlayerEntity):
try: try:
websession = async_get_clientsession(self._hass) websession = async_get_clientsession(self._hass)
async with async_timeout.timeout(10): async with timeout(10):
response = await websession.get(url) response = await websession.get(url)
if response.status == HTTPStatus.OK: if response.status == HTTPStatus.OK:
@@ -396,7 +395,7 @@ class BluesoundPlayer(MediaPlayerEntity):
_LOGGER.debug("Calling URL: %s", url) _LOGGER.debug("Calling URL: %s", url)
try: try:
async with async_timeout.timeout(125): async with timeout(125):
response = await self._polling_session.get( response = await self._polling_session.get(
url, headers={CONNECTION: KEEP_ALIVE} url, headers={CONNECTION: KEEP_ALIVE}
) )

View File

@@ -4,11 +4,11 @@ These APIs are the only documented way to interact with the bluetooth integratio
""" """
from __future__ import annotations from __future__ import annotations
import asyncio
from asyncio import Future from asyncio import Future
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
import async_timeout
from home_assistant_bluetooth import BluetoothServiceInfoBleak from home_assistant_bluetooth import BluetoothServiceInfoBleak
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
@@ -152,7 +152,7 @@ async def async_process_advertisements(
) )
try: try:
async with async_timeout.timeout(timeout): async with asyncio.timeout(timeout):
return await done return await done
finally: finally:
unload() unload()

View File

@@ -19,6 +19,6 @@
"bluetooth-adapters==0.16.0", "bluetooth-adapters==0.16.0",
"bluetooth-auto-recovery==1.2.1", "bluetooth-auto-recovery==1.2.1",
"bluetooth-data-tools==1.8.0", "bluetooth-data-tools==1.8.0",
"dbus-fast==1.91.2" "dbus-fast==1.91.4"
] ]
} }

View File

@@ -8,7 +8,6 @@ import logging
import platform import platform
from typing import Any from typing import Any
import async_timeout
import bleak import bleak
from bleak import BleakError from bleak import BleakError
from bleak.assigned_numbers import AdvertisementDataType from bleak.assigned_numbers import AdvertisementDataType
@@ -220,7 +219,7 @@ class HaScanner(BaseHaScanner):
START_ATTEMPTS, START_ATTEMPTS,
) )
try: try:
async with async_timeout.timeout(START_TIMEOUT): async with asyncio.timeout(START_TIMEOUT):
await self.scanner.start() # type: ignore[no-untyped-call] await self.scanner.start() # type: ignore[no-untyped-call]
except InvalidMessageError as ex: except InvalidMessageError as ex:
_LOGGER.debug( _LOGGER.debug(

View File

@@ -25,7 +25,9 @@ async def async_setup_entry(
entities: list[BinarySensorEntity] = [] entities: list[BinarySensorEntity] = []
session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION]
for binary_sensor in session.device_helper.shutter_contacts: for binary_sensor in (
session.device_helper.shutter_contacts + session.device_helper.shutter_contacts2
):
entities.append( entities.append(
ShutterContactSensor( ShutterContactSensor(
device=binary_sensor, device=binary_sensor,
@@ -37,6 +39,7 @@ async def async_setup_entry(
for binary_sensor in ( for binary_sensor in (
session.device_helper.motion_detectors session.device_helper.motion_detectors
+ session.device_helper.shutter_contacts + session.device_helper.shutter_contacts
+ session.device_helper.shutter_contacts2
+ session.device_helper.smoke_detectors + session.device_helper.smoke_detectors
+ session.device_helper.thermostats + session.device_helper.thermostats
+ session.device_helper.twinguards + session.device_helper.twinguards

View File

@@ -1,10 +1,10 @@
"""The Brother component.""" """The Brother component."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
import async_timeout
from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -79,7 +79,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]):
async def _async_update_data(self) -> BrotherSensors: async def _async_update_data(self) -> BrotherSensors:
"""Update data via library.""" """Update data via library."""
try: try:
async with async_timeout.timeout(20): async with timeout(20):
data = await self.brother.async_update() data = await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModelError) as error: except (ConnectionError, SnmpError, UnsupportedModelError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error

View File

@@ -1,10 +1,10 @@
"""The brunt component.""" """The brunt component."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
import logging import logging
from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
import async_timeout
from brunt import BruntClientAsync, Thing from brunt import BruntClientAsync, Thing
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account. Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account.
""" """
try: try:
async with async_timeout.timeout(10): async with timeout(10):
things = await bapi.async_get_things(force=True) things = await bapi.async_get_things(force=True)
return {thing.serial: thing for thing in things} return {thing.serial: thing for thing in things}
except ServerDisconnectedError as err: except ServerDisconnectedError as err:

View File

@@ -14,10 +14,10 @@ CONF_TIMEFRAME = "timeframe"
SUPPORTED_COUNTRY_CODES = ["NL", "BE"] SUPPORTED_COUNTRY_CODES = ["NL", "BE"]
DEFAULT_COUNTRY = "NL" DEFAULT_COUNTRY = "NL"
"""Schedule next call after (minutes)."""
SCHEDULE_OK = 10 SCHEDULE_OK = 10
"""When an error occurred, new call after (minutes).""" """Schedule next call after (minutes)."""
SCHEDULE_NOK = 2 SCHEDULE_NOK = 2
"""When an error occurred, new call after (minutes)."""
STATE_CONDITIONS = ["clear", "cloudy", "fog", "rainy", "snowy", "lightning"] STATE_CONDITIONS = ["clear", "cloudy", "fog", "rainy", "snowy", "lightning"]

View File

@@ -714,17 +714,18 @@ async def async_setup_entry(
timeframe, timeframe,
) )
# create weather entities:
entities = [ entities = [
BrSensor(config.get(CONF_NAME, "Buienradar"), coordinates, description) BrSensor(config.get(CONF_NAME, "Buienradar"), coordinates, description)
for description in SENSOR_TYPES for description in SENSOR_TYPES
] ]
async_add_entities(entities) # create weather data:
data = BrData(hass, coordinates, timeframe, entities) data = BrData(hass, coordinates, timeframe, entities)
# schedule the first update in 1 minute from now:
await data.schedule_update(1)
hass.data[DOMAIN][entry.entry_id][Platform.SENSOR] = data hass.data[DOMAIN][entry.entry_id][Platform.SENSOR] = data
await data.async_update()
async_add_entities(entities)
class BrSensor(SensorEntity): class BrSensor(SensorEntity):
@@ -753,9 +754,9 @@ class BrSensor(SensorEntity):
self._timeframe = None self._timeframe = None
@callback @callback
def data_updated(self, data): def data_updated(self, data: BrData):
"""Update data.""" """Update data."""
if self.hass and self._load_data(data): if self._load_data(data.data) and self.hass:
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback

View File

@@ -1,11 +1,11 @@
"""Shared utilities for different supported platforms.""" """Shared utilities for different supported platforms."""
import asyncio import asyncio
from asyncio import timeout
from datetime import datetime, timedelta from datetime import datetime, timedelta
from http import HTTPStatus from http import HTTPStatus
import logging import logging
import aiohttp import aiohttp
import async_timeout
from buienradar.buienradar import parse_data from buienradar.buienradar import parse_data
from buienradar.constants import ( from buienradar.constants import (
ATTRIBUTION, ATTRIBUTION,
@@ -27,7 +27,7 @@ from buienradar.constants import (
from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import CALLBACK_TYPE from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -75,9 +75,10 @@ class BrData:
# Update all devices # Update all devices
for dev in self.devices: for dev in self.devices:
dev.data_updated(self.data) dev.data_updated(self)
async def schedule_update(self, minute=1): @callback
def async_schedule_update(self, minute=1):
"""Schedule an update after minute minutes.""" """Schedule an update after minute minutes."""
_LOGGER.debug("Scheduling next update in %s minutes", minute) _LOGGER.debug("Scheduling next update in %s minutes", minute)
nxt = dt_util.utcnow() + timedelta(minutes=minute) nxt = dt_util.utcnow() + timedelta(minutes=minute)
@@ -92,7 +93,7 @@ class BrData:
resp = None resp = None
try: try:
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
async with async_timeout.timeout(10): async with timeout(10):
resp = await websession.get(url) resp = await websession.get(url)
result[STATUS_CODE] = resp.status result[STATUS_CODE] = resp.status
@@ -108,9 +109,9 @@ class BrData:
return result return result
finally: finally:
if resp is not None: if resp is not None:
await resp.release() resp.release()
async def async_update(self, *_): async def _async_update(self):
"""Update the data from buienradar.""" """Update the data from buienradar."""
content = await self.get_data(JSON_FEED_URL) content = await self.get_data(JSON_FEED_URL)
@@ -123,9 +124,7 @@ class BrData:
content.get(MESSAGE), content.get(MESSAGE),
content.get(STATUS_CODE), content.get(STATUS_CODE),
) )
# schedule new call return None
await self.schedule_update(SCHEDULE_NOK)
return
self.load_error_count = 0 self.load_error_count = 0
# rounding coordinates prevents unnecessary redirects/calls # rounding coordinates prevents unnecessary redirects/calls
@@ -143,9 +142,7 @@ class BrData:
raincontent.get(MESSAGE), raincontent.get(MESSAGE),
raincontent.get(STATUS_CODE), raincontent.get(STATUS_CODE),
) )
# schedule new call return None
await self.schedule_update(SCHEDULE_NOK)
return
self.rain_error_count = 0 self.rain_error_count = 0
result = parse_data( result = parse_data(
@@ -164,12 +161,21 @@ class BrData:
"Unable to parse data from Buienradar. (Msg: %s)", "Unable to parse data from Buienradar. (Msg: %s)",
result.get(MESSAGE), result.get(MESSAGE),
) )
await self.schedule_update(SCHEDULE_NOK) return None
return result[DATA]
async def async_update(self, *_):
"""Update the data from buienradar and schedule the next update."""
data = await self._async_update()
if data is None:
self.async_schedule_update(SCHEDULE_NOK)
return return
self.data = result.get(DATA) self.data = data
await self.update_devices() await self.update_devices()
await self.schedule_update(SCHEDULE_OK) self.async_schedule_update(SCHEDULE_OK)
@property @property
def attribution(self): def attribution(self):

View File

@@ -34,7 +34,9 @@ from homeassistant.components.weather import (
ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
Forecast,
WeatherEntity, WeatherEntity,
WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -48,7 +50,7 @@ from homeassistant.const import (
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
# Reuse data and API logic from the sensor implementation # Reuse data and API logic from the sensor implementation
@@ -82,6 +84,11 @@ CONDITION_CLASSES = {
ATTR_CONDITION_WINDY_VARIANT: (), ATTR_CONDITION_WINDY_VARIANT: (),
ATTR_CONDITION_EXCEPTIONAL: (), ATTR_CONDITION_EXCEPTIONAL: (),
} }
CONDITION_MAP = {
cond_code: cond_ha
for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes
}
async def async_setup_entry( async def async_setup_entry(
@@ -99,24 +106,16 @@ async def async_setup_entry(
coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)}
# create weather data: # create weather entity:
data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, None)
hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data
# create weather device:
_LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates)
entities = [BrWeather(config, coordinates)]
# create condition helper # create weather data:
if DATA_CONDITION not in hass.data[DOMAIN]: data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, entities)
cond_keys = [str(chr(x)) for x in range(97, 123)] hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data
hass.data[DOMAIN][DATA_CONDITION] = dict.fromkeys(cond_keys) await data.async_update()
for cond, condlst in CONDITION_CLASSES.items():
for condi in condlst:
hass.data[DOMAIN][DATA_CONDITION][condi] = cond
async_add_entities([BrWeather(data, config, coordinates)]) async_add_entities(entities)
# schedule the first update in 1 minute from now:
await data.schedule_update(1)
class BrWeather(WeatherEntity): class BrWeather(WeatherEntity):
@@ -127,81 +126,62 @@ class BrWeather(WeatherEntity):
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_visibility_unit = UnitOfLength.METERS _attr_native_visibility_unit = UnitOfLength.METERS
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
_attr_should_poll = False
_attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
def __init__(self, data, config, coordinates): def __init__(self, config, coordinates):
"""Initialize the platform with a data instance and station name.""" """Initialize the platform with a data instance and station name."""
self._stationname = config.get(CONF_NAME, "Buienradar") self._stationname = config.get(CONF_NAME, "Buienradar")
self._attr_name = ( self._attr_name = self._stationname or f"BR {'(unknown station)'}"
self._stationname or f"BR {data.stationname or '(unknown station)'}"
)
self._data = data
self._attr_unique_id = "{:2.6f}{:2.6f}".format( self._attr_unique_id = "{:2.6f}{:2.6f}".format(
coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE] coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE]
) )
@property @callback
def attribution(self): def data_updated(self, data: BrData) -> None:
"""Return the attribution.""" """Update data."""
return self._data.attribution self._attr_attribution = data.attribution
self._attr_condition = self._calc_condition(data)
self._attr_forecast = self._calc_forecast(data)
self._attr_humidity = data.humidity
self._attr_name = (
self._stationname or f"BR {data.stationname or '(unknown station)'}"
)
self._attr_native_pressure = data.pressure
self._attr_native_temperature = data.temperature
self._attr_native_visibility = data.visibility
self._attr_native_wind_speed = data.wind_speed
self._attr_wind_bearing = data.wind_bearing
@property if not self.hass:
def condition(self): return
self.async_write_ha_state()
assert self.platform.config_entry
self.platform.config_entry.async_create_task(
self.hass, self.async_update_listeners(("daily",))
)
def _calc_condition(self, data: BrData):
"""Return the current condition.""" """Return the current condition."""
if ( if data.condition and (ccode := data.condition.get(CONDCODE)):
self._data return CONDITION_MAP.get(ccode)
and self._data.condition return None
and (ccode := self._data.condition.get(CONDCODE))
and (conditions := self.hass.data[DOMAIN].get(DATA_CONDITION))
):
return conditions.get(ccode)
@property def _calc_forecast(self, data: BrData):
def native_temperature(self):
"""Return the current temperature."""
return self._data.temperature
@property
def native_pressure(self):
"""Return the current pressure."""
return self._data.pressure
@property
def humidity(self):
"""Return the name of the sensor."""
return self._data.humidity
@property
def native_visibility(self):
"""Return the current visibility in m."""
return self._data.visibility
@property
def native_wind_speed(self):
"""Return the current windspeed in m/s."""
return self._data.wind_speed
@property
def wind_bearing(self):
"""Return the current wind bearing (degrees)."""
return self._data.wind_bearing
@property
def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
fcdata_out = [] fcdata_out = []
cond = self.hass.data[DOMAIN][DATA_CONDITION]
if not self._data.forecast: if not data.forecast:
return None return None
for data_in in self._data.forecast: for data_in in data.forecast:
# remap keys from external library to # remap keys from external library to
# keys understood by the weather component: # keys understood by the weather component:
condcode = data_in.get(CONDITION, []).get(CONDCODE) condcode = data_in.get(CONDITION, {}).get(CONDCODE)
data_out = { data_out = {
ATTR_FORECAST_TIME: data_in.get(DATETIME).isoformat(), ATTR_FORECAST_TIME: data_in.get(DATETIME).isoformat(),
ATTR_FORECAST_CONDITION: cond[condcode], ATTR_FORECAST_CONDITION: CONDITION_MAP.get(condcode),
ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.get(MIN_TEMP), ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.get(MIN_TEMP),
ATTR_FORECAST_NATIVE_TEMP: data_in.get(MAX_TEMP), ATTR_FORECAST_NATIVE_TEMP: data_in.get(MAX_TEMP),
ATTR_FORECAST_NATIVE_PRECIPITATION: data_in.get(RAIN), ATTR_FORECAST_NATIVE_PRECIPITATION: data_in.get(RAIN),
@@ -212,3 +192,7 @@ class BrWeather(WeatherEntity):
fcdata_out.append(data_out) fcdata_out.append(data_out)
return fcdata_out return fcdata_out
async def async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units."""
return self._attr_forecast

View File

@@ -87,6 +87,7 @@ def setup_platform(
calendars = client.principal().calendars() calendars = client.principal().calendars()
calendar_devices = [] calendar_devices = []
device_id: str | None
for calendar in list(calendars): for calendar in list(calendars):
# If a calendar name was given in the configuration, # If a calendar name was given in the configuration,
# ignore all the others # ignore all the others

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav", "documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"], "loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.2.0"] "requirements": ["caldav==1.3.6"]
} }

View File

@@ -15,7 +15,6 @@ from random import SystemRandom
from typing import Any, Final, cast, final from typing import Any, Final, cast, final
from aiohttp import hdrs, web from aiohttp import hdrs, web
import async_timeout
import attr import attr
import voluptuous as vol import voluptuous as vol
@@ -168,7 +167,7 @@ async def _async_get_image(
are handled. are handled.
""" """
with suppress(asyncio.CancelledError, asyncio.TimeoutError): with suppress(asyncio.CancelledError, asyncio.TimeoutError):
async with async_timeout.timeout(timeout): async with asyncio.timeout(timeout):
if image_bytes := await camera.async_camera_image( if image_bytes := await camera.async_camera_image(
width=width, height=height width=width, height=height
): ):
@@ -525,7 +524,7 @@ class Camera(Entity):
self._create_stream_lock = asyncio.Lock() self._create_stream_lock = asyncio.Lock()
async with self._create_stream_lock: async with self._create_stream_lock:
if not self.stream: if not self.stream:
async with async_timeout.timeout(CAMERA_STREAM_SOURCE_TIMEOUT): async with asyncio.timeout(CAMERA_STREAM_SOURCE_TIMEOUT):
source = await self.stream_source() source = await self.stream_source()
if not source: if not source:
return None return None

View File

@@ -1,11 +1,11 @@
"""Provides the Canary DataUpdateCoordinator.""" """Provides the Canary DataUpdateCoordinator."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import ValuesView from collections.abc import ValuesView
from datetime import timedelta from datetime import timedelta
import logging import logging
from async_timeout import timeout
from canary.api import Api from canary.api import Api
from canary.model import Location, Reading from canary.model import Location, Reading
from requests.exceptions import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
@@ -58,7 +58,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]):
"""Fetch data from Canary.""" """Fetch data from Canary."""
try: try:
async with timeout(15): async with asyncio.timeout(15):
return await self.hass.async_add_executor_job(self._update_data) return await self.hass.async_add_executor_job(self._update_data)
except (ConnectTimeout, HTTPError) as error: except (ConnectTimeout, HTTPError) as error:
raise UpdateFailed(f"Invalid response from API: {error}") from error raise UpdateFailed(f"Invalid response from API: {error}") from error

View File

@@ -77,6 +77,7 @@ class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]):
"""Defines a base Cert Expiry entity.""" """Defines a base Cert Expiry entity."""
_attr_icon = "mdi:certificate" _attr_icon = "mdi:certificate"
_attr_has_entity_name = True
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@@ -91,6 +92,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
"""Implementation of the Cert Expiry timestamp sensor.""" """Implementation of the Cert Expiry timestamp sensor."""
_attr_device_class = SensorDeviceClass.TIMESTAMP _attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_translation_key = "certificate_expiry"
def __init__( def __init__(
self, self,
@@ -98,7 +100,6 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
) -> None: ) -> None:
"""Initialize a Cert Expiry timestamp sensor.""" """Initialize a Cert Expiry timestamp sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})"
self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{coordinator.host}:{coordinator.port}")}, identifiers={(DOMAIN, f"{coordinator.host}:{coordinator.port}")},

View File

@@ -20,5 +20,12 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"import_failed": "Import from config failed" "import_failed": "Import from config failed"
} }
},
"entity": {
"sensor": {
"certificate_expiry": {
"name": "Cert expiry"
}
}
} }
} }

View File

@@ -6,7 +6,6 @@ from datetime import timedelta
import logging import logging
import aiohttp import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@@ -140,7 +139,7 @@ async def async_citybikes_request(hass, uri, schema):
try: try:
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
req = await session.get(DEFAULT_ENDPOINT.format(uri=uri)) req = await session.get(DEFAULT_ENDPOINT.format(uri=uri))
json_response = await req.json() json_response = await req.json()

View File

@@ -10,7 +10,6 @@ import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import aiohttp import aiohttp
import async_timeout
from hass_nabucasa import Cloud, cloud_api from hass_nabucasa import Cloud, cloud_api
from yarl import URL from yarl import URL
@@ -501,7 +500,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
) )
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
return True return True

View File

@@ -54,7 +54,6 @@ class CloudClient(Interface):
@property @property
def base_path(self) -> Path: def base_path(self) -> Path:
"""Return path to base dir.""" """Return path to base dir."""
assert self._hass.config.config_dir is not None
return Path(self._hass.config.config_dir) return Path(self._hass.config.config_dir)
@property @property

View File

@@ -10,7 +10,6 @@ from typing import Any, Concatenate, ParamSpec, TypeVar
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
import async_timeout
import attr import attr
from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa import Cloud, auth, thingtalk
from hass_nabucasa.const import STATE_DISCONNECTED from hass_nabucasa.const import STATE_DISCONNECTED
@@ -252,7 +251,7 @@ class CloudLogoutView(HomeAssistantView):
hass = request.app["hass"] hass = request.app["hass"]
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
await cloud.logout() await cloud.logout()
return self.json_message("ok") return self.json_message("ok")
@@ -292,7 +291,7 @@ class CloudRegisterView(HomeAssistantView):
if location_info.zip_code is not None: if location_info.zip_code is not None:
client_metadata["NC_ZIP_CODE"] = location_info.zip_code client_metadata["NC_ZIP_CODE"] = location_info.zip_code
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
await cloud.auth.async_register( await cloud.auth.async_register(
data["email"], data["email"],
data["password"], data["password"],
@@ -316,7 +315,7 @@ class CloudResendConfirmView(HomeAssistantView):
hass = request.app["hass"] hass = request.app["hass"]
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
await cloud.auth.async_resend_email_confirm(data["email"]) await cloud.auth.async_resend_email_confirm(data["email"])
return self.json_message("ok") return self.json_message("ok")
@@ -336,7 +335,7 @@ class CloudForgotPasswordView(HomeAssistantView):
hass = request.app["hass"] hass = request.app["hass"]
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
await cloud.auth.async_forgot_password(data["email"]) await cloud.auth.async_forgot_password(data["email"])
return self.json_message("ok") return self.json_message("ok")
@@ -439,7 +438,7 @@ async def websocket_update_prefs(
if changes.get(PREF_ALEXA_REPORT_STATE): if changes.get(PREF_ALEXA_REPORT_STATE):
alexa_config = await cloud.client.get_alexa_config() alexa_config = await cloud.client.get_alexa_config()
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
await alexa_config.async_get_access_token() await alexa_config.async_get_access_token()
except asyncio.TimeoutError: except asyncio.TimeoutError:
connection.send_error( connection.send_error(
@@ -779,7 +778,7 @@ async def alexa_sync(
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
alexa_config = await cloud.client.get_alexa_config() alexa_config = await cloud.client.get_alexa_config()
async with async_timeout.timeout(10): async with asyncio.timeout(10):
try: try:
success = await alexa_config.async_sync_entities() success = await alexa_config.async_sync_entities()
except alexa_errors.NoTokenAvailable: except alexa_errors.NoTokenAvailable:
@@ -808,7 +807,7 @@ async def thingtalk_convert(
"""Convert a query.""" """Convert a query."""
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
async with async_timeout.timeout(10): async with asyncio.timeout(10):
try: try:
connection.send_result( connection.send_result(
msg["id"], await thingtalk.async_convert(cloud, msg["query"]) msg["id"], await thingtalk.async_convert(cloud, msg["query"])

View File

@@ -6,7 +6,6 @@ import logging
from typing import Any from typing import Any
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
import async_timeout
from hass_nabucasa import Cloud, cloud_api from hass_nabucasa import Cloud, cloud_api
from .client import CloudClient from .client import CloudClient
@@ -18,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None: async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None:
"""Fetch the subscription info.""" """Fetch the subscription info."""
try: try:
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
return await cloud_api.async_subscription_info(cloud) return await cloud_api.async_subscription_info(cloud)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error( _LOGGER.error(
@@ -39,7 +38,7 @@ async def async_migrate_paypal_agreement(
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
"""Migrate a paypal agreement from legacy.""" """Migrate a paypal agreement from legacy."""
try: try:
async with async_timeout.timeout(REQUEST_TIMEOUT): async with asyncio.timeout(REQUEST_TIMEOUT):
return await cloud_api.async_migrate_paypal_agreement(cloud) return await cloud_api.async_migrate_paypal_agreement(cloud)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error( _LOGGER.error(

View File

@@ -4,7 +4,6 @@ import io
import logging import logging
import aiohttp import aiohttp
import async_timeout
from colorthief import ColorThief from colorthief import ColorThief
from PIL import UnidentifiedImageError from PIL import UnidentifiedImageError
import voluptuous as vol import voluptuous as vol
@@ -120,7 +119,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
try: try:
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
async with async_timeout.timeout(10): async with asyncio.timeout(10):
response = await session.get(url) response = await session.get(url)
except (asyncio.TimeoutError, aiohttp.ClientError) as err: except (asyncio.TimeoutError, aiohttp.ClientError) as err:

View File

@@ -7,7 +7,6 @@ import json
import logging import logging
import aiohttp import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@@ -112,7 +111,7 @@ class ComedHourlyPricingSensor(SensorEntity):
else: else:
url_string += "?type=currenthouraverage" url_string += "?type=currenthouraverage"
async with async_timeout.timeout(60): async with asyncio.timeout(60):
response = await self.websession.get(url_string) response = await self.websession.get(url_string)
# The API responds with MIME type 'text/html' # The API responds with MIME type 'text/html'
text = await response.text() text = await response.text()

View File

@@ -0,0 +1,34 @@
"""Comelit integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PIN, Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ComelitSerialBridge
PLATFORMS = [Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Comelit platform."""
coordinator = ComelitSerialBridge(hass, entry.data[CONF_HOST], entry.data[CONF_PIN])
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
coordinator: ComelitSerialBridge = hass.data[DOMAIN][entry.entry_id]
await coordinator.api.logout()
await coordinator.api.close()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -0,0 +1,145 @@
"""Config flow for Comelit integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions
import voluptuous as vol
from homeassistant import core, exceptions
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PIN
from homeassistant.data_entry_flow import FlowResult
from .const import _LOGGER, DOMAIN
DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = "111111"
def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema:
"""Return user form schema."""
user_input = user_input or {}
return vol.Schema(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): str,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): str,
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str})
async def validate_input(
hass: core.HomeAssistant, data: dict[str, Any]
) -> dict[str, str]:
"""Validate the user input allows us to connect."""
api = ComeliteSerialBridgeAPi(data[CONF_HOST], data[CONF_PIN])
try:
await api.login()
except aiocomelit_exceptions.CannotConnect as err:
raise CannotConnect from err
except aiocomelit_exceptions.CannotAuthenticate as err:
raise InvalidAuth from err
finally:
await api.logout()
await api.close()
return {"title": data[CONF_HOST]}
class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Comelit."""
VERSION = 1
_reauth_entry: ConfigEntry | None
_reauth_host: str
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=user_form_schema(user_input)
)
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
errors = {}
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=user_form_schema(user_input), errors=errors
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle reauth flow."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
self._reauth_host = entry_data[CONF_HOST]
self.context["title_placeholders"] = {"host": self._reauth_host}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reauth confirm."""
assert self._reauth_entry
errors = {}
if user_input is not None:
try:
await validate_input(
self.hass, {CONF_HOST: self._reauth_host} | user_input
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self.hass.config_entries.async_update_entry(
self._reauth_entry,
data={
CONF_HOST: self._reauth_host,
CONF_PIN: user_input[CONF_PIN],
},
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={CONF_HOST: self._reauth_entry.data[CONF_HOST]},
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,6 @@
"""Comelit constants."""
import logging
_LOGGER = logging.getLogger(__package__)
DOMAIN = "comelit"

View File

@@ -0,0 +1,50 @@
"""Support for Comelit."""
import asyncio
from datetime import timedelta
from typing import Any
from aiocomelit import ComeliteSerialBridgeAPi
import aiohttp
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, DOMAIN
class ComelitSerialBridge(DataUpdateCoordinator):
"""Queries Comelit Serial Bridge."""
def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None:
"""Initialize the scanner."""
self._host = host
self._pin = pin
self.api = ComeliteSerialBridgeAPi(host, pin)
super().__init__(
hass=hass,
logger=_LOGGER,
name=f"{DOMAIN}-{host}-coordinator",
update_interval=timedelta(seconds=5),
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update router data."""
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
try:
logged = await self.api.login()
except (asyncio.exceptions.TimeoutError, aiohttp.ClientConnectorError) as err:
_LOGGER.warning("Connection error for %s", self._host)
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
if not logged:
raise ConfigEntryAuthFailed
devices_data = await self.api.get_all_devices()
alarm_data = await self.api.get_alarm_config()
await self.api.logout()
return devices_data | alarm_data

View File

@@ -0,0 +1,78 @@
"""Support for lights."""
from __future__ import annotations
from typing import Any
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON
from homeassistant.components.light import LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ComelitSerialBridge
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Comelit lights."""
coordinator: ComelitSerialBridge = hass.data[DOMAIN][config_entry.entry_id]
# Use config_entry.entry_id as base for unique_id because no serial number or mac is available
async_add_entities(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
)
class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
"""Light device."""
_attr_has_entity_name = True
_attr_name = None
def __init__(
self,
coordinator: ComelitSerialBridge,
device: ComelitSerialBridgeObject,
config_entry_unique_id: str | None,
) -> None:
"""Init light entity."""
self._api = coordinator.api
self._device = device
super().__init__(coordinator)
self._attr_unique_id = f"{config_entry_unique_id}-{device.index}"
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, self._attr_unique_id),
},
manufacturer="Comelit",
model="Serial Bridge",
name=device.name,
)
async def _light_set_state(self, state: int) -> None:
"""Set desired light state."""
await self.coordinator.api.light_switch(self._device.index, state)
await self.coordinator.async_request_refresh()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
await self._light_set_state(LIGHT_ON)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
await self._light_set_state(LIGHT_OFF)
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
return self.coordinator.data[LIGHT][self._device.index].status == LIGHT_ON

View File

@@ -0,0 +1,10 @@
{
"domain": "comelit",
"name": "Comelit SimpleHome",
"codeowners": ["@chemelli74"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/comelit",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.0.5"]
}

View File

@@ -0,0 +1,31 @@
{
"config": {
"flow_title": "{host}",
"step": {
"reauth_confirm": {
"description": "Please enter the correct PIN for VEDO system: {host}",
"data": {
"pin": "[%key:common::config_flow::data::pin%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"pin": "[%key:common::config_flow::data::pin%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@@ -16,13 +16,12 @@ from homeassistant.components.sensor import (
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
STATE_CLASSES_SCHEMA, STATE_CLASSES_SCHEMA,
SensorDeviceClass, SensorDeviceClass,
SensorEntity,
SensorStateClass,
) )
from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_COMMAND, CONF_COMMAND,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ICON,
CONF_NAME, CONF_NAME,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
@@ -36,7 +35,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.template_entity import ManualTriggerEntity from homeassistant.helpers.template_entity import (
CONF_AVAILABILITY,
CONF_PICTURE,
ManualTriggerSensorEntity,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -47,6 +50,16 @@ CONF_JSON_ATTRIBUTES = "json_attributes"
DEFAULT_NAME = "Command Sensor" DEFAULT_NAME = "Command Sensor"
TRIGGER_ENTITY_OPTIONS = (
CONF_AVAILABILITY,
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_PICTURE,
CONF_UNIQUE_ID,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
SCAN_INTERVAL = timedelta(seconds=60) SCAN_INTERVAL = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -87,30 +100,25 @@ async def async_setup_platform(
name: str = sensor_config[CONF_NAME] name: str = sensor_config[CONF_NAME]
command: str = sensor_config[CONF_COMMAND] command: str = sensor_config[CONF_COMMAND]
unit: str | None = sensor_config.get(CONF_UNIT_OF_MEASUREMENT)
value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT]
unique_id: str | None = sensor_config.get(CONF_UNIQUE_ID)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
state_class: SensorStateClass | None = sensor_config.get(CONF_STATE_CLASS)
data = CommandSensorData(hass, command, command_timeout) data = CommandSensorData(hass, command, command_timeout)
trigger_entity_config = { trigger_entity_config = {CONF_NAME: Template(name, hass)}
CONF_UNIQUE_ID: unique_id, for key in TRIGGER_ENTITY_OPTIONS:
CONF_NAME: Template(name, hass), if key not in sensor_config:
CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), continue
} trigger_entity_config[key] = sensor_config[key]
async_add_entities( async_add_entities(
[ [
CommandSensor( CommandSensor(
data, data,
trigger_entity_config, trigger_entity_config,
unit,
state_class,
value_template, value_template,
json_attributes, json_attributes,
scan_interval, scan_interval,
@@ -119,7 +127,7 @@ async def async_setup_platform(
) )
class CommandSensor(ManualTriggerEntity, SensorEntity): class CommandSensor(ManualTriggerSensorEntity):
"""Representation of a sensor that is using shell commands.""" """Representation of a sensor that is using shell commands."""
_attr_should_poll = False _attr_should_poll = False
@@ -128,8 +136,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
self, self,
data: CommandSensorData, data: CommandSensorData,
config: ConfigType, config: ConfigType,
unit_of_measurement: str | None,
state_class: SensorStateClass | None,
value_template: Template | None, value_template: Template | None,
json_attributes: list[str] | None, json_attributes: list[str] | None,
scan_interval: timedelta, scan_interval: timedelta,
@@ -141,8 +147,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
self._json_attributes = json_attributes self._json_attributes = json_attributes
self._attr_native_value = None self._attr_native_value = None
self._value_template = value_template self._value_template = value_template
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_state_class = state_class
self._scan_interval = scan_interval self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None self._process_updates: asyncio.Lock | None = None

View File

@@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView, require_admin
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import DependencyError, Unauthorized from homeassistant.exceptions import DependencyError, Unauthorized
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@@ -138,12 +138,11 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""Not implemented.""" """Not implemented."""
raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"]) raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"])
# pylint: disable=arguments-differ @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def post(self, request): async def post(self, request):
"""Handle a POST request.""" """Handle a POST request."""
if not request["hass_user"].is_admin:
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
try: try:
return await super().post(request) return await super().post(request)
@@ -164,19 +163,18 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
url = "/api/config/config_entries/flow/{flow_id}" url = "/api/config/config_entries/flow/{flow_id}"
name = "api:config:config_entries:flow:resource" name = "api:config:config_entries:flow:resource"
async def get(self, request, flow_id): @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def get(self, request, /, flow_id):
"""Get the current state of a data_entry_flow.""" """Get the current state of a data_entry_flow."""
if not request["hass_user"].is_admin:
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
return await super().get(request, flow_id) return await super().get(request, flow_id)
# pylint: disable=arguments-differ @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def post(self, request, flow_id): async def post(self, request, flow_id):
"""Handle a POST request.""" """Handle a POST request."""
if not request["hass_user"].is_admin:
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
return await super().post(request, flow_id) return await super().post(request, flow_id)
@@ -206,15 +204,14 @@ class OptionManagerFlowIndexView(FlowManagerIndexView):
url = "/api/config/config_entries/options/flow" url = "/api/config/config_entries/options/flow"
name = "api:config:config_entries:option:flow" name = "api:config:config_entries:option:flow"
# pylint: disable=arguments-differ @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request): async def post(self, request):
"""Handle a POST request. """Handle a POST request.
handler in request is entry_id. handler in request is entry_id.
""" """
if not request["hass_user"].is_admin:
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
return await super().post(request) return await super().post(request)
@@ -225,19 +222,18 @@ class OptionManagerFlowResourceView(FlowManagerResourceView):
url = "/api/config/config_entries/options/flow/{flow_id}" url = "/api/config/config_entries/options/flow/{flow_id}"
name = "api:config:config_entries:options:flow:resource" name = "api:config:config_entries:options:flow:resource"
async def get(self, request, flow_id): @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def get(self, request, /, flow_id):
"""Get the current state of a data_entry_flow.""" """Get the current state of a data_entry_flow."""
if not request["hass_user"].is_admin:
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
return await super().get(request, flow_id) return await super().get(request, flow_id)
# pylint: disable=arguments-differ @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request, flow_id): async def post(self, request, flow_id):
"""Handle a POST request.""" """Handle a POST request."""
if not request["hass_user"].is_admin:
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
return await super().post(request, flow_id) return await super().post(request, flow_id)

View File

@@ -4,7 +4,6 @@ from datetime import timedelta
import logging import logging
from aiohttp import ClientConnectionError from aiohttp import ClientConnectionError
from async_timeout import timeout
from pydaikin.daikin_base import Appliance from pydaikin.daikin_base import Appliance
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -74,7 +73,7 @@ async def daikin_api_setup(hass: HomeAssistant, host, key, uuid, password):
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
try: try:
async with timeout(TIMEOUT): async with asyncio.timeout(TIMEOUT):
device = await Appliance.factory( device = await Appliance.factory(
host, session, key=key, uuid=uuid, password=password host, session, key=key, uuid=uuid, password=password
) )

View File

@@ -4,7 +4,6 @@ import logging
from uuid import uuid4 from uuid import uuid4
from aiohttp import ClientError, web_exceptions from aiohttp import ClientError, web_exceptions
from async_timeout import timeout
from pydaikin.daikin_base import Appliance, DaikinException from pydaikin.daikin_base import Appliance, DaikinException
from pydaikin.discovery import Discovery from pydaikin.discovery import Discovery
import voluptuous as vol import voluptuous as vol
@@ -70,7 +69,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
password = None password = None
try: try:
async with timeout(TIMEOUT): async with asyncio.timeout(TIMEOUT):
device = await Appliance.factory( device = await Appliance.factory(
host, host,
async_get_clientsession(self.hass), async_get_clientsession(self.hass),

View File

@@ -9,7 +9,6 @@ from pprint import pformat
from typing import Any, cast from typing import Any, cast
from urllib.parse import urlparse from urllib.parse import urlparse
import async_timeout
from pydeconz.errors import LinkButtonNotPressed, RequestError, ResponseError from pydeconz.errors import LinkButtonNotPressed, RequestError, ResponseError
from pydeconz.gateway import DeconzSession from pydeconz.gateway import DeconzSession
from pydeconz.utils import ( from pydeconz.utils import (
@@ -101,7 +100,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
self.bridges = await deconz_discovery(session) self.bridges = await deconz_discovery(session)
except (asyncio.TimeoutError, ResponseError): except (asyncio.TimeoutError, ResponseError):
@@ -159,7 +158,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
deconz_session = DeconzSession(session, self.host, self.port) deconz_session = DeconzSession(session, self.host, self.port)
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
api_key = await deconz_session.get_api_key() api_key = await deconz_session.get_api_key()
except LinkButtonNotPressed: except LinkButtonNotPressed:
@@ -180,7 +179,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
self.bridge_id = await deconz_get_bridge_id( self.bridge_id = await deconz_get_bridge_id(
session, self.host, self.port, self.api_key session, self.host, self.port, self.api_key
) )

View File

@@ -7,7 +7,6 @@ from collections.abc import Callable
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
import async_timeout
from pydeconz import DeconzSession, errors from pydeconz import DeconzSession, errors
from pydeconz.interfaces import sensors from pydeconz.interfaces import sensors
from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler
@@ -353,7 +352,7 @@ async def get_deconz_session(
config[CONF_API_KEY], config[CONF_API_KEY],
) )
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
await deconz_session.refresh_state() await deconz_session.refresh_state()
return deconz_session return deconz_session

View File

@@ -46,6 +46,11 @@ CONDITION_CLASSES: dict[str, list[str]] = {
ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_WINDY_VARIANT: [],
ATTR_CONDITION_EXCEPTIONAL: [], ATTR_CONDITION_EXCEPTIONAL: [],
} }
CONDITION_MAP = {
cond_code: cond_ha
for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes
}
WEATHER_UPDATE_INTERVAL = timedelta(minutes=30) WEATHER_UPDATE_INTERVAL = timedelta(minutes=30)
@@ -237,9 +242,7 @@ class DemoWeather(WeatherEntity):
@property @property
def condition(self) -> str: def condition(self) -> str:
"""Return the weather condition.""" """Return the weather condition."""
return [ return CONDITION_MAP[self._condition.lower()]
k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v
][0]
async def async_forecast_daily(self) -> list[Forecast]: async def async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast.""" """Return the daily forecast."""

View File

@@ -1,10 +1,10 @@
"""The devolo Home Network integration.""" """The devolo Home Network integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from typing import Any from typing import Any
import async_timeout
from devolo_plc_api import Device from devolo_plc_api import Device
from devolo_plc_api.device_api import ( from devolo_plc_api.device_api import (
ConnectedStationInfo, ConnectedStationInfo,
@@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
assert device.plcnet assert device.plcnet
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
return await device.plcnet.async_get_network_overview() return await device.plcnet.async_get_network_overview()
except DeviceUnavailable as err: except DeviceUnavailable as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
@@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
assert device.device assert device.device
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
return await device.device.async_get_wifi_guest_access() return await device.device.async_get_wifi_guest_access()
except DeviceUnavailable as err: except DeviceUnavailable as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
@@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
assert device.device assert device.device
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
return await device.device.async_get_led_setting() return await device.device.async_get_led_setting()
except DeviceUnavailable as err: except DeviceUnavailable as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
@@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
assert device.device assert device.device
try: try:
async with async_timeout.timeout(10): async with asyncio.timeout(10):
return await device.device.async_get_wifi_connected_station() return await device.device.async_get_wifi_connected_station()
except DeviceUnavailable as err: except DeviceUnavailable as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
@@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
assert device.device assert device.device
try: try:
async with async_timeout.timeout(30): async with asyncio.timeout(30):
return await device.device.async_get_wifi_neighbor_access_points() return await device.device.async_get_wifi_neighbor_access_points()
except DeviceUnavailable as err: except DeviceUnavailable as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err

View File

@@ -6,7 +6,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, GLUCOSE_VALUE_ICON, MG_DL from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, GLUCOSE_VALUE_ICON, MG_DL
@@ -29,18 +32,33 @@ async def async_setup_entry(
) )
class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity): class DexcomSensorEntity(CoordinatorEntity, SensorEntity):
"""Base Dexcom sensor entity."""
def __init__(
self, coordinator: DataUpdateCoordinator, username: str, key: str
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{username}-{key}"
class DexcomGlucoseValueSensor(DexcomSensorEntity):
"""Representation of a Dexcom glucose value sensor.""" """Representation of a Dexcom glucose value sensor."""
_attr_icon = GLUCOSE_VALUE_ICON _attr_icon = GLUCOSE_VALUE_ICON
def __init__(self, coordinator, username, unit_of_measurement): def __init__(
self,
coordinator: DataUpdateCoordinator,
username: str,
unit_of_measurement: str,
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator, username, "value")
self._attr_native_unit_of_measurement = unit_of_measurement self._attr_native_unit_of_measurement = unit_of_measurement
self._key = "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" self._key = "mg_dl" if unit_of_measurement == MG_DL else "mmol_l"
self._attr_name = f"{DOMAIN}_{username}_glucose_value" self._attr_name = f"{DOMAIN}_{username}_glucose_value"
self._attr_unique_id = f"{username}-value"
@property @property
def native_value(self): def native_value(self):
@@ -50,14 +68,13 @@ class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity):
return None return None
class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity): class DexcomGlucoseTrendSensor(DexcomSensorEntity):
"""Representation of a Dexcom glucose trend sensor.""" """Representation of a Dexcom glucose trend sensor."""
def __init__(self, coordinator, username): def __init__(self, coordinator: DataUpdateCoordinator, username: str) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator, username, "trend")
self._attr_name = f"{DOMAIN}_{username}_glucose_trend" self._attr_name = f"{DOMAIN}_{username}_glucose_trend"
self._attr_unique_id = f"{username}-trend"
@property @property
def icon(self): def icon(self):

View File

@@ -1,7 +1,9 @@
"""Discovergy sensor entity.""" """Discovergy sensor entity."""
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime
from pydiscovergy.models import Meter from pydiscovergy.models import Meter, Reading
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@@ -11,6 +13,7 @@ from homeassistant.components.sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
EntityCategory,
UnitOfElectricPotential, UnitOfElectricPotential,
UnitOfEnergy, UnitOfEnergy,
UnitOfPower, UnitOfPower,
@@ -19,7 +22,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DiscovergyData, DiscovergyUpdateCoordinator from . import DiscovergyData, DiscovergyUpdateCoordinator
@@ -32,6 +34,9 @@ PARALLEL_UPDATES = 1
class DiscovergyMixin: class DiscovergyMixin:
"""Mixin for alternative keys.""" """Mixin for alternative keys."""
value_fn: Callable[[Reading, str, int], datetime | float | None] = field(
default=lambda reading, key, scale: float(reading.values[key] / scale)
)
alternative_keys: list[str] = field(default_factory=lambda: []) alternative_keys: list[str] = field(default_factory=lambda: [])
scale: int = field(default_factory=lambda: 1000) scale: int = field(default_factory=lambda: 1000)
@@ -144,6 +149,17 @@ ELECTRICITY_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = (
), ),
) )
ADDITIONAL_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = (
DiscovergySensorEntityDescription(
key="last_transmitted",
translation_key="last_transmitted",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda reading, key, scale: reading.time_with_timezone,
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@@ -160,18 +176,22 @@ async def async_setup_entry(
elif meter.measurement_type == "GAS": elif meter.measurement_type == "GAS":
sensors = GAS_SENSORS sensors = GAS_SENSORS
coordinator: DiscovergyUpdateCoordinator = data.coordinators[meter.meter_id]
if sensors is not None: if sensors is not None:
for description in sensors: for description in sensors:
# check if this meter has this data, then add this sensor # check if this meter has this data, then add this sensor
for key in {description.key, *description.alternative_keys}: for key in {description.key, *description.alternative_keys}:
coordinator: DiscovergyUpdateCoordinator = data.coordinators[
meter.meter_id
]
if key in coordinator.data.values: if key in coordinator.data.values:
entities.append( entities.append(
DiscovergySensor(key, description, meter, coordinator) DiscovergySensor(key, description, meter, coordinator)
) )
for description in ADDITIONAL_SENSORS:
entities.append(
DiscovergySensor(description.key, description, meter, coordinator)
)
async_add_entities(entities, False) async_add_entities(entities, False)
@@ -204,8 +224,8 @@ class DiscovergySensor(CoordinatorEntity[DiscovergyUpdateCoordinator], SensorEnt
) )
@property @property
def native_value(self) -> StateType: def native_value(self) -> datetime | float | None:
"""Return the sensor state.""" """Return the sensor state."""
return float( return self.entity_description.value_fn(
self.coordinator.data.values[self.data_key] / self.entity_description.scale self.coordinator.data, self.data_key, self.entity_description.scale
) )

View File

@@ -60,6 +60,9 @@
}, },
"phase_3_power": { "phase_3_power": {
"name": "Phase 3 power" "name": "Phase 3 power"
},
"last_transmitted": {
"name": "Last transmitted"
} }
} }
} }

View File

@@ -14,35 +14,24 @@ from homeassistant.components import persistent_notification
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_TOKEN, CONF_TOKEN,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import get_url
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util, slugify
from .const import ( from .const import API_URL, CONF_EVENTS, DOMAIN, PLATFORMS
CONF_EVENTS, from .device import ConfiguredDoorBird
DOMAIN, from .models import DoorBirdData
DOOR_STATION, from .util import get_door_station_by_token
DOOR_STATION_EVENT_ENTITY_IDS,
DOOR_STATION_INFO,
PLATFORMS,
UNDO_UPDATE_LISTENER,
)
from .util import get_doorstation_by_token
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
API_URL = f"/api/{DOMAIN}"
CONF_CUSTOM_URL = "hass_url_override" CONF_CUSTOM_URL = "hass_url_override"
RESET_DEVICE_FAVORITES = "doorbird_reset_favorites" RESET_DEVICE_FAVORITES = "doorbird_reset_favorites"
@@ -66,26 +55,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the DoorBird component.""" """Set up the DoorBird component."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
# Provide an endpoint for the doorstations to call to trigger events # Provide an endpoint for the door stations to call to trigger events
hass.http.register_view(DoorBirdRequestView) hass.http.register_view(DoorBirdRequestView)
def _reset_device_favorites_handler(event): def _reset_device_favorites_handler(event: Event) -> None:
"""Handle clearing favorites on device.""" """Handle clearing favorites on device."""
if (token := event.data.get("token")) is None: if (token := event.data.get("token")) is None:
return return
doorstation = get_doorstation_by_token(hass, token) door_station = get_door_station_by_token(hass, token)
if doorstation is None: if door_station is None:
_LOGGER.error("Device not found for provided token") _LOGGER.error("Device not found for provided token")
return return
# Clear webhooks # Clear webhooks
favorites = doorstation.device.favorites() favorites: dict[str, list[str]] = door_station.device.favorites()
for favorite_type, favorite_ids in favorites.items():
for favorite_type in favorites: for favorite_id in favorite_ids:
for favorite_id in favorites[favorite_type]: door_station.device.delete_favorite(favorite_type, favorite_id)
doorstation.device.delete_favorite(favorite_type, favorite_id)
hass.bus.async_listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) hass.bus.async_listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler)
@@ -97,17 +85,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_async_import_options_from_data_if_missing(hass, entry) _async_import_options_from_data_if_missing(hass, entry)
doorstation_config = entry.data door_station_config = entry.data
doorstation_options = entry.options
config_entry_id = entry.entry_id config_entry_id = entry.entry_id
device_ip = doorstation_config[CONF_HOST] device_ip = door_station_config[CONF_HOST]
username = doorstation_config[CONF_USERNAME] username = door_station_config[CONF_USERNAME]
password = doorstation_config[CONF_PASSWORD] password = door_station_config[CONF_PASSWORD]
device = DoorBird(device_ip, username, password) device = DoorBird(device_ip, username, password)
try: try:
status, info = await hass.async_add_executor_job(_init_doorbird_device, device) status, info = await hass.async_add_executor_job(_init_door_bird_device, device)
except requests.exceptions.HTTPError as err: except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTPStatus.UNAUTHORIZED: if err.response.status_code == HTTPStatus.UNAUTHORIZED:
_LOGGER.error( _LOGGER.error(
@@ -128,50 +115,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
raise ConfigEntryNotReady raise ConfigEntryNotReady
token = doorstation_config.get(CONF_TOKEN, config_entry_id) token: str = door_station_config.get(CONF_TOKEN, config_entry_id)
custom_url = doorstation_config.get(CONF_CUSTOM_URL) custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL)
name = doorstation_config.get(CONF_NAME) name: str | None = door_station_config.get(CONF_NAME)
events = doorstation_options.get(CONF_EVENTS, []) events = entry.options.get(CONF_EVENTS, [])
doorstation = ConfiguredDoorBird(device, name, custom_url, token) event_entity_ids: dict[str, str] = {}
doorstation.update_events(events) door_station = ConfiguredDoorBird(device, name, custom_url, token, event_entity_ids)
door_bird_data = DoorBirdData(door_station, info, event_entity_ids)
door_station.update_events(events)
# Subscribe to doorbell or motion events # Subscribe to doorbell or motion events
if not await _async_register_events(hass, doorstation): if not await _async_register_events(hass, door_station):
raise ConfigEntryNotReady raise ConfigEntryNotReady
undo_listener = entry.add_update_listener(_update_listener) entry.async_on_unload(entry.add_update_listener(_update_listener))
hass.data[DOMAIN][config_entry_id] = door_bird_data
hass.data[DOMAIN][config_entry_id] = {
DOOR_STATION: doorstation,
DOOR_STATION_INFO: info,
UNDO_UPDATE_LISTENER: undo_listener,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
def _init_doorbird_device(device): def _init_door_bird_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]:
"""Verify we can connect to the device and return the status."""
return device.ready(), device.info() return device.ready(), device.info()
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
data: dict[str, DoorBirdData] = hass.data[DOMAIN]
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
data.pop(entry.entry_id)
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 unload_ok
async def _async_register_events( async def _async_register_events(
hass: HomeAssistant, doorstation: ConfiguredDoorBird hass: HomeAssistant, door_station: ConfiguredDoorBird
) -> bool: ) -> bool:
try: try:
await hass.async_add_executor_job(doorstation.register_events, hass) await hass.async_add_executor_job(door_station.register_events, hass)
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
persistent_notification.async_create( persistent_notification.async_create(
hass, hass,
@@ -192,10 +172,11 @@ async def _async_register_events(
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update.""" """Handle options update."""
config_entry_id = entry.entry_id config_entry_id = entry.entry_id
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] data: DoorBirdData = hass.data[DOMAIN][config_entry_id]
doorstation.update_events(entry.options[CONF_EVENTS]) door_station = data.door_station
door_station.update_events(entry.options[CONF_EVENTS])
# Subscribe to doorbell or motion events # Subscribe to doorbell or motion events
await _async_register_events(hass, doorstation) await _async_register_events(hass, door_station)
@callback @callback
@@ -211,122 +192,6 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi
hass.config_entries.async_update_entry(entry, options=options) hass.config_entries.async_update_entry(entry, options=options)
class ConfiguredDoorBird:
"""Attach additional information to pass along with configured device."""
def __init__(self, device, name, custom_url, token):
"""Initialize configured device."""
self._name = name
self._device = device
self._custom_url = custom_url
self.events = None
self.doorstation_events = None
self._token = token
def update_events(self, events):
"""Update the doorbird events."""
self.events = events
self.doorstation_events = [self._get_event_name(event) for event in self.events]
@property
def name(self):
"""Get custom device name."""
return self._name
@property
def device(self):
"""Get the configured device."""
return self._device
@property
def custom_url(self):
"""Get custom url for device."""
return self._custom_url
@property
def token(self):
"""Get token for device."""
return self._token
def register_events(self, hass: HomeAssistant) -> None:
"""Register events on device."""
# Get the URL of this server
hass_url = get_url(hass, prefer_external=False)
# Override url if another is specified in the configuration
if self.custom_url is not None:
hass_url = self.custom_url
if not self.doorstation_events:
# User may not have permission to get the favorites
return
favorites = self.device.favorites()
for event in self.doorstation_events:
if self._register_event(hass_url, event, favs=favorites):
_LOGGER.info(
"Successfully registered URL for %s on %s", event, self.name
)
@property
def slug(self):
"""Get device slug."""
return slugify(self._name)
def _get_event_name(self, event):
return f"{self.slug}_{event}"
def _register_event(
self, hass_url: str, event: str, favs: dict[str, Any] | None = None
) -> bool:
"""Add a schedule entry in the device for a sensor."""
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
# Register HA URL as webhook if not already, then get the ID
if self.webhook_is_registered(url, favs=favs):
return True
self.device.change_favorite("http", f"Home Assistant ({event})", url)
if not self.webhook_is_registered(url):
_LOGGER.warning(
'Unable to set favorite URL "%s". Event "%s" will not fire',
url,
event,
)
return False
return True
def webhook_is_registered(self, url, favs=None) -> bool:
"""Return whether the given URL is registered as a device favorite."""
return self.get_webhook_id(url, favs) is not None
def get_webhook_id(self, url, favs=None) -> str | None:
"""Return the device favorite ID for the given URL.
The favorite must exist or there will be problems.
"""
favs = favs if favs else self.device.favorites()
if "http" not in favs:
return None
for fav_id in favs["http"]:
if favs["http"][fav_id]["value"] == url:
return fav_id
return None
def get_event_data(self):
"""Get data to pass along with HA event."""
return {
"timestamp": dt_util.utcnow().isoformat(),
"live_video_url": self._device.live_video_url,
"live_image_url": self._device.live_image_url,
"rtsp_live_video_url": self._device.rtsp_live_video_url,
"html5_viewer_url": self._device.html5_viewer_url,
}
class DoorBirdRequestView(HomeAssistantView): class DoorBirdRequestView(HomeAssistantView):
"""Provide a page for the device to call.""" """Provide a page for the device to call."""
@@ -335,21 +200,17 @@ class DoorBirdRequestView(HomeAssistantView):
name = API_URL[1:].replace("/", ":") name = API_URL[1:].replace("/", ":")
extra_urls = [API_URL + "/{event}"] extra_urls = [API_URL + "/{event}"]
async def get(self, request, event): async def get(self, request: web.Request, event: str) -> web.Response:
"""Respond to requests from the device.""" """Respond to requests from the device."""
hass = request.app["hass"] hass: HomeAssistant = request.app["hass"]
token: str | None = request.query.get("token")
token = request.query.get("token") if token is None or (device := get_door_station_by_token(hass, token)) is None:
device = get_doorstation_by_token(hass, token)
if device is None:
return web.Response( return web.Response(
status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided."
) )
if device: if device:
event_data = device.get_event_data() event_data = device.get_event_data(event)
else: else:
event_data = {} event_data = {}
@@ -359,10 +220,6 @@ class DoorBirdRequestView(HomeAssistantView):
message = f"HTTP Favorites cleared for {device.slug}" message = f"HTTP Favorites cleared for {device.slug}"
return web.Response(text=message) return web.Response(text=message)
event_data[ATTR_ENTITY_ID] = hass.data[DOMAIN][
DOOR_STATION_EVENT_ENTITY_IDS
].get(event)
hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) hass.bus.async_fire(f"{DOMAIN}_{event}", event_data)
return web.Response(text="OK") return web.Response(text="OK")

View File

@@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO from .const import DOMAIN
from .entity import DoorBirdEntity from .entity import DoorBirdEntity
from .models import DoorBirdData
IR_RELAY = "__ir_light__" IR_RELAY = "__ir_light__"
@@ -49,20 +50,14 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the DoorBird button platform.""" """Set up the DoorBird button platform."""
config_entry_id = config_entry.entry_id config_entry_id = config_entry.entry_id
door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id]
data = hass.data[DOMAIN][config_entry_id] relays = door_bird_data.door_station_info["RELAYS"]
doorstation = data[DOOR_STATION]
doorstation_info = data[DOOR_STATION_INFO]
relays = doorstation_info["RELAYS"]
entities = [ entities = [
DoorBirdButton(doorstation, doorstation_info, relay, RELAY_ENTITY_DESCRIPTION) DoorBirdButton(door_bird_data, relay, RELAY_ENTITY_DESCRIPTION)
for relay in relays for relay in relays
] ]
entities.append( entities.append(DoorBirdButton(door_bird_data, IR_RELAY, IR_ENTITY_DESCRIPTION))
DoorBirdButton(doorstation, doorstation_info, IR_RELAY, IR_ENTITY_DESCRIPTION)
)
async_add_entities(entities) async_add_entities(entities)
@@ -74,16 +69,14 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity):
def __init__( def __init__(
self, self,
doorstation: DoorBird, door_bird_data: DoorBirdData,
doorstation_info,
relay: str, relay: str,
entity_description: DoorbirdButtonEntityDescription, entity_description: DoorbirdButtonEntityDescription,
) -> None: ) -> None:
"""Initialize a relay in a DoorBird device.""" """Initialize a relay in a DoorBird device."""
super().__init__(doorstation, doorstation_info) super().__init__(door_bird_data)
self._relay = relay self._relay = relay
self.entity_description = entity_description self.entity_description = entity_description
if self._relay == IR_RELAY: if self._relay == IR_RELAY:
self._attr_name = "IR" self._attr_name = "IR"
else: else:
@@ -92,4 +85,4 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity):
def press(self) -> None: def press(self) -> None:
"""Power the relay.""" """Power the relay."""
self.entity_description.press_action(self._doorstation.device, self._relay) self.entity_description.press_action(self._door_station.device, self._relay)

View File

@@ -6,7 +6,6 @@ import datetime
import logging import logging
import aiohttp import aiohttp
import async_timeout
from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -15,13 +14,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import ( from .const import DOMAIN
DOMAIN,
DOOR_STATION,
DOOR_STATION_EVENT_ENTITY_IDS,
DOOR_STATION_INFO,
)
from .entity import DoorBirdEntity from .entity import DoorBirdEntity
from .models import DoorBirdData
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2) _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2)
_LAST_MOTION_INTERVAL = datetime.timedelta(seconds=30) _LAST_MOTION_INTERVAL = datetime.timedelta(seconds=30)
@@ -37,39 +32,31 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the DoorBird camera platform.""" """Set up the DoorBird camera platform."""
config_entry_id = config_entry.entry_id config_entry_id = config_entry.entry_id
config_data = hass.data[DOMAIN][config_entry_id] door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id]
doorstation = config_data[DOOR_STATION] device = door_bird_data.door_station.device
doorstation_info = config_data[DOOR_STATION_INFO]
device = doorstation.device
async_add_entities( async_add_entities(
[ [
DoorBirdCamera( DoorBirdCamera(
doorstation, door_bird_data,
doorstation_info,
device.live_image_url, device.live_image_url,
"live", "live",
"live", "live",
doorstation.doorstation_events,
_LIVE_INTERVAL, _LIVE_INTERVAL,
device.rtsp_live_video_url, device.rtsp_live_video_url,
), ),
DoorBirdCamera( DoorBirdCamera(
doorstation, door_bird_data,
doorstation_info,
device.history_image_url(1, "doorbell"), device.history_image_url(1, "doorbell"),
"last_ring", "last_ring",
"last_ring", "last_ring",
[],
_LAST_VISITOR_INTERVAL, _LAST_VISITOR_INTERVAL,
), ),
DoorBirdCamera( DoorBirdCamera(
doorstation, door_bird_data,
doorstation_info,
device.history_image_url(1, "motionsensor"), device.history_image_url(1, "motionsensor"),
"last_motion", "last_motion",
"last_motion", "last_motion",
[],
_LAST_MOTION_INTERVAL, _LAST_MOTION_INTERVAL,
), ),
] ]
@@ -81,17 +68,15 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
def __init__( def __init__(
self, self,
doorstation, door_bird_data: DoorBirdData,
doorstation_info, url: str,
url, camera_id: str,
camera_id, translation_key: str,
translation_key, interval: datetime.timedelta,
doorstation_events, stream_url: str | None = None,
interval,
stream_url=None,
) -> None: ) -> None:
"""Initialize the camera on a DoorBird device.""" """Initialize the camera on a DoorBird device."""
super().__init__(doorstation, doorstation_info) super().__init__(door_bird_data)
self._url = url self._url = url
self._stream_url = stream_url self._stream_url = stream_url
self._attr_translation_key = translation_key self._attr_translation_key = translation_key
@@ -101,7 +86,6 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
self._interval = interval self._interval = interval
self._last_update = datetime.datetime.min self._last_update = datetime.datetime.min
self._attr_unique_id = f"{self._mac_addr}_{camera_id}" self._attr_unique_id = f"{self._mac_addr}_{camera_id}"
self._doorstation_events = doorstation_events
async def stream_source(self): async def stream_source(self):
"""Return the stream source.""" """Return the stream source."""
@@ -118,7 +102,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
try: try:
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
async with async_timeout.timeout(_TIMEOUT): async with asyncio.timeout(_TIMEOUT):
response = await websession.get(self._url) response = await websession.get(self._url)
self._last_image = await response.read() self._last_image = await response.read()
@@ -134,19 +118,15 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
return self._last_image return self._last_image
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Add callback after being added to hass. """Subscribe to events."""
await super().async_added_to_hass()
Registers entity_id map for the logbook event_to_entity_id = self._door_bird_data.event_entity_ids
""" for event in self._door_station.events:
event_to_entity_id = self.hass.data[DOMAIN].setdefault(
DOOR_STATION_EVENT_ENTITY_IDS, {}
)
for event in self._doorstation_events:
event_to_entity_id[event] = self.entity_id event_to_entity_id[event] = self.entity_id
async def will_remove_from_hass(self): async def async_will_remove_from_hass(self) -> None:
"""Unregister entity_id map for the logbook.""" """Unsubscribe from events."""
event_to_entity_id = self.hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS] event_to_entity_id = self._door_bird_data.event_entity_ids
for event in self._doorstation_events: for event in self._door_station.events:
if event in event_to_entity_id: del event_to_entity_id[event]
del event_to_entity_id[event] await super().async_will_remove_from_hass()

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from http import HTTPStatus from http import HTTPStatus
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
from typing import Any
from doorbirdpy import DoorBird from doorbirdpy import DoorBird
import requests import requests
@@ -12,12 +13,12 @@ import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.util.network import is_ipv4_address, is_link_local from homeassistant.util.network import is_ipv4_address, is_link_local
from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI
from .util import get_mac_address_from_doorstation_info from .util import get_mac_address_from_door_station_info
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -33,7 +34,7 @@ def _schema_with_defaults(host=None, name=None):
) )
def _check_device(device): def _check_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]:
"""Verify we can connect to the device and return the status.""" """Verify we can connect to the device and return the status."""
return device.ready(), device.info() return device.ready(), device.info()
@@ -53,13 +54,13 @@ async def validate_input(hass: core.HomeAssistant, data):
if not status[0]: if not status[0]:
raise CannotConnect raise CannotConnect
mac_addr = get_mac_address_from_doorstation_info(info) mac_addr = get_mac_address_from_door_station_info(info)
# Return info that you want to store in the config entry. # Return info that you want to store in the config entry.
return {"title": data[CONF_HOST], "mac_addr": mac_addr} return {"title": data[CONF_HOST], "mac_addr": mac_addr}
async def async_verify_supported_device(hass, host): async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool:
"""Verify the doorbell state endpoint returns a 401.""" """Verify the doorbell state endpoint returns a 401."""
device = DoorBird(host, "", "") device = DoorBird(host, "", "")
try: try:

View File

@@ -19,3 +19,5 @@ DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR"
DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR" DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR"
UNDO_UPDATE_LISTENER = "undo_update_listener" UNDO_UPDATE_LISTENER = "undo_update_listener"
API_URL = f"/api/{DOMAIN}"

View File

@@ -0,0 +1,147 @@
"""Support for DoorBird devices."""
from __future__ import annotations
import logging
from typing import Any
from doorbirdpy import DoorBird
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import get_url
from homeassistant.util import dt as dt_util, slugify
from .const import API_URL
_LOGGER = logging.getLogger(__name__)
class ConfiguredDoorBird:
"""Attach additional information to pass along with configured device."""
def __init__(
self,
device: DoorBird,
name: str | None,
custom_url: str | None,
token: str,
event_entity_ids: dict[str, str],
) -> None:
"""Initialize configured device."""
self._name = name
self._device = device
self._custom_url = custom_url
self._token = token
self._event_entity_ids = event_entity_ids
self.events: list[str] = []
self.door_station_events: list[str] = []
def update_events(self, events: list[str]) -> None:
"""Update the doorbird events."""
self.events = events
self.door_station_events = [
self._get_event_name(event) for event in self.events
]
@property
def name(self) -> str | None:
"""Get custom device name."""
return self._name
@property
def device(self) -> DoorBird:
"""Get the configured device."""
return self._device
@property
def custom_url(self) -> str | None:
"""Get custom url for device."""
return self._custom_url
@property
def token(self) -> str:
"""Get token for device."""
return self._token
def register_events(self, hass: HomeAssistant) -> None:
"""Register events on device."""
# Get the URL of this server
hass_url = get_url(hass, prefer_external=False)
# Override url if another is specified in the configuration
if self.custom_url is not None:
hass_url = self.custom_url
if not self.door_station_events:
# User may not have permission to get the favorites
return
favorites = self.device.favorites()
for event in self.door_station_events:
if self._register_event(hass_url, event, favs=favorites):
_LOGGER.info(
"Successfully registered URL for %s on %s", event, self.name
)
@property
def slug(self) -> str:
"""Get device slug."""
return slugify(self._name)
def _get_event_name(self, event: str) -> str:
return f"{self.slug}_{event}"
def _register_event(
self, hass_url: str, event: str, favs: dict[str, Any] | None = None
) -> bool:
"""Add a schedule entry in the device for a sensor."""
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
# Register HA URL as webhook if not already, then get the ID
if self.webhook_is_registered(url, favs=favs):
return True
self.device.change_favorite("http", f"Home Assistant ({event})", url)
if not self.webhook_is_registered(url):
_LOGGER.warning(
'Unable to set favorite URL "%s". Event "%s" will not fire',
url,
event,
)
return False
return True
def webhook_is_registered(
self, url: str, favs: dict[str, Any] | None = None
) -> bool:
"""Return whether the given URL is registered as a device favorite."""
return self.get_webhook_id(url, favs) is not None
def get_webhook_id(
self, url: str, favs: dict[str, Any] | None = None
) -> str | None:
"""Return the device favorite ID for the given URL.
The favorite must exist or there will be problems.
"""
favs = favs if favs else self.device.favorites()
if "http" not in favs:
return None
for fav_id in favs["http"]:
if favs["http"][fav_id]["value"] == url:
return fav_id
return None
def get_event_data(self, event: str) -> dict[str, str | None]:
"""Get data to pass along with HA event."""
return {
"timestamp": dt_util.utcnow().isoformat(),
"live_video_url": self._device.live_video_url,
"live_image_url": self._device.live_image_url,
"rtsp_live_video_url": self._device.rtsp_live_video_url,
"html5_viewer_url": self._device.html5_viewer_url,
ATTR_ENTITY_ID: self._event_entity_ids.get(event),
}

View File

@@ -1,5 +1,6 @@
"""The DoorBird integration base entity.""" """The DoorBird integration base entity."""
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@@ -10,7 +11,8 @@ from .const import (
DOORBIRD_INFO_KEY_FIRMWARE, DOORBIRD_INFO_KEY_FIRMWARE,
MANUFACTURER, MANUFACTURER,
) )
from .util import get_mac_address_from_doorstation_info from .models import DoorBirdData
from .util import get_mac_address_from_door_station_info
class DoorBirdEntity(Entity): class DoorBirdEntity(Entity):
@@ -18,19 +20,20 @@ class DoorBirdEntity(Entity):
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__(self, doorstation, doorstation_info): def __init__(self, door_bird_data: DoorBirdData) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__() super().__init__()
self._doorstation = doorstation self._door_bird_data = door_bird_data
self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info) self._door_station = door_bird_data.door_station
door_station_info = door_bird_data.door_station_info
firmware = doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] self._mac_addr = get_mac_address_from_door_station_info(door_station_info)
firmware_build = doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] firmware = door_station_info[DOORBIRD_INFO_KEY_FIRMWARE]
firmware_build = door_station_info[DOORBIRD_INFO_KEY_BUILD_NUMBER]
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
configuration_url="https://webadmin.doorbird.com/", configuration_url="https://webadmin.doorbird.com/",
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], model=door_station_info[DOORBIRD_INFO_KEY_DEVICE_TYPE],
name=self._doorstation.name, name=self._door_station.name,
sw_version=f"{firmware} {firmware_build}", sw_version=f"{firmware} {firmware_build}",
) )

View File

@@ -1,43 +1,35 @@
"""Describe logbook events.""" """Describe logbook events."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from homeassistant.components.logbook import ( from homeassistant.components.logbook import (
LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ENTITY_ID,
LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_MESSAGE,
LOGBOOK_ENTRY_NAME, LOGBOOK_ENTRY_NAME,
) )
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import callback from homeassistant.core import Event, HomeAssistant, callback
from .const import DOMAIN, DOOR_STATION, DOOR_STATION_EVENT_ENTITY_IDS from .const import DOMAIN
from .models import DoorBirdData
@callback @callback
def async_describe_events(hass, async_describe_event): def async_describe_events(hass: HomeAssistant, async_describe_event):
"""Describe logbook events.""" """Describe logbook events."""
@callback @callback
def async_describe_logbook_event(event): def async_describe_logbook_event(event: Event):
"""Describe a logbook event.""" """Describe a logbook event."""
doorbird_event = event.event_type.split("_", 1)[1]
return { return {
LOGBOOK_ENTRY_NAME: "Doorbird", LOGBOOK_ENTRY_NAME: "Doorbird",
LOGBOOK_ENTRY_MESSAGE: f"Event {event.event_type} was fired", LOGBOOK_ENTRY_MESSAGE: f"Event {event.event_type} was fired",
LOGBOOK_ENTRY_ENTITY_ID: hass.data[DOMAIN][ # Database entries before Jun 25th 2020 will not have an entity ID
DOOR_STATION_EVENT_ENTITY_IDS LOGBOOK_ENTRY_ENTITY_ID: event.data.get(ATTR_ENTITY_ID),
].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)),
} }
domain_data: dict[str, Any] = hass.data[DOMAIN] domain_data: dict[str, DoorBirdData] = hass.data[DOMAIN]
for data in domain_data.values(): for data in domain_data.values():
if DOOR_STATION not in data: for event in data.door_station.door_station_events:
# We need to skip door_station_event_entity_ids
continue
for event in data[DOOR_STATION].doorstation_events:
async_describe_event( async_describe_event(
DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event
) )

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