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