mirror of
https://github.com/home-assistant/core.git
synced 2025-08-05 21:55:10 +02:00
Merge branch 'dev' into feature/starlink-device-tracker
This commit is contained in:
13
.coveragerc
13
.coveragerc
@@ -168,6 +168,10 @@ omit =
|
||||
homeassistant/components/cmus/media_player.py
|
||||
homeassistant/components/coinbase/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/concord232/alarm_control_panel.py
|
||||
homeassistant/components/concord232/binary_sensor.py
|
||||
@@ -212,8 +216,9 @@ omit =
|
||||
homeassistant/components/dominos/*
|
||||
homeassistant/components/doods/*
|
||||
homeassistant/components/doorbird/__init__.py
|
||||
homeassistant/components/doorbird/button.py
|
||||
homeassistant/components/doorbird/camera.py
|
||||
homeassistant/components/doorbird/button.py
|
||||
homeassistant/components/doorbird/device.py
|
||||
homeassistant/components/doorbird/entity.py
|
||||
homeassistant/components/doorbird/util.py
|
||||
homeassistant/components/dormakaba_dkey/__init__.py
|
||||
@@ -305,6 +310,8 @@ omit =
|
||||
homeassistant/components/enphase_envoy/binary_sensor.py
|
||||
homeassistant/components/enphase_envoy/coordinator.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/switch.py
|
||||
homeassistant/components/entur_public_transport/*
|
||||
@@ -341,6 +348,7 @@ omit =
|
||||
homeassistant/components/ezviz/entity.py
|
||||
homeassistant/components/ezviz/select.py
|
||||
homeassistant/components/ezviz/sensor.py
|
||||
homeassistant/components/ezviz/siren.py
|
||||
homeassistant/components/ezviz/switch.py
|
||||
homeassistant/components/ezviz/update.py
|
||||
homeassistant/components/faa_delays/__init__.py
|
||||
@@ -1325,9 +1333,6 @@ omit =
|
||||
homeassistant/components/tplink_omada/__init__.py
|
||||
homeassistant/components/tplink_omada/binary_sensor.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/traccar/device_tracker.py
|
||||
homeassistant/components/tractive/__init__.py
|
||||
|
49
.github/workflows/ci.yaml
vendored
49
.github/workflows/ci.yaml
vendored
@@ -734,9 +734,14 @@ jobs:
|
||||
- name: Run pytest (fully)
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
timeout-minutes: 60
|
||||
id: pytest-full
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
set -o pipefail
|
||||
|
||||
python3 -X dev -m pytest \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
@@ -749,14 +754,19 @@ jobs:
|
||||
--cov-report=xml \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
tests
|
||||
tests \
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Run pytest (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
timeout-minutes: 10
|
||||
id: pytest-partial
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
set -o pipefail
|
||||
|
||||
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
|
||||
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
|
||||
@@ -774,7 +784,14 @@ jobs:
|
||||
--durations=0 \
|
||||
--durations-min=1 \
|
||||
-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
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
@@ -862,10 +879,15 @@ jobs:
|
||||
python3 -m script.translations develop --all
|
||||
- name: Run pytest (partially)
|
||||
timeout-minutes: 20
|
||||
id: pytest-partial
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
set -o pipefail
|
||||
mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g")
|
||||
|
||||
python3 -X dev -m pytest \
|
||||
-qq \
|
||||
@@ -881,7 +903,14 @@ jobs:
|
||||
tests/components/history \
|
||||
tests/components/logbook \
|
||||
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
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
@@ -969,10 +998,15 @@ jobs:
|
||||
python3 -m script.translations develop --all
|
||||
- name: Run pytest (partially)
|
||||
timeout-minutes: 20
|
||||
id: pytest-partial
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
set -o pipefail
|
||||
postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g")
|
||||
|
||||
python3 -X dev -m pytest \
|
||||
-qq \
|
||||
@@ -989,7 +1023,14 @@ jobs:
|
||||
tests/components/history \
|
||||
tests/components/logbook \
|
||||
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
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,6 +67,7 @@ htmlcov/
|
||||
test-reports/
|
||||
test-results.xml
|
||||
test-output.xml
|
||||
pytest-*.txt
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@@ -209,6 +209,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/coinbase/ @tombrien
|
||||
/homeassistant/components/color_extractor/ @GenericStudent
|
||||
/tests/components/color_extractor/ @GenericStudent
|
||||
/homeassistant/components/comelit/ @chemelli74
|
||||
/tests/components/comelit/ @chemelli74
|
||||
/homeassistant/components/comfoconnect/ @michaelarnauts
|
||||
/tests/components/comfoconnect/ @michaelarnauts
|
||||
/homeassistant/components/command_line/ @gjohansson-ST
|
||||
@@ -606,8 +608,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||
/homeassistant/components/ipma/ @dgomes @abmantis
|
||||
/tests/components/ipma/ @dgomes @abmantis
|
||||
/homeassistant/components/ipma/ @dgomes
|
||||
/tests/components/ipma/ @dgomes
|
||||
/homeassistant/components/ipp/ @ctalkington
|
||||
/tests/components/ipp/ @ctalkington
|
||||
/homeassistant/components/iqvia/ @bachya
|
||||
|
@@ -110,8 +110,7 @@ async def async_setup_hass(
|
||||
runtime_config: RuntimeConfig,
|
||||
) -> core.HomeAssistant | None:
|
||||
"""Set up Home Assistant."""
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = runtime_config.config_dir
|
||||
hass = core.HomeAssistant(runtime_config.config_dir)
|
||||
|
||||
async_enable_logging(
|
||||
hass,
|
||||
@@ -134,6 +133,7 @@ async def async_setup_hass(
|
||||
|
||||
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
|
||||
|
||||
loader.async_setup(hass)
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
|
||||
@@ -177,14 +177,15 @@ async def async_setup_hass(
|
||||
old_config = hass.config
|
||||
old_logging = hass.data.get(DATA_LOGGING)
|
||||
|
||||
hass = core.HomeAssistant()
|
||||
hass = core.HomeAssistant(old_config.config_dir)
|
||||
if old_logging:
|
||||
hass.data[DATA_LOGGING] = old_logging
|
||||
hass.config.skip_pip = old_config.skip_pip
|
||||
hass.config.skip_pip_packages = old_config.skip_pip_packages
|
||||
hass.config.internal_url = old_config.internal_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:
|
||||
_LOGGER.info("Starting in safe mode")
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""The AccuWeather component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
@@ -8,7 +9,6 @@ from typing import Any
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@@ -2,12 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from typing import Any
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@@ -50,3 +50,8 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
|
||||
ATTR_CONDITION_SUNNY: [1, 2, 5],
|
||||
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
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ from .const import (
|
||||
ATTR_SPEED,
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
CONDITION_CLASSES,
|
||||
CONDITION_MAP,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@@ -80,14 +80,7 @@ class AccuWeatherEntity(
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
try:
|
||||
return [
|
||||
k
|
||||
for k, v in CONDITION_CLASSES.items()
|
||||
if self.coordinator.data["WeatherIcon"] in v
|
||||
][0]
|
||||
except IndexError:
|
||||
return None
|
||||
return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float:
|
||||
@@ -177,9 +170,7 @@ class AccuWeatherEntity(
|
||||
],
|
||||
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
|
||||
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
|
||||
ATTR_FORECAST_CONDITION: [
|
||||
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
|
||||
][0],
|
||||
ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
|
||||
}
|
||||
for item in self.coordinator.data[ATTR_FORECAST]
|
||||
]
|
||||
|
@@ -2,11 +2,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
import aiopulse
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -43,7 +43,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(asyncio.TimeoutError):
|
||||
async with async_timeout.timeout(5):
|
||||
async with timeout(5):
|
||||
async for hub in aiopulse.Hub.discover():
|
||||
if hub.id not in already_configured:
|
||||
hubs.append(hub)
|
||||
|
@@ -1,12 +1,12 @@
|
||||
"""Support for Automation Device Specification (ADS)."""
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from collections import namedtuple
|
||||
import ctypes
|
||||
import logging
|
||||
import struct
|
||||
import threading
|
||||
|
||||
import async_timeout
|
||||
import pyads
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -301,7 +301,7 @@ class AdsEntity(Entity):
|
||||
self._ads_hub.add_device_notification, ads_var, plctype, update
|
||||
)
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
await self._event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
|
||||
|
@@ -1,4 +1,6 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||
@@ -8,7 +10,10 @@ from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -17,7 +22,8 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
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.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -79,10 +85,28 @@ async def async_setup_entry(
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
entities = []
|
||||
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))
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Add daily + hourly entity for legacy config entries, only add daily for new
|
||||
# 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)
|
||||
|
||||
@@ -95,6 +119,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
|
||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -112,20 +139,44 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
|
||||
self._attr_name = name
|
||||
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
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return self.coordinator.data[ATTR_API_CONDITION]
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
def _forecast(self, forecast_mode: str) -> list[Forecast]:
|
||||
"""Return the forecast array."""
|
||||
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
|
||||
forecast_map = FORECAST_MAP[self._forecast_mode]
|
||||
return [
|
||||
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
|
||||
for forecast in forecasts
|
||||
]
|
||||
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]]
|
||||
forecast_map = FORECAST_MAP[forecast_mode]
|
||||
return cast(
|
||||
list[Forecast],
|
||||
[
|
||||
{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
|
||||
def humidity(self):
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Weather data coordinator for the AEMET OpenData service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -41,7 +42,6 @@ from aemet_opendata.helpers import (
|
||||
get_forecast_hour_value,
|
||||
get_forecast_interval_value,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -139,7 +139,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
async def _async_update_data(self):
|
||||
data = {}
|
||||
async with async_timeout.timeout(120):
|
||||
async with timeout(120):
|
||||
weather_response = await self._get_aemet_weather()
|
||||
data = self._convert_weather_response(weather_response)
|
||||
return data
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""The Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from math import ceil
|
||||
@@ -9,7 +10,6 @@ from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
measurements = self.airly.create_measurements_session_point(
|
||||
self.latitude, self.longitude
|
||||
)
|
||||
async with async_timeout.timeout(20):
|
||||
async with timeout(20):
|
||||
try:
|
||||
await measurements.update()
|
||||
except (AirlyError, ClientConnectorError) as error:
|
||||
|
@@ -1,13 +1,13 @@
|
||||
"""Adds config flow for Airly."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -105,7 +105,7 @@ async def test_location(
|
||||
measurements = airly.create_measurements_session_point(
|
||||
latitude=latitude, longitude=longitude
|
||||
)
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
await measurements.update()
|
||||
|
||||
current = measurements.current
|
||||
|
@@ -1,13 +1,13 @@
|
||||
"""The Airzone integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone.exceptions import AirzoneError
|
||||
from aioairzone.localapi import AirzoneLocalApi
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
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]:
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
|
||||
async with timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneError as error:
|
||||
|
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.5"]
|
||||
"requirements": ["aioairzone==0.6.6"]
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
"""The Airzone Cloud integration coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
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]:
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
async with timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneCloudError as error:
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Support for Alexa skill auth."""
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
@@ -7,7 +8,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
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:
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
response = await session.post(
|
||||
LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
|
@@ -474,7 +474,24 @@ async def async_api_unlock(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""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 = (
|
||||
"The unlock directive is not supported for the following locales:"
|
||||
f" {config.locale}"
|
||||
|
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
@@ -10,7 +11,6 @@ from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components import event
|
||||
from homeassistant.const import MATCH_ALL, STATE_ON
|
||||
@@ -364,7 +364,7 @@ async def async_send_changereport_message(
|
||||
|
||||
assert config.endpoint is not None
|
||||
try:
|
||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
async with timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(
|
||||
config.endpoint,
|
||||
headers=headers,
|
||||
@@ -517,7 +517,7 @@ async def async_send_doorbell_event_message(
|
||||
|
||||
assert config.endpoint is not None
|
||||
try:
|
||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
async with timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(
|
||||
config.endpoint,
|
||||
headers=headers,
|
||||
|
@@ -2,13 +2,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from dataclasses import asdict as dataclass_asdict, dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components import hassio
|
||||
from homeassistant.components.api import ATTR_INSTALLATION_TYPE
|
||||
@@ -313,7 +313,7 @@ class Analytics:
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
async with timeout(30):
|
||||
response = await self.session.post(self.endpoint, json=payload)
|
||||
if response.status == 200:
|
||||
LOGGER.info(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
|
||||
from androidtvremote2 import (
|
||||
@@ -10,7 +11,6 @@ from androidtvremote2 import (
|
||||
ConnectionClosed,
|
||||
InvalidAuth,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(5.0):
|
||||
async with timeout(5.0):
|
||||
await api.async_connect()
|
||||
except InvalidAuth as exc:
|
||||
# The Android TV is hard reset or the certificate and key files were deleted.
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"""Support for Anova Coordinators."""
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -47,7 +47,7 @@ class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]):
|
||||
|
||||
async def _async_update_data(self) -> APCUpdate:
|
||||
try:
|
||||
async with async_timeout.timeout(5):
|
||||
async with timeout(5):
|
||||
return await self.anova_device.update()
|
||||
except AnovaOffline as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
@@ -1,17 +1,17 @@
|
||||
"""Rest API for Home Assistant."""
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from functools import lru_cache
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPBadRequest
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions.const import POLICY_READ
|
||||
from homeassistant.bootstrap import DATA_LOGGING
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import HomeAssistantView, require_admin
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
MATCH_ALL,
|
||||
@@ -110,10 +110,9 @@ class APIEventStream(HomeAssistantView):
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
|
||||
@require_admin
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized()
|
||||
hass = request.app["hass"]
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue()
|
||||
@@ -149,7 +148,7 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with async_timeout.timeout(STREAM_PING_INTERVAL):
|
||||
async with timeout(STREAM_PING_INTERVAL):
|
||||
payload = await to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
@@ -278,10 +277,9 @@ class APIEventView(HomeAssistantView):
|
||||
url = "/api/events/{event_type}"
|
||||
name = "api:event"
|
||||
|
||||
@require_admin
|
||||
async def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized()
|
||||
body = await request.text()
|
||||
try:
|
||||
event_data = json_loads(body) if body else None
|
||||
@@ -385,10 +383,9 @@ class APITemplateView(HomeAssistantView):
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
@require_admin
|
||||
async def post(self, request):
|
||||
"""Render a template."""
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized()
|
||||
try:
|
||||
data = await request.json()
|
||||
tpl = _cached_template(data["template"], request.app["hass"])
|
||||
@@ -405,10 +402,9 @@ class APIErrorLog(HomeAssistantView):
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error_log"
|
||||
|
||||
@require_admin
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized()
|
||||
return web.FileResponse(request.app["hass"].data[DATA_LOGGING])
|
||||
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"""Arcam component."""
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from arcam.fmj import ConnectionFailed
|
||||
from arcam.fmj.client import Client
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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:
|
||||
try:
|
||||
async with async_timeout.timeout(interval):
|
||||
async with timeout(interval):
|
||||
await client.start()
|
||||
|
||||
_LOGGER.debug("Client connected %s", client.host)
|
||||
|
@@ -1,10 +1,11 @@
|
||||
"""Arcam media player."""
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from arcam.fmj import SourceCodes
|
||||
from arcam.fmj import ConnectionFailed, SourceCodes
|
||||
from arcam.fmj.state import State
|
||||
|
||||
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.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
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):
|
||||
"""Representation of a media device."""
|
||||
|
||||
@@ -105,7 +122,10 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Once registered, add listener for events."""
|
||||
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
|
||||
def _data(host: str) -> None:
|
||||
@@ -137,13 +157,18 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Force update of state."""
|
||||
_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:
|
||||
"""Send mute command."""
|
||||
await self._state.set_mute(mute)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_exception
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select a specific source."""
|
||||
try:
|
||||
@@ -155,31 +180,37 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
await self._state.set_source(value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_exception
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Select a specific source."""
|
||||
try:
|
||||
await self._state.set_decode_mode(sound_mode)
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.error("Unsupported sound_mode %s", sound_mode)
|
||||
return
|
||||
except (KeyError, ValueError) as exception:
|
||||
raise HomeAssistantError(
|
||||
f"Unsupported sound_mode {sound_mode}"
|
||||
) from exception
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_exception
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
await self._state.set_volume(round(volume * 99.0))
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_exception
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Turn volume up for media player."""
|
||||
await self._state.inc_volume()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_exception
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Turn volume up for media player."""
|
||||
await self._state.dec_volume()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@convert_exception
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
if self._state.get_power() is not None:
|
||||
@@ -189,6 +220,7 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
_LOGGER.debug("Firing event to turn on device")
|
||||
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
|
||||
|
||||
@convert_exception
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the media player off."""
|
||||
await self._state.set_power(False)
|
||||
@@ -230,6 +262,7 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
|
||||
return root
|
||||
|
||||
@convert_exception
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
|
@@ -37,19 +37,18 @@ class AsekoBinarySensorEntityDescription(
|
||||
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="water_flow",
|
||||
name="Water Flow",
|
||||
translation_key="water_flow",
|
||||
icon="mdi:waves-arrow-right",
|
||||
value_fn=lambda unit: unit.water_flow,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_alarm",
|
||||
name="Alarm",
|
||||
translation_key="alarm",
|
||||
value_fn=lambda unit: unit.has_alarm,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_error",
|
||||
name="Error",
|
||||
value_fn=lambda unit: unit.has_error,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
),
|
||||
|
@@ -11,6 +11,8 @@ from .coordinator import AsekoDataUpdateCoordinator
|
||||
class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
|
||||
"""Representation of an aseko entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
|
||||
"""Initialize the aseko entity."""
|
||||
super().__init__(coordinator)
|
||||
|
@@ -45,13 +45,16 @@ class VariableSensorEntity(AsekoEntity, SensorEntity):
|
||||
super().__init__(unit, coordinator)
|
||||
self._variable = variable
|
||||
|
||||
variable_name = {
|
||||
"Air temp.": "Air Temperature",
|
||||
"Cl free": "Free Chlorine",
|
||||
"Water temp.": "Water Temperature",
|
||||
}.get(self._variable.name, self._variable.name)
|
||||
translation_key = {
|
||||
"Air temp.": "air_temperature",
|
||||
"Cl free": "free_chlorine",
|
||||
"Water temp.": "water_temperature",
|
||||
}.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_native_unit_of_measurement = self._variable.unit
|
||||
|
||||
|
@@ -16,5 +16,26 @@
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -254,6 +254,8 @@ class PipelineEventType(StrEnum):
|
||||
WAKE_WORD_START = "wake_word-start"
|
||||
WAKE_WORD_END = "wake_word-end"
|
||||
STT_START = "stt-start"
|
||||
STT_VAD_START = "stt-vad-start"
|
||||
STT_VAD_END = "stt-vad-end"
|
||||
STT_END = "stt-end"
|
||||
INTENT_START = "intent-start"
|
||||
INTENT_END = "intent-end"
|
||||
@@ -612,11 +614,31 @@ class PipelineRun:
|
||||
stream: AsyncIterable[bytes],
|
||||
) -> AsyncGenerator[bytes, None]:
|
||||
"""Stop stream when voice command is finished."""
|
||||
sent_vad_start = False
|
||||
timestamp_ms = 0
|
||||
async for chunk in stream:
|
||||
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
|
||||
|
||||
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
|
||||
timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz
|
||||
|
||||
# Transcribe audio stream
|
||||
result = await self.stt_provider.async_process_audio_stream(
|
||||
|
@@ -7,7 +7,6 @@ from collections.abc import AsyncGenerator, Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation, stt, tts, websocket_api
|
||||
@@ -207,7 +206,7 @@ async def websocket_run(
|
||||
|
||||
try:
|
||||
# Task contains a timeout
|
||||
async with async_timeout.timeout(timeout):
|
||||
async with asyncio.timeout(timeout):
|
||||
await run_task
|
||||
except asyncio.TimeoutError:
|
||||
pipeline_input.run.process_event(
|
||||
|
@@ -1,8 +1,8 @@
|
||||
"""The ATAG Integration."""
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from pyatag import AtagException, AtagOne
|
||||
|
||||
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():
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(20):
|
||||
async with timeout(20):
|
||||
try:
|
||||
await atag.update()
|
||||
except AtagException as err:
|
||||
|
@@ -109,10 +109,6 @@ def _native_datetime() -> datetime:
|
||||
class AugustBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes August binary_sensor entity."""
|
||||
|
||||
# AugustBinarySensor does not support UNDEFINED or None,
|
||||
# restrict the type to str.
|
||||
name: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AugustDoorbellRequiredKeysMixin:
|
||||
@@ -128,34 +124,28 @@ class AugustDoorbellBinarySensorEntityDescription(
|
||||
):
|
||||
"""Describes August binary_sensor entity."""
|
||||
|
||||
# AugustDoorbellBinarySensor does not support UNDEFINED or None,
|
||||
# restrict the type to str.
|
||||
name: str = ""
|
||||
|
||||
|
||||
SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription(
|
||||
key="door_open",
|
||||
name="Open",
|
||||
key="open",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
)
|
||||
|
||||
SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_motion",
|
||||
name="Motion",
|
||||
key="motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
value_fn=_retrieve_motion_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_image_capture",
|
||||
name="Image Capture",
|
||||
key="image capture",
|
||||
translation_key="image_capture",
|
||||
icon="mdi:file-image",
|
||||
value_fn=_retrieve_image_capture_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_online",
|
||||
name="Online",
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_online_state,
|
||||
@@ -166,8 +156,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
|
||||
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_ding",
|
||||
name="Ding",
|
||||
key="ding",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
value_fn=_retrieve_ding_state,
|
||||
is_time_based=True,
|
||||
@@ -236,8 +225,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||
self.entity_description = description
|
||||
self._data = data
|
||||
self._device = device
|
||||
self._attr_name = f"{device.device_name} {description.name}"
|
||||
self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
|
||||
self._attr_unique_id = f"{self._device_id}_{description.key}"
|
||||
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
@@ -284,8 +272,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||
self.entity_description = description
|
||||
self._check_for_off_update_listener = None
|
||||
self._data = data
|
||||
self._attr_name = f"{device.device_name} {description.name}"
|
||||
self._attr_unique_id = f"{self._device_id}_{description.name.lower()}"
|
||||
self._attr_unique_id = f"{self._device_id}_{description.key}"
|
||||
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
|
@@ -24,10 +24,11 @@ async def async_setup_entry(
|
||||
class AugustWakeLockButton(AugustEntityMixin, ButtonEntity):
|
||||
"""Representation of an August lock wake button."""
|
||||
|
||||
_attr_translation_key = "wake"
|
||||
|
||||
def __init__(self, data: AugustData, device: Lock) -> None:
|
||||
"""Initialize the lock wake button."""
|
||||
super().__init__(data, device)
|
||||
self._attr_name = f"{device.device_name} Wake"
|
||||
self._attr_unique_id = f"{self._device_id}_wake"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
@@ -33,16 +33,17 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
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):
|
||||
"""Initialize a August security camera."""
|
||||
"""Initialize an August security camera."""
|
||||
super().__init__(data, device)
|
||||
self._timeout = timeout
|
||||
self._session = session
|
||||
self._image_url = None
|
||||
self._image_content = None
|
||||
self._attr_name = f"{device.device_name} Camera"
|
||||
self._attr_unique_id = f"{self._device_id:s}_camera"
|
||||
|
||||
@property
|
||||
|
@@ -19,6 +19,7 @@ class AugustEntityMixin(Entity):
|
||||
"""Base implementation for August device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, data: AugustData, device: Doorbell | Lock) -> None:
|
||||
"""Initialize an August device."""
|
||||
|
@@ -37,11 +37,12 @@ async def async_setup_entry(
|
||||
class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
|
||||
"""Representation of an August lock."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, data, device):
|
||||
"""Initialize the lock."""
|
||||
super().__init__(data, device)
|
||||
self._lock_status = None
|
||||
self._attr_name = device.device_name
|
||||
self._attr_unique_id = f"{self._device_id:s}_lock"
|
||||
self._update_from_data()
|
||||
|
||||
|
@@ -75,7 +75,6 @@ class AugustSensorEntityDescription(
|
||||
|
||||
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
|
||||
key="device_battery",
|
||||
name="Battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=_retrieve_device_battery_state,
|
||||
@@ -83,7 +82,6 @@ SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
|
||||
|
||||
SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail](
|
||||
key="linked_keypad_battery",
|
||||
name="Battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
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):
|
||||
"""Representation of an August lock operation sensor."""
|
||||
|
||||
_attr_translation_key = "operator"
|
||||
|
||||
def __init__(self, data, device):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data, device)
|
||||
@@ -188,11 +188,6 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
|
||||
self._entity_picture = None
|
||||
self._update_from_data()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._device.device_name} Operator"
|
||||
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
"""Get the latest state of the sensor and update activity."""
|
||||
@@ -278,7 +273,6 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]):
|
||||
super().__init__(data, device)
|
||||
self.entity_description = description
|
||||
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._update_from_data()
|
||||
|
||||
|
@@ -37,5 +37,27 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,9 +13,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the binary_sensor platform."""
|
||||
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])
|
||||
|
||||
|
@@ -19,14 +19,14 @@ class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AuroraDataUpdateCoordinator,
|
||||
name: str,
|
||||
translation_key: str,
|
||||
icon: str,
|
||||
) -> None:
|
||||
"""Initialize the Aurora Entity."""
|
||||
|
||||
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_icon = icon
|
||||
|
||||
|
@@ -17,7 +17,7 @@ async def async_setup_entry(
|
||||
|
||||
entity = AuroraSensor(
|
||||
coordinator=coordinator,
|
||||
name=f"{coordinator.name} Aurora Visibility %",
|
||||
translation_key="visibility",
|
||||
icon="mdi:gauge",
|
||||
)
|
||||
|
||||
|
@@ -25,5 +25,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"visibility_alert": {
|
||||
"name": "Visibility alert"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"visibility": {
|
||||
"name": "Visibility"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,11 @@
|
||||
"""The awair component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import gather
|
||||
from asyncio import gather, timeout
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from async_timeout import timeout
|
||||
from python_awair import Awair, AwairLocal
|
||||
from python_awair.air_data import AirData
|
||||
from python_awair.devices import AwairBaseDevice, AwairLocalDevice
|
||||
|
@@ -1,10 +1,10 @@
|
||||
"""Axis network device abstraction."""
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
import axis
|
||||
from axis.configuration import Configuration
|
||||
from axis.errors import Unauthorized
|
||||
@@ -253,7 +253,7 @@ async def get_axis_device(
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
async with timeout(30):
|
||||
await device.vapix.initialize()
|
||||
|
||||
return device
|
||||
|
@@ -2,10 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
|
||||
from aiobafi6 import Device, Service
|
||||
from aiobafi6.discovery import PORT
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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()
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(RUN_TIMEOUT):
|
||||
async with timeout(RUN_TIMEOUT):
|
||||
await device.async_wait_available()
|
||||
except asyncio.TimeoutError as ex:
|
||||
run_future.cancel()
|
||||
|
@@ -2,12 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiobafi6 import Device, Service
|
||||
from aiobafi6.discovery import PORT
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
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))
|
||||
run_future = device.async_run()
|
||||
try:
|
||||
async with async_timeout.timeout(RUN_TIMEOUT):
|
||||
async with timeout(RUN_TIMEOUT):
|
||||
await device.async_wait_available()
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise CannotConnect from ex
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"""Websocket API for blueprint."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, cast
|
||||
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
@@ -72,7 +72,7 @@ async def ws_import_blueprint(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""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"])
|
||||
|
||||
if imported_blueprint is None:
|
||||
|
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import CancelledError
|
||||
from asyncio import CancelledError, timeout
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
@@ -12,7 +12,6 @@ from urllib import parse
|
||||
import aiohttp
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from aiohttp.hdrs import CONNECTION, KEEP_ALIVE
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
import xmltodict
|
||||
|
||||
@@ -355,7 +354,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self._hass)
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
response = await websession.get(url)
|
||||
|
||||
if response.status == HTTPStatus.OK:
|
||||
@@ -396,7 +395,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
_LOGGER.debug("Calling URL: %s", url)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(125):
|
||||
async with timeout(125):
|
||||
response = await self._polling_session.get(
|
||||
url, headers={CONNECTION: KEEP_ALIVE}
|
||||
)
|
||||
|
@@ -4,11 +4,11 @@ These APIs are the only documented way to interact with the bluetooth integratio
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import Future
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import async_timeout
|
||||
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||
@@ -152,7 +152,7 @@ async def async_process_advertisements(
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(timeout):
|
||||
async with asyncio.timeout(timeout):
|
||||
return await done
|
||||
finally:
|
||||
unload()
|
||||
|
@@ -19,6 +19,6 @@
|
||||
"bluetooth-adapters==0.16.0",
|
||||
"bluetooth-auto-recovery==1.2.1",
|
||||
"bluetooth-data-tools==1.8.0",
|
||||
"dbus-fast==1.91.2"
|
||||
"dbus-fast==1.91.4"
|
||||
]
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import logging
|
||||
import platform
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
import bleak
|
||||
from bleak import BleakError
|
||||
from bleak.assigned_numbers import AdvertisementDataType
|
||||
@@ -220,7 +219,7 @@ class HaScanner(BaseHaScanner):
|
||||
START_ATTEMPTS,
|
||||
)
|
||||
try:
|
||||
async with async_timeout.timeout(START_TIMEOUT):
|
||||
async with asyncio.timeout(START_TIMEOUT):
|
||||
await self.scanner.start() # type: ignore[no-untyped-call]
|
||||
except InvalidMessageError as ex:
|
||||
_LOGGER.debug(
|
||||
|
@@ -25,7 +25,9 @@ async def async_setup_entry(
|
||||
entities: list[BinarySensorEntity] = []
|
||||
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(
|
||||
ShutterContactSensor(
|
||||
device=binary_sensor,
|
||||
@@ -37,6 +39,7 @@ async def async_setup_entry(
|
||||
for binary_sensor in (
|
||||
session.device_helper.motion_detectors
|
||||
+ session.device_helper.shutter_contacts
|
||||
+ session.device_helper.shutter_contacts2
|
||||
+ session.device_helper.smoke_detectors
|
||||
+ session.device_helper.thermostats
|
||||
+ session.device_helper.twinguards
|
||||
|
@@ -1,10 +1,10 @@
|
||||
"""The Brother component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -79,7 +79,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]):
|
||||
async def _async_update_data(self) -> BrotherSensors:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
async with async_timeout.timeout(20):
|
||||
async with timeout(20):
|
||||
data = await self.brother.async_update()
|
||||
except (ConnectionError, SnmpError, UnsupportedModelError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
@@ -1,10 +1,10 @@
|
||||
"""The brunt component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
|
||||
import async_timeout
|
||||
from brunt import BruntClientAsync, Thing
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
things = await bapi.async_get_things(force=True)
|
||||
return {thing.serial: thing for thing in things}
|
||||
except ServerDisconnectedError as err:
|
||||
|
@@ -14,10 +14,10 @@ CONF_TIMEFRAME = "timeframe"
|
||||
SUPPORTED_COUNTRY_CODES = ["NL", "BE"]
|
||||
DEFAULT_COUNTRY = "NL"
|
||||
|
||||
"""Schedule next call after (minutes)."""
|
||||
SCHEDULE_OK = 10
|
||||
"""When an error occurred, new call after (minutes)."""
|
||||
"""Schedule next call after (minutes)."""
|
||||
SCHEDULE_NOK = 2
|
||||
"""When an error occurred, new call after (minutes)."""
|
||||
|
||||
STATE_CONDITIONS = ["clear", "cloudy", "fog", "rainy", "snowy", "lightning"]
|
||||
|
||||
|
@@ -714,17 +714,18 @@ async def async_setup_entry(
|
||||
timeframe,
|
||||
)
|
||||
|
||||
# create weather entities:
|
||||
entities = [
|
||||
BrSensor(config.get(CONF_NAME, "Buienradar"), coordinates, description)
|
||||
for description in SENSOR_TYPES
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
# create weather data:
|
||||
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
|
||||
await data.async_update()
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class BrSensor(SensorEntity):
|
||||
@@ -753,9 +754,9 @@ class BrSensor(SensorEntity):
|
||||
self._timeframe = None
|
||||
|
||||
@callback
|
||||
def data_updated(self, data):
|
||||
def data_updated(self, data: BrData):
|
||||
"""Update data."""
|
||||
if self.hass and self._load_data(data):
|
||||
if self._load_data(data.data) and self.hass:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"""Shared utilities for different supported platforms."""
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
from buienradar.buienradar import parse_data
|
||||
from buienradar.constants import (
|
||||
ATTRIBUTION,
|
||||
@@ -27,7 +27,7 @@ from buienradar.constants import (
|
||||
from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
|
||||
|
||||
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.event import async_track_point_in_utc_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -75,9 +75,10 @@ class BrData:
|
||||
|
||||
# Update all 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."""
|
||||
_LOGGER.debug("Scheduling next update in %s minutes", minute)
|
||||
nxt = dt_util.utcnow() + timedelta(minutes=minute)
|
||||
@@ -92,7 +93,7 @@ class BrData:
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
resp = await websession.get(url)
|
||||
|
||||
result[STATUS_CODE] = resp.status
|
||||
@@ -108,9 +109,9 @@ class BrData:
|
||||
return result
|
||||
finally:
|
||||
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."""
|
||||
content = await self.get_data(JSON_FEED_URL)
|
||||
|
||||
@@ -123,9 +124,7 @@ class BrData:
|
||||
content.get(MESSAGE),
|
||||
content.get(STATUS_CODE),
|
||||
)
|
||||
# schedule new call
|
||||
await self.schedule_update(SCHEDULE_NOK)
|
||||
return
|
||||
return None
|
||||
self.load_error_count = 0
|
||||
|
||||
# rounding coordinates prevents unnecessary redirects/calls
|
||||
@@ -143,9 +142,7 @@ class BrData:
|
||||
raincontent.get(MESSAGE),
|
||||
raincontent.get(STATUS_CODE),
|
||||
)
|
||||
# schedule new call
|
||||
await self.schedule_update(SCHEDULE_NOK)
|
||||
return
|
||||
return None
|
||||
self.rain_error_count = 0
|
||||
|
||||
result = parse_data(
|
||||
@@ -164,12 +161,21 @@ class BrData:
|
||||
"Unable to parse data from Buienradar. (Msg: %s)",
|
||||
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
|
||||
|
||||
self.data = result.get(DATA)
|
||||
self.data = data
|
||||
await self.update_devices()
|
||||
await self.schedule_update(SCHEDULE_OK)
|
||||
self.async_schedule_update(SCHEDULE_OK)
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
|
@@ -34,7 +34,9 @@ from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -48,7 +50,7 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
# Reuse data and API logic from the sensor implementation
|
||||
@@ -82,6 +84,11 @@ CONDITION_CLASSES = {
|
||||
ATTR_CONDITION_WINDY_VARIANT: (),
|
||||
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(
|
||||
@@ -99,24 +106,16 @@ async def async_setup_entry(
|
||||
|
||||
coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)}
|
||||
|
||||
# create weather data:
|
||||
data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, None)
|
||||
hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data
|
||||
# create weather device:
|
||||
# create weather entity:
|
||||
_LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates)
|
||||
entities = [BrWeather(config, coordinates)]
|
||||
|
||||
# create condition helper
|
||||
if DATA_CONDITION not in hass.data[DOMAIN]:
|
||||
cond_keys = [str(chr(x)) for x in range(97, 123)]
|
||||
hass.data[DOMAIN][DATA_CONDITION] = dict.fromkeys(cond_keys)
|
||||
for cond, condlst in CONDITION_CLASSES.items():
|
||||
for condi in condlst:
|
||||
hass.data[DOMAIN][DATA_CONDITION][condi] = cond
|
||||
# create weather data:
|
||||
data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, entities)
|
||||
hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data
|
||||
await data.async_update()
|
||||
|
||||
async_add_entities([BrWeather(data, config, coordinates)])
|
||||
|
||||
# schedule the first update in 1 minute from now:
|
||||
await data.schedule_update(1)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class BrWeather(WeatherEntity):
|
||||
@@ -127,81 +126,62 @@ class BrWeather(WeatherEntity):
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_visibility_unit = UnitOfLength.METERS
|
||||
_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."""
|
||||
self._stationname = config.get(CONF_NAME, "Buienradar")
|
||||
self._attr_name = (
|
||||
self._stationname or f"BR {data.stationname or '(unknown station)'}"
|
||||
)
|
||||
self._data = data
|
||||
self._attr_name = self._stationname or f"BR {'(unknown station)'}"
|
||||
|
||||
self._attr_unique_id = "{:2.6f}{:2.6f}".format(
|
||||
coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE]
|
||||
)
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return self._data.attribution
|
||||
@callback
|
||||
def data_updated(self, data: BrData) -> None:
|
||||
"""Update data."""
|
||||
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
|
||||
def condition(self):
|
||||
if not self.hass:
|
||||
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."""
|
||||
if (
|
||||
self._data
|
||||
and self._data.condition
|
||||
and (ccode := self._data.condition.get(CONDCODE))
|
||||
and (conditions := self.hass.data[DOMAIN].get(DATA_CONDITION))
|
||||
):
|
||||
return conditions.get(ccode)
|
||||
if data.condition and (ccode := data.condition.get(CONDCODE)):
|
||||
return CONDITION_MAP.get(ccode)
|
||||
return None
|
||||
|
||||
@property
|
||||
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):
|
||||
def _calc_forecast(self, data: BrData):
|
||||
"""Return the forecast array."""
|
||||
fcdata_out = []
|
||||
cond = self.hass.data[DOMAIN][DATA_CONDITION]
|
||||
|
||||
if not self._data.forecast:
|
||||
if not data.forecast:
|
||||
return None
|
||||
|
||||
for data_in in self._data.forecast:
|
||||
for data_in in data.forecast:
|
||||
# remap keys from external library to
|
||||
# keys understood by the weather component:
|
||||
condcode = data_in.get(CONDITION, []).get(CONDCODE)
|
||||
condcode = data_in.get(CONDITION, {}).get(CONDCODE)
|
||||
data_out = {
|
||||
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: data_in.get(MAX_TEMP),
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: data_in.get(RAIN),
|
||||
@@ -212,3 +192,7 @@ class BrWeather(WeatherEntity):
|
||||
fcdata_out.append(data_out)
|
||||
|
||||
return fcdata_out
|
||||
|
||||
async def async_forecast_daily(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
return self._attr_forecast
|
||||
|
@@ -87,6 +87,7 @@ def setup_platform(
|
||||
calendars = client.principal().calendars()
|
||||
|
||||
calendar_devices = []
|
||||
device_id: str | None
|
||||
for calendar in list(calendars):
|
||||
# If a calendar name was given in the configuration,
|
||||
# ignore all the others
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.2.0"]
|
||||
"requirements": ["caldav==1.3.6"]
|
||||
}
|
||||
|
@@ -15,7 +15,6 @@ from random import SystemRandom
|
||||
from typing import Any, Final, cast, final
|
||||
|
||||
from aiohttp import hdrs, web
|
||||
import async_timeout
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -168,7 +167,7 @@ async def _async_get_image(
|
||||
are handled.
|
||||
"""
|
||||
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(
|
||||
width=width, height=height
|
||||
):
|
||||
@@ -525,7 +524,7 @@ class Camera(Entity):
|
||||
self._create_stream_lock = asyncio.Lock()
|
||||
async with self._create_stream_lock:
|
||||
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()
|
||||
if not source:
|
||||
return None
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"""Provides the Canary DataUpdateCoordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import ValuesView
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from async_timeout import timeout
|
||||
from canary.api import Api
|
||||
from canary.model import Location, Reading
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
@@ -58,7 +58,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]):
|
||||
"""Fetch data from Canary."""
|
||||
|
||||
try:
|
||||
async with timeout(15):
|
||||
async with asyncio.timeout(15):
|
||||
return await self.hass.async_add_executor_job(self._update_data)
|
||||
except (ConnectTimeout, HTTPError) as error:
|
||||
raise UpdateFailed(f"Invalid response from API: {error}") from error
|
||||
|
@@ -77,6 +77,7 @@ class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]):
|
||||
"""Defines a base Cert Expiry entity."""
|
||||
|
||||
_attr_icon = "mdi:certificate"
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
@@ -91,6 +92,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
|
||||
"""Implementation of the Cert Expiry timestamp sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
_attr_translation_key = "certificate_expiry"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -98,7 +100,6 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize a Cert Expiry timestamp sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})"
|
||||
self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{coordinator.host}:{coordinator.port}")},
|
||||
|
@@ -20,5 +20,12 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"import_failed": "Import from config failed"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"certificate_expiry": {
|
||||
"name": "Cert expiry"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -140,7 +139,7 @@ async def async_citybikes_request(hass, uri, schema):
|
||||
try:
|
||||
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))
|
||||
|
||||
json_response = await req.json()
|
||||
|
@@ -10,7 +10,6 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
from hass_nabucasa import Cloud, cloud_api
|
||||
from yarl import URL
|
||||
|
||||
@@ -501,7 +500,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
||||
|
||||
return True
|
||||
|
@@ -54,7 +54,6 @@ class CloudClient(Interface):
|
||||
@property
|
||||
def base_path(self) -> Path:
|
||||
"""Return path to base dir."""
|
||||
assert self._hass.config.config_dir is not None
|
||||
return Path(self._hass.config.config_dir)
|
||||
|
||||
@property
|
||||
|
@@ -10,7 +10,6 @@ from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
import attr
|
||||
from hass_nabucasa import Cloud, auth, thingtalk
|
||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||
@@ -252,7 +251,7 @@ class CloudLogoutView(HomeAssistantView):
|
||||
hass = request.app["hass"]
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
async with asyncio.timeout(REQUEST_TIMEOUT):
|
||||
await cloud.logout()
|
||||
|
||||
return self.json_message("ok")
|
||||
@@ -292,7 +291,7 @@ class CloudRegisterView(HomeAssistantView):
|
||||
if location_info.zip_code is not None:
|
||||
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(
|
||||
data["email"],
|
||||
data["password"],
|
||||
@@ -316,7 +315,7 @@ class CloudResendConfirmView(HomeAssistantView):
|
||||
hass = request.app["hass"]
|
||||
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"])
|
||||
|
||||
return self.json_message("ok")
|
||||
@@ -336,7 +335,7 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
hass = request.app["hass"]
|
||||
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"])
|
||||
|
||||
return self.json_message("ok")
|
||||
@@ -439,7 +438,7 @@ async def websocket_update_prefs(
|
||||
if changes.get(PREF_ALEXA_REPORT_STATE):
|
||||
alexa_config = await cloud.client.get_alexa_config()
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
await alexa_config.async_get_access_token()
|
||||
except asyncio.TimeoutError:
|
||||
connection.send_error(
|
||||
@@ -779,7 +778,7 @@ async def alexa_sync(
|
||||
cloud = hass.data[DOMAIN]
|
||||
alexa_config = await cloud.client.get_alexa_config()
|
||||
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
try:
|
||||
success = await alexa_config.async_sync_entities()
|
||||
except alexa_errors.NoTokenAvailable:
|
||||
@@ -808,7 +807,7 @@ async def thingtalk_convert(
|
||||
"""Convert a query."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
try:
|
||||
connection.send_result(
|
||||
msg["id"], await thingtalk.async_convert(cloud, msg["query"])
|
||||
|
@@ -6,7 +6,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
import async_timeout
|
||||
from hass_nabucasa import Cloud, cloud_api
|
||||
|
||||
from .client import CloudClient
|
||||
@@ -18,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None:
|
||||
"""Fetch the subscription info."""
|
||||
try:
|
||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
async with asyncio.timeout(REQUEST_TIMEOUT):
|
||||
return await cloud_api.async_subscription_info(cloud)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error(
|
||||
@@ -39,7 +38,7 @@ async def async_migrate_paypal_agreement(
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate a paypal agreement from legacy."""
|
||||
try:
|
||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
async with asyncio.timeout(REQUEST_TIMEOUT):
|
||||
return await cloud_api.async_migrate_paypal_agreement(cloud)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error(
|
||||
|
@@ -4,7 +4,6 @@ import io
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
from colorthief import ColorThief
|
||||
from PIL import UnidentifiedImageError
|
||||
import voluptuous as vol
|
||||
@@ -120,7 +119,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
response = await session.get(url)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||
|
@@ -7,7 +7,6 @@ import json
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -112,7 +111,7 @@ class ComedHourlyPricingSensor(SensorEntity):
|
||||
else:
|
||||
url_string += "?type=currenthouraverage"
|
||||
|
||||
async with async_timeout.timeout(60):
|
||||
async with asyncio.timeout(60):
|
||||
response = await self.websession.get(url_string)
|
||||
# The API responds with MIME type 'text/html'
|
||||
text = await response.text()
|
||||
|
34
homeassistant/components/comelit/__init__.py
Normal file
34
homeassistant/components/comelit/__init__.py
Normal 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
|
145
homeassistant/components/comelit/config_flow.py
Normal file
145
homeassistant/components/comelit/config_flow.py
Normal 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."""
|
6
homeassistant/components/comelit/const.py
Normal file
6
homeassistant/components/comelit/const.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Comelit constants."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "comelit"
|
50
homeassistant/components/comelit/coordinator.py
Normal file
50
homeassistant/components/comelit/coordinator.py
Normal 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
|
78
homeassistant/components/comelit/light.py
Normal file
78
homeassistant/components/comelit/light.py
Normal 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
|
10
homeassistant/components/comelit/manifest.json
Normal file
10
homeassistant/components/comelit/manifest.json
Normal 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"]
|
||||
}
|
31
homeassistant/components/comelit/strings.json
Normal file
31
homeassistant/components/comelit/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,13 +16,12 @@ from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
STATE_CLASSES_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_SCAN_INTERVAL,
|
||||
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.issue_registry import IssueSeverity, async_create_issue
|
||||
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.util import dt as dt_util
|
||||
|
||||
@@ -47,6 +50,16 @@ CONF_JSON_ATTRIBUTES = "json_attributes"
|
||||
|
||||
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)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
@@ -87,30 +100,25 @@ async def async_setup_platform(
|
||||
|
||||
name: str = sensor_config[CONF_NAME]
|
||||
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)
|
||||
command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT]
|
||||
unique_id: str | None = sensor_config.get(CONF_UNIQUE_ID)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
|
||||
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)
|
||||
|
||||
trigger_entity_config = {
|
||||
CONF_UNIQUE_ID: unique_id,
|
||||
CONF_NAME: Template(name, hass),
|
||||
CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS),
|
||||
}
|
||||
trigger_entity_config = {CONF_NAME: Template(name, hass)}
|
||||
for key in TRIGGER_ENTITY_OPTIONS:
|
||||
if key not in sensor_config:
|
||||
continue
|
||||
trigger_entity_config[key] = sensor_config[key]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
CommandSensor(
|
||||
data,
|
||||
trigger_entity_config,
|
||||
unit,
|
||||
state_class,
|
||||
value_template,
|
||||
json_attributes,
|
||||
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."""
|
||||
|
||||
_attr_should_poll = False
|
||||
@@ -128,8 +136,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
config: ConfigType,
|
||||
unit_of_measurement: str | None,
|
||||
state_class: SensorStateClass | None,
|
||||
value_template: Template | None,
|
||||
json_attributes: list[str] | None,
|
||||
scan_interval: timedelta,
|
||||
@@ -141,8 +147,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity):
|
||||
self._json_attributes = json_attributes
|
||||
self._attr_native_value = None
|
||||
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._process_updates: asyncio.Lock | None = None
|
||||
|
||||
|
@@ -11,7 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
|
||||
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.exceptions import DependencyError, Unauthorized
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -138,12 +138,11 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
|
||||
"""Not implemented."""
|
||||
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):
|
||||
"""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
|
||||
try:
|
||||
return await super().post(request)
|
||||
@@ -164,19 +163,18 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
url = "/api/config/config_entries/flow/{flow_id}"
|
||||
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."""
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
|
||||
|
||||
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):
|
||||
"""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
|
||||
return await super().post(request, flow_id)
|
||||
|
||||
@@ -206,15 +204,14 @@ class OptionManagerFlowIndexView(FlowManagerIndexView):
|
||||
url = "/api/config/config_entries/options/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):
|
||||
"""Handle a POST request.
|
||||
|
||||
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
|
||||
return await super().post(request)
|
||||
|
||||
@@ -225,19 +222,18 @@ class OptionManagerFlowResourceView(FlowManagerResourceView):
|
||||
url = "/api/config/config_entries/options/flow/{flow_id}"
|
||||
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."""
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||
|
||||
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):
|
||||
"""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
|
||||
return await super().post(request, flow_id)
|
||||
|
||||
|
@@ -4,7 +4,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
from async_timeout import timeout
|
||||
from pydaikin.daikin_base import Appliance
|
||||
|
||||
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)
|
||||
try:
|
||||
async with timeout(TIMEOUT):
|
||||
async with asyncio.timeout(TIMEOUT):
|
||||
device = await Appliance.factory(
|
||||
host, session, key=key, uuid=uuid, password=password
|
||||
)
|
||||
|
@@ -4,7 +4,6 @@ import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from aiohttp import ClientError, web_exceptions
|
||||
from async_timeout import timeout
|
||||
from pydaikin.daikin_base import Appliance, DaikinException
|
||||
from pydaikin.discovery import Discovery
|
||||
import voluptuous as vol
|
||||
@@ -70,7 +69,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
password = None
|
||||
|
||||
try:
|
||||
async with timeout(TIMEOUT):
|
||||
async with asyncio.timeout(TIMEOUT):
|
||||
device = await Appliance.factory(
|
||||
host,
|
||||
async_get_clientsession(self.hass),
|
||||
|
@@ -9,7 +9,6 @@ from pprint import pformat
|
||||
from typing import Any, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import async_timeout
|
||||
from pydeconz.errors import LinkButtonNotPressed, RequestError, ResponseError
|
||||
from pydeconz.gateway import DeconzSession
|
||||
from pydeconz.utils import (
|
||||
@@ -101,7 +100,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
self.bridges = await deconz_discovery(session)
|
||||
|
||||
except (asyncio.TimeoutError, ResponseError):
|
||||
@@ -159,7 +158,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
deconz_session = DeconzSession(session, self.host, self.port)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
api_key = await deconz_session.get_api_key()
|
||||
|
||||
except LinkButtonNotPressed:
|
||||
@@ -180,7 +179,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
self.bridge_id = await deconz_get_bridge_id(
|
||||
session, self.host, self.port, self.api_key
|
||||
)
|
||||
|
@@ -7,7 +7,6 @@ from collections.abc import Callable
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import async_timeout
|
||||
from pydeconz import DeconzSession, errors
|
||||
from pydeconz.interfaces import sensors
|
||||
from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler
|
||||
@@ -353,7 +352,7 @@ async def get_deconz_session(
|
||||
config[CONF_API_KEY],
|
||||
)
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
await deconz_session.refresh_state()
|
||||
return deconz_session
|
||||
|
||||
|
@@ -46,6 +46,11 @@ CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
ATTR_CONDITION_WINDY_VARIANT: [],
|
||||
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)
|
||||
|
||||
@@ -237,9 +242,7 @@ class DemoWeather(WeatherEntity):
|
||||
@property
|
||||
def condition(self) -> str:
|
||||
"""Return the weather condition."""
|
||||
return [
|
||||
k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v
|
||||
][0]
|
||||
return CONDITION_MAP[self._condition.lower()]
|
||||
|
||||
async def async_forecast_daily(self) -> list[Forecast]:
|
||||
"""Return the daily forecast."""
|
||||
|
@@ -1,10 +1,10 @@
|
||||
"""The devolo Home Network integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
from devolo_plc_api import Device
|
||||
from devolo_plc_api.device_api import (
|
||||
ConnectedStationInfo,
|
||||
@@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.plcnet
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
return await device.plcnet.async_get_network_overview()
|
||||
except DeviceUnavailable as 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."""
|
||||
assert device.device
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
return await device.device.async_get_wifi_guest_access()
|
||||
except DeviceUnavailable as 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."""
|
||||
assert device.device
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
return await device.device.async_get_led_setting()
|
||||
except DeviceUnavailable as 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."""
|
||||
assert device.device
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with asyncio.timeout(10):
|
||||
return await device.device.async_get_wifi_connected_station()
|
||||
except DeviceUnavailable as 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."""
|
||||
assert device.device
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
async with asyncio.timeout(30):
|
||||
return await device.device.async_get_wifi_neighbor_access_points()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
@@ -6,7 +6,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
_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."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, username, "value")
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._key = "mg_dl" if unit_of_measurement == MG_DL else "mmol_l"
|
||||
self._attr_name = f"{DOMAIN}_{username}_glucose_value"
|
||||
self._attr_unique_id = f"{username}-value"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
@@ -50,14 +68,13 @@ class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity):
|
||||
return None
|
||||
|
||||
|
||||
class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity):
|
||||
class DexcomGlucoseTrendSensor(DexcomSensorEntity):
|
||||
"""Representation of a Dexcom glucose trend sensor."""
|
||||
|
||||
def __init__(self, coordinator, username):
|
||||
def __init__(self, coordinator: DataUpdateCoordinator, username: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, username, "trend")
|
||||
self._attr_name = f"{DOMAIN}_{username}_glucose_trend"
|
||||
self._attr_unique_id = f"{username}-trend"
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
@@ -1,7 +1,9 @@
|
||||
"""Discovergy sensor entity."""
|
||||
from collections.abc import Callable
|
||||
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 (
|
||||
SensorDeviceClass,
|
||||
@@ -11,6 +13,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
@@ -19,7 +22,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import DiscovergyData, DiscovergyUpdateCoordinator
|
||||
@@ -32,6 +34,9 @@ PARALLEL_UPDATES = 1
|
||||
class DiscovergyMixin:
|
||||
"""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: [])
|
||||
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(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
@@ -160,18 +176,22 @@ async def async_setup_entry(
|
||||
elif meter.measurement_type == "GAS":
|
||||
sensors = GAS_SENSORS
|
||||
|
||||
coordinator: DiscovergyUpdateCoordinator = data.coordinators[meter.meter_id]
|
||||
|
||||
if sensors is not None:
|
||||
for description in sensors:
|
||||
# check if this meter has this data, then add this sensor
|
||||
for key in {description.key, *description.alternative_keys}:
|
||||
coordinator: DiscovergyUpdateCoordinator = data.coordinators[
|
||||
meter.meter_id
|
||||
]
|
||||
if key in coordinator.data.values:
|
||||
entities.append(
|
||||
DiscovergySensor(key, description, meter, coordinator)
|
||||
)
|
||||
|
||||
for description in ADDITIONAL_SENSORS:
|
||||
entities.append(
|
||||
DiscovergySensor(description.key, description, meter, coordinator)
|
||||
)
|
||||
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
@@ -204,8 +224,8 @@ class DiscovergySensor(CoordinatorEntity[DiscovergyUpdateCoordinator], SensorEnt
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> datetime | float | None:
|
||||
"""Return the sensor state."""
|
||||
return float(
|
||||
self.coordinator.data.values[self.data_key] / self.entity_description.scale
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data, self.data_key, self.entity_description.scale
|
||||
)
|
||||
|
@@ -60,6 +60,9 @@
|
||||
},
|
||||
"phase_3_power": {
|
||||
"name": "Phase 3 power"
|
||||
},
|
||||
"last_transmitted": {
|
||||
"name": "Last transmitted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,35 +14,24 @@ from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_TOKEN,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import (
|
||||
CONF_EVENTS,
|
||||
DOMAIN,
|
||||
DOOR_STATION,
|
||||
DOOR_STATION_EVENT_ENTITY_IDS,
|
||||
DOOR_STATION_INFO,
|
||||
PLATFORMS,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from .util import get_doorstation_by_token
|
||||
from .const import API_URL, CONF_EVENTS, DOMAIN, PLATFORMS
|
||||
from .device import ConfiguredDoorBird
|
||||
from .models import DoorBirdData
|
||||
from .util import get_door_station_by_token
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_URL = f"/api/{DOMAIN}"
|
||||
|
||||
CONF_CUSTOM_URL = "hass_url_override"
|
||||
|
||||
RESET_DEVICE_FAVORITES = "doorbird_reset_favorites"
|
||||
@@ -66,26 +55,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the DoorBird component."""
|
||||
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)
|
||||
|
||||
def _reset_device_favorites_handler(event):
|
||||
def _reset_device_favorites_handler(event: Event) -> None:
|
||||
"""Handle clearing favorites on device."""
|
||||
if (token := event.data.get("token")) is None:
|
||||
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")
|
||||
return
|
||||
|
||||
# Clear webhooks
|
||||
favorites = doorstation.device.favorites()
|
||||
|
||||
for favorite_type in favorites:
|
||||
for favorite_id in favorites[favorite_type]:
|
||||
doorstation.device.delete_favorite(favorite_type, favorite_id)
|
||||
favorites: dict[str, list[str]] = door_station.device.favorites()
|
||||
for favorite_type, favorite_ids in favorites.items():
|
||||
for favorite_id in favorite_ids:
|
||||
door_station.device.delete_favorite(favorite_type, favorite_id)
|
||||
|
||||
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)
|
||||
|
||||
doorstation_config = entry.data
|
||||
doorstation_options = entry.options
|
||||
door_station_config = entry.data
|
||||
config_entry_id = entry.entry_id
|
||||
|
||||
device_ip = doorstation_config[CONF_HOST]
|
||||
username = doorstation_config[CONF_USERNAME]
|
||||
password = doorstation_config[CONF_PASSWORD]
|
||||
device_ip = door_station_config[CONF_HOST]
|
||||
username = door_station_config[CONF_USERNAME]
|
||||
password = door_station_config[CONF_PASSWORD]
|
||||
|
||||
device = DoorBird(device_ip, username, password)
|
||||
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:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
_LOGGER.error(
|
||||
@@ -128,50 +115,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
token = doorstation_config.get(CONF_TOKEN, config_entry_id)
|
||||
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
|
||||
name = doorstation_config.get(CONF_NAME)
|
||||
events = doorstation_options.get(CONF_EVENTS, [])
|
||||
doorstation = ConfiguredDoorBird(device, name, custom_url, token)
|
||||
doorstation.update_events(events)
|
||||
token: str = door_station_config.get(CONF_TOKEN, config_entry_id)
|
||||
custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL)
|
||||
name: str | None = door_station_config.get(CONF_NAME)
|
||||
events = entry.options.get(CONF_EVENTS, [])
|
||||
event_entity_ids: dict[str, str] = {}
|
||||
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
|
||||
if not await _async_register_events(hass, doorstation):
|
||||
if not await _async_register_events(hass, door_station):
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
undo_listener = entry.add_update_listener(_update_listener)
|
||||
|
||||
hass.data[DOMAIN][config_entry_id] = {
|
||||
DOOR_STATION: doorstation,
|
||||
DOOR_STATION_INFO: info,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_update_listener))
|
||||
hass.data[DOMAIN][config_entry_id] = door_bird_data
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
data: dict[str, DoorBirdData] = hass.data[DOMAIN]
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
data.pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def _async_register_events(
|
||||
hass: HomeAssistant, doorstation: ConfiguredDoorBird
|
||||
hass: HomeAssistant, door_station: ConfiguredDoorBird
|
||||
) -> bool:
|
||||
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:
|
||||
persistent_notification.async_create(
|
||||
hass,
|
||||
@@ -192,10 +172,11 @@ async def _async_register_events(
|
||||
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
config_entry_id = entry.entry_id
|
||||
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
|
||||
doorstation.update_events(entry.options[CONF_EVENTS])
|
||||
data: DoorBirdData = hass.data[DOMAIN][config_entry_id]
|
||||
door_station = data.door_station
|
||||
door_station.update_events(entry.options[CONF_EVENTS])
|
||||
# Subscribe to doorbell or motion events
|
||||
await _async_register_events(hass, doorstation)
|
||||
await _async_register_events(hass, door_station)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
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):
|
||||
"""Provide a page for the device to call."""
|
||||
|
||||
@@ -335,21 +200,17 @@ class DoorBirdRequestView(HomeAssistantView):
|
||||
name = API_URL[1:].replace("/", ":")
|
||||
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."""
|
||||
hass = request.app["hass"]
|
||||
|
||||
token = request.query.get("token")
|
||||
|
||||
device = get_doorstation_by_token(hass, token)
|
||||
|
||||
if device is None:
|
||||
hass: HomeAssistant = request.app["hass"]
|
||||
token: str | None = request.query.get("token")
|
||||
if token is None or (device := get_door_station_by_token(hass, token)) is None:
|
||||
return web.Response(
|
||||
status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided."
|
||||
)
|
||||
|
||||
if device:
|
||||
event_data = device.get_event_data()
|
||||
event_data = device.get_event_data(event)
|
||||
else:
|
||||
event_data = {}
|
||||
|
||||
@@ -359,10 +220,6 @@ class DoorBirdRequestView(HomeAssistantView):
|
||||
message = f"HTTP Favorites cleared for {device.slug}"
|
||||
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)
|
||||
|
||||
return web.Response(text="OK")
|
||||
|
@@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 .models import DoorBirdData
|
||||
|
||||
IR_RELAY = "__ir_light__"
|
||||
|
||||
@@ -49,20 +50,14 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the DoorBird button platform."""
|
||||
config_entry_id = config_entry.entry_id
|
||||
|
||||
data = hass.data[DOMAIN][config_entry_id]
|
||||
doorstation = data[DOOR_STATION]
|
||||
doorstation_info = data[DOOR_STATION_INFO]
|
||||
|
||||
relays = doorstation_info["RELAYS"]
|
||||
door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id]
|
||||
relays = door_bird_data.door_station_info["RELAYS"]
|
||||
|
||||
entities = [
|
||||
DoorBirdButton(doorstation, doorstation_info, relay, RELAY_ENTITY_DESCRIPTION)
|
||||
DoorBirdButton(door_bird_data, relay, RELAY_ENTITY_DESCRIPTION)
|
||||
for relay in relays
|
||||
]
|
||||
entities.append(
|
||||
DoorBirdButton(doorstation, doorstation_info, IR_RELAY, IR_ENTITY_DESCRIPTION)
|
||||
)
|
||||
entities.append(DoorBirdButton(door_bird_data, IR_RELAY, IR_ENTITY_DESCRIPTION))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -74,16 +69,14 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
doorstation: DoorBird,
|
||||
doorstation_info,
|
||||
door_bird_data: DoorBirdData,
|
||||
relay: str,
|
||||
entity_description: DoorbirdButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a relay in a DoorBird device."""
|
||||
super().__init__(doorstation, doorstation_info)
|
||||
super().__init__(door_bird_data)
|
||||
self._relay = relay
|
||||
self.entity_description = entity_description
|
||||
|
||||
if self._relay == IR_RELAY:
|
||||
self._attr_name = "IR"
|
||||
else:
|
||||
@@ -92,4 +85,4 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity):
|
||||
|
||||
def press(self) -> None:
|
||||
"""Power the relay."""
|
||||
self.entity_description.press_action(self._doorstation.device, self._relay)
|
||||
self.entity_description.press_action(self._door_station.device, self._relay)
|
||||
|
@@ -6,7 +6,6 @@ import datetime
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
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
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DOOR_STATION,
|
||||
DOOR_STATION_EVENT_ENTITY_IDS,
|
||||
DOOR_STATION_INFO,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .entity import DoorBirdEntity
|
||||
from .models import DoorBirdData
|
||||
|
||||
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2)
|
||||
_LAST_MOTION_INTERVAL = datetime.timedelta(seconds=30)
|
||||
@@ -37,39 +32,31 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the DoorBird camera platform."""
|
||||
config_entry_id = config_entry.entry_id
|
||||
config_data = hass.data[DOMAIN][config_entry_id]
|
||||
doorstation = config_data[DOOR_STATION]
|
||||
doorstation_info = config_data[DOOR_STATION_INFO]
|
||||
device = doorstation.device
|
||||
door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id]
|
||||
device = door_bird_data.door_station.device
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
DoorBirdCamera(
|
||||
doorstation,
|
||||
doorstation_info,
|
||||
door_bird_data,
|
||||
device.live_image_url,
|
||||
"live",
|
||||
"live",
|
||||
doorstation.doorstation_events,
|
||||
_LIVE_INTERVAL,
|
||||
device.rtsp_live_video_url,
|
||||
),
|
||||
DoorBirdCamera(
|
||||
doorstation,
|
||||
doorstation_info,
|
||||
door_bird_data,
|
||||
device.history_image_url(1, "doorbell"),
|
||||
"last_ring",
|
||||
"last_ring",
|
||||
[],
|
||||
_LAST_VISITOR_INTERVAL,
|
||||
),
|
||||
DoorBirdCamera(
|
||||
doorstation,
|
||||
doorstation_info,
|
||||
door_bird_data,
|
||||
device.history_image_url(1, "motionsensor"),
|
||||
"last_motion",
|
||||
"last_motion",
|
||||
[],
|
||||
_LAST_MOTION_INTERVAL,
|
||||
),
|
||||
]
|
||||
@@ -81,17 +68,15 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
doorstation,
|
||||
doorstation_info,
|
||||
url,
|
||||
camera_id,
|
||||
translation_key,
|
||||
doorstation_events,
|
||||
interval,
|
||||
stream_url=None,
|
||||
door_bird_data: DoorBirdData,
|
||||
url: str,
|
||||
camera_id: str,
|
||||
translation_key: str,
|
||||
interval: datetime.timedelta,
|
||||
stream_url: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the camera on a DoorBird device."""
|
||||
super().__init__(doorstation, doorstation_info)
|
||||
super().__init__(door_bird_data)
|
||||
self._url = url
|
||||
self._stream_url = stream_url
|
||||
self._attr_translation_key = translation_key
|
||||
@@ -101,7 +86,6 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
self._interval = interval
|
||||
self._last_update = datetime.datetime.min
|
||||
self._attr_unique_id = f"{self._mac_addr}_{camera_id}"
|
||||
self._doorstation_events = doorstation_events
|
||||
|
||||
async def stream_source(self):
|
||||
"""Return the stream source."""
|
||||
@@ -118,7 +102,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(_TIMEOUT):
|
||||
async with asyncio.timeout(_TIMEOUT):
|
||||
response = await websession.get(self._url)
|
||||
|
||||
self._last_image = await response.read()
|
||||
@@ -134,19 +118,15 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
return self._last_image
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add callback after being added to hass.
|
||||
|
||||
Registers entity_id map for the logbook
|
||||
"""
|
||||
event_to_entity_id = self.hass.data[DOMAIN].setdefault(
|
||||
DOOR_STATION_EVENT_ENTITY_IDS, {}
|
||||
)
|
||||
for event in self._doorstation_events:
|
||||
"""Subscribe to events."""
|
||||
await super().async_added_to_hass()
|
||||
event_to_entity_id = self._door_bird_data.event_entity_ids
|
||||
for event in self._door_station.events:
|
||||
event_to_entity_id[event] = self.entity_id
|
||||
|
||||
async def will_remove_from_hass(self):
|
||||
"""Unregister entity_id map for the logbook."""
|
||||
event_to_entity_id = self.hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS]
|
||||
for event in self._doorstation_events:
|
||||
if event in event_to_entity_id:
|
||||
del event_to_entity_id[event]
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from events."""
|
||||
event_to_entity_id = self._door_bird_data.event_entity_ids
|
||||
for event in self._door_station.events:
|
||||
del event_to_entity_id[event]
|
||||
await super().async_will_remove_from_hass()
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
from http import HTTPStatus
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
@@ -12,12 +13,12 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.components import zeroconf
|
||||
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.util.network import is_ipv4_address, is_link_local
|
||||
|
||||
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__)
|
||||
|
||||
@@ -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."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
@@ -53,13 +54,13 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
if not status[0]:
|
||||
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 {"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."""
|
||||
device = DoorBird(host, "", "")
|
||||
try:
|
||||
|
@@ -19,3 +19,5 @@ DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR"
|
||||
DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR"
|
||||
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
API_URL = f"/api/{DOMAIN}"
|
||||
|
147
homeassistant/components/doorbird/device.py
Normal file
147
homeassistant/components/doorbird/device.py
Normal 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),
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
"""The DoorBird integration base entity."""
|
||||
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -10,7 +11,8 @@ from .const import (
|
||||
DOORBIRD_INFO_KEY_FIRMWARE,
|
||||
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):
|
||||
@@ -18,19 +20,20 @@ class DoorBirdEntity(Entity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, doorstation, doorstation_info):
|
||||
def __init__(self, door_bird_data: DoorBirdData) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__()
|
||||
self._doorstation = doorstation
|
||||
self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info)
|
||||
|
||||
firmware = doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE]
|
||||
firmware_build = doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER]
|
||||
self._door_bird_data = door_bird_data
|
||||
self._door_station = door_bird_data.door_station
|
||||
door_station_info = door_bird_data.door_station_info
|
||||
self._mac_addr = get_mac_address_from_door_station_info(door_station_info)
|
||||
firmware = door_station_info[DOORBIRD_INFO_KEY_FIRMWARE]
|
||||
firmware_build = door_station_info[DOORBIRD_INFO_KEY_BUILD_NUMBER]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url="https://webadmin.doorbird.com/",
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE],
|
||||
name=self._doorstation.name,
|
||||
model=door_station_info[DOORBIRD_INFO_KEY_DEVICE_TYPE],
|
||||
name=self._door_station.name,
|
||||
sw_version=f"{firmware} {firmware_build}",
|
||||
)
|
||||
|
@@ -1,43 +1,35 @@
|
||||
"""Describe logbook events."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.logbook import (
|
||||
LOGBOOK_ENTRY_ENTITY_ID,
|
||||
LOGBOOK_ENTRY_MESSAGE,
|
||||
LOGBOOK_ENTRY_NAME,
|
||||
)
|
||||
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
|
||||
def async_describe_events(hass, async_describe_event):
|
||||
def async_describe_events(hass: HomeAssistant, async_describe_event):
|
||||
"""Describe logbook events."""
|
||||
|
||||
@callback
|
||||
def async_describe_logbook_event(event):
|
||||
def async_describe_logbook_event(event: Event):
|
||||
"""Describe a logbook event."""
|
||||
doorbird_event = event.event_type.split("_", 1)[1]
|
||||
|
||||
return {
|
||||
LOGBOOK_ENTRY_NAME: "Doorbird",
|
||||
LOGBOOK_ENTRY_MESSAGE: f"Event {event.event_type} was fired",
|
||||
LOGBOOK_ENTRY_ENTITY_ID: hass.data[DOMAIN][
|
||||
DOOR_STATION_EVENT_ENTITY_IDS
|
||||
].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)),
|
||||
# Database entries before Jun 25th 2020 will not have an entity ID
|
||||
LOGBOOK_ENTRY_ENTITY_ID: 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():
|
||||
if DOOR_STATION not in data:
|
||||
# We need to skip door_station_event_entity_ids
|
||||
continue
|
||||
for event in data[DOOR_STATION].doorstation_events:
|
||||
for event in data.door_station.door_station_events:
|
||||
async_describe_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
Reference in New Issue
Block a user