mirror of
https://github.com/home-assistant/core.git
synced 2025-08-09 23:55:07 +02:00
Merge branch 'dev' into feature/starlink-device-tracker
This commit is contained in:
@@ -86,6 +86,7 @@ components: &components
|
||||
- homeassistant/components/lovelace/**
|
||||
- homeassistant/components/media_source/**
|
||||
- homeassistant/components/mjpeg/**
|
||||
- homeassistant/components/modbus/**
|
||||
- homeassistant/components/mqtt/**
|
||||
- homeassistant/components/network/**
|
||||
- homeassistant/components/onboarding/**
|
||||
|
@@ -302,8 +302,11 @@ omit =
|
||||
homeassistant/components/enocean/sensor.py
|
||||
homeassistant/components/enocean/switch.py
|
||||
homeassistant/components/enphase_envoy/__init__.py
|
||||
homeassistant/components/enphase_envoy/binary_sensor.py
|
||||
homeassistant/components/enphase_envoy/coordinator.py
|
||||
homeassistant/components/enphase_envoy/entity.py
|
||||
homeassistant/components/enphase_envoy/sensor.py
|
||||
homeassistant/components/enphase_envoy/switch.py
|
||||
homeassistant/components/entur_public_transport/*
|
||||
homeassistant/components/environment_canada/__init__.py
|
||||
homeassistant/components/environment_canada/camera.py
|
||||
@@ -419,6 +422,7 @@ omit =
|
||||
homeassistant/components/garadget/cover.py
|
||||
homeassistant/components/garages_amsterdam/__init__.py
|
||||
homeassistant/components/garages_amsterdam/binary_sensor.py
|
||||
homeassistant/components/garages_amsterdam/entity.py
|
||||
homeassistant/components/garages_amsterdam/sensor.py
|
||||
homeassistant/components/gc100/*
|
||||
homeassistant/components/geniushub/*
|
||||
@@ -772,6 +776,7 @@ omit =
|
||||
homeassistant/components/neato/sensor.py
|
||||
homeassistant/components/neato/switch.py
|
||||
homeassistant/components/neato/vacuum.py
|
||||
homeassistant/components/neato/button.py
|
||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||
homeassistant/components/netdata/sensor.py
|
||||
homeassistant/components/netgear/__init__.py
|
||||
@@ -1342,6 +1347,7 @@ omit =
|
||||
homeassistant/components/trafikverket_train/__init__.py
|
||||
homeassistant/components/trafikverket_train/coordinator.py
|
||||
homeassistant/components/trafikverket_train/sensor.py
|
||||
homeassistant/components/trafikverket_train/util.py
|
||||
homeassistant/components/trafikverket_weatherstation/__init__.py
|
||||
homeassistant/components/trafikverket_weatherstation/coordinator.py
|
||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||
|
5
.github/workflows/builder.yml
vendored
5
.github/workflows/builder.yml
vendored
@@ -197,7 +197,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.06.1
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -251,6 +251,7 @@ jobs:
|
||||
- raspberrypi4-64
|
||||
- tinker
|
||||
- yellow
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
@@ -274,7 +275,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.06.1
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
|
@@ -53,6 +53,7 @@ homeassistant.components.airzone_cloud.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.ambient_station.*
|
||||
homeassistant.components.amcrest.*
|
||||
|
@@ -343,8 +343,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/enigma2/ @fbradyirl
|
||||
/homeassistant/components/enocean/ @bdurrer
|
||||
/tests/components/enocean/ @bdurrer
|
||||
/homeassistant/components/enphase_envoy/ @gtdiehl
|
||||
/tests/components/enphase_envoy/ @gtdiehl
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
|
||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
|
||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
@@ -1373,6 +1373,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vulcan/ @Antoni-Czaplicki
|
||||
/homeassistant/components/wake_on_lan/ @ntilley905
|
||||
/tests/components/wake_on_lan/ @ntilley905
|
||||
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
|
||||
/tests/components/wake_word/ @home-assistant/core @synesthesiam
|
||||
/homeassistant/components/wallbox/ @hesselonline
|
||||
/tests/components/wallbox/ @hesselonline
|
||||
/homeassistant/components/waqi/ @andrey-git
|
||||
|
@@ -28,6 +28,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
|
||||
@@ -320,9 +321,9 @@ class AbodeDevice(AbodeEntity):
|
||||
}
|
||||
|
||||
@property
|
||||
def device_info(self) -> entity.DeviceInfo:
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return entity.DeviceInfo(
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device.id)},
|
||||
manufacturer="Abode",
|
||||
model=self._device.type,
|
||||
|
@@ -16,8 +16,7 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER
|
||||
|
@@ -68,9 +68,6 @@ class AccuWeatherEntity(
|
||||
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
# Coordinator data is used also for sensors which don't have units automatically
|
||||
# converted, hence the weather entity's native units follow the configured unit
|
||||
# system
|
||||
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||
self._attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
@@ -74,9 +74,9 @@ class AcmedaBase(entity.Entity):
|
||||
return self.roller.id
|
||||
|
||||
@property
|
||||
def device_info(self) -> entity.DeviceInfo:
|
||||
def device_info(self) -> dr.DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return entity.DeviceInfo(
|
||||
return dr.DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
manufacturer="Rollease Acmeda",
|
||||
name=self.roller.name,
|
||||
|
@@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
|
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
from adguardhome import AdGuardHome, AdGuardHomeError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER
|
||||
|
||||
|
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from advantage_air import ApiError
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
|
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN
|
||||
|
@@ -8,7 +8,7 @@ from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
|
@@ -20,8 +20,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
@@ -17,8 +17,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
@@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
|
||||
|
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
|
@@ -34,8 +34,12 @@ class Discovery:
|
||||
|
||||
|
||||
def get_name(device: AirthingsDevice) -> str:
|
||||
"""Generate name with identifier for device."""
|
||||
return f"{device.name} ({device.identifier})"
|
||||
"""Generate name with model and identifier for device."""
|
||||
|
||||
name = device.friendly_name()
|
||||
if identifier := device.identifier:
|
||||
name += f" ({identifier})"
|
||||
return name
|
||||
|
||||
|
||||
class AirthingsDeviceUpdateError(Exception):
|
||||
@@ -156,7 +160,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
titles = {
|
||||
address: get_name(discovery.device)
|
||||
address: discovery.device.name
|
||||
for (address, discovery) in self._discovered_devices.items()
|
||||
}
|
||||
return self.async_show_form(
|
||||
|
@@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==0.5.3"]
|
||||
"requirements": ["airthings-ble==0.5.6-2"]
|
||||
}
|
||||
|
@@ -22,8 +22,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
@@ -162,10 +161,11 @@ class AirthingsSensor(
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
|
||||
name = f"{airthings_device.name} {airthings_device.identifier}"
|
||||
name = airthings_device.name
|
||||
if identifier := airthings_device.identifier:
|
||||
name += f" ({identifier})"
|
||||
|
||||
self._attr_unique_id = f"{name}_{entity_description.key}"
|
||||
|
||||
self._id = airthings_device.address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={
|
||||
@@ -175,9 +175,10 @@ class AirthingsSensor(
|
||||
)
|
||||
},
|
||||
name=name,
|
||||
manufacturer="Airthings",
|
||||
manufacturer=airthings_device.manufacturer,
|
||||
hw_version=airthings_device.hw_version,
|
||||
sw_version=airthings_device.sw_version,
|
||||
model=airthings_device.model,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"],
|
||||
"requirements": ["pyairvisual==2022.12.1"]
|
||||
"requirements": ["pyairvisual==2023.08.1"]
|
||||
}
|
||||
|
@@ -23,7 +23,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"],
|
||||
"requirements": ["pyairvisual==2022.12.1"]
|
||||
"requirements": ["pyairvisual==2023.08.1"]
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ from aioairzone.exceptions import AirzoneError
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
|
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.4"]
|
||||
"requirements": ["aioairzone==0.6.5"]
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ from aioairzone_cloud.const import (
|
||||
)
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.56"]
|
||||
"requirements": ["AIOAladdinConnect==0.1.57"]
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -1,15 +1,16 @@
|
||||
"""Support for Alexa skill auth."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -30,24 +31,24 @@ STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||
class Auth:
|
||||
"""Handle authentication to send events to Alexa."""
|
||||
|
||||
def __init__(self, hass, client_id, client_secret):
|
||||
def __init__(self, hass: HomeAssistant, client_id: str, client_secret: str) -> None:
|
||||
"""Initialize the Auth class."""
|
||||
self.hass = hass
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
self._prefs = None
|
||||
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
self._prefs: dict[str, Any] | None = None
|
||||
self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
self._get_token_lock = asyncio.Lock()
|
||||
|
||||
async def async_do_auth(self, accept_grant_code):
|
||||
async def async_do_auth(self, accept_grant_code: str) -> str | None:
|
||||
"""Do authentication with an AcceptGrant code."""
|
||||
# access token not retrieved yet for the first time, so this should
|
||||
# be an access token request
|
||||
|
||||
lwa_params = {
|
||||
lwa_params: dict[str, str] = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": accept_grant_code,
|
||||
CONF_CLIENT_ID: self.client_id,
|
||||
@@ -61,25 +62,28 @@ class Auth:
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
@callback
|
||||
def async_invalidate_access_token(self):
|
||||
def async_invalidate_access_token(self) -> None:
|
||||
"""Invalidate access token."""
|
||||
assert self._prefs is not None
|
||||
self._prefs[STORAGE_ACCESS_TOKEN] = None
|
||||
|
||||
async def async_get_access_token(self):
|
||||
async def async_get_access_token(self) -> str | None:
|
||||
"""Perform access token or token refresh request."""
|
||||
async with self._get_token_lock:
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
|
||||
assert self._prefs is not None
|
||||
if self.is_token_valid():
|
||||
_LOGGER.debug("Token still valid, using it")
|
||||
return self._prefs[STORAGE_ACCESS_TOKEN]
|
||||
token: str = self._prefs[STORAGE_ACCESS_TOKEN]
|
||||
return token
|
||||
|
||||
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
|
||||
_LOGGER.debug("Token invalid and no refresh token available")
|
||||
return None
|
||||
|
||||
lwa_params = {
|
||||
lwa_params: dict[str, str] = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
|
||||
CONF_CLIENT_ID: self.client_id,
|
||||
@@ -90,19 +94,23 @@ class Auth:
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
@callback
|
||||
def is_token_valid(self):
|
||||
def is_token_valid(self) -> bool:
|
||||
"""Check if a token is already loaded and if it is still valid."""
|
||||
assert self._prefs is not None
|
||||
if not self._prefs[STORAGE_ACCESS_TOKEN]:
|
||||
return False
|
||||
|
||||
expire_time = dt_util.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
|
||||
expire_time: datetime | None = dt_util.parse_datetime(
|
||||
self._prefs[STORAGE_EXPIRE_TIME]
|
||||
)
|
||||
assert expire_time is not None
|
||||
preemptive_expire_time = expire_time - timedelta(
|
||||
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
|
||||
)
|
||||
|
||||
return dt_util.utcnow() < preemptive_expire_time
|
||||
|
||||
async def _async_request_new_token(self, lwa_params):
|
||||
async def _async_request_new_token(self, lwa_params: dict[str, str]) -> str | None:
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(10):
|
||||
@@ -127,9 +135,9 @@ class Auth:
|
||||
response_json = await response.json()
|
||||
_LOGGER.debug("LWA response body : %s", response_json)
|
||||
|
||||
access_token = response_json["access_token"]
|
||||
refresh_token = response_json["refresh_token"]
|
||||
expires_in = response_json["expires_in"]
|
||||
access_token: str = response_json["access_token"]
|
||||
refresh_token: str = response_json["refresh_token"]
|
||||
expires_in: int = response_json["expires_in"]
|
||||
expire_time = dt_util.utcnow() + timedelta(seconds=expires_in)
|
||||
|
||||
await self._async_update_preferences(
|
||||
@@ -138,7 +146,7 @@ class Auth:
|
||||
|
||||
return access_token
|
||||
|
||||
async def async_load_preferences(self):
|
||||
async def async_load_preferences(self) -> None:
|
||||
"""Load preferences with stored tokens."""
|
||||
self._prefs = await self._store.async_load()
|
||||
|
||||
@@ -149,10 +157,13 @@ class Auth:
|
||||
STORAGE_EXPIRE_TIME: None,
|
||||
}
|
||||
|
||||
async def _async_update_preferences(self, access_token, refresh_token, expire_time):
|
||||
async def _async_update_preferences(
|
||||
self, access_token: str, refresh_token: str, expire_time: str
|
||||
) -> None:
|
||||
"""Update user preferences."""
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
assert self._prefs is not None
|
||||
|
||||
if access_token is not None:
|
||||
self._prefs[STORAGE_ACCESS_TOKEN] = access_token
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,9 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -33,38 +36,38 @@ class AbstractConfig(ABC):
|
||||
await self._store.async_load()
|
||||
|
||||
@property
|
||||
def supports_auth(self):
|
||||
def supports_auth(self) -> bool:
|
||||
"""Return if config supports auth."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
def should_report_state(self) -> bool:
|
||||
"""Return if states should be proactively reported."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
@abstractmethod
|
||||
def endpoint(self) -> str | URL | None:
|
||||
"""Endpoint for report state."""
|
||||
return None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def locale(self):
|
||||
def locale(self) -> str | None:
|
||||
"""Return config locale."""
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
def entity_config(self) -> dict[str, Any]:
|
||||
"""Return entity config."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def is_reporting_states(self):
|
||||
def is_reporting_states(self) -> bool:
|
||||
"""Return if proactive mode is enabled."""
|
||||
return self._unsub_proactive_report is not None
|
||||
|
||||
@callback
|
||||
@abstractmethod
|
||||
def user_identifier(self):
|
||||
def user_identifier(self) -> str:
|
||||
"""Return an identifier for the user that represents this config."""
|
||||
|
||||
async def async_enable_proactive_mode(self) -> None:
|
||||
@@ -85,29 +88,29 @@ class AbstractConfig(ABC):
|
||||
self._unsub_proactive_report = None
|
||||
|
||||
@callback
|
||||
def should_expose(self, entity_id):
|
||||
def should_expose(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def async_invalidate_access_token(self):
|
||||
def async_invalidate_access_token(self) -> None:
|
||||
"""Invalidate access token."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_access_token(self):
|
||||
async def async_get_access_token(self) -> str | None:
|
||||
"""Get an access token."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_accept_grant(self, code):
|
||||
async def async_accept_grant(self, code: str) -> str | None:
|
||||
"""Accept a grant."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def authorized(self):
|
||||
def authorized(self) -> bool:
|
||||
"""Return authorization status."""
|
||||
return self._store.authorized
|
||||
|
||||
async def set_authorized(self, authorized) -> None:
|
||||
async def set_authorized(self, authorized: bool) -> None:
|
||||
"""Set authorization status.
|
||||
|
||||
- Set when an incoming message is received from Alexa.
|
||||
@@ -132,25 +135,26 @@ class AlexaConfigStore:
|
||||
_STORAGE_VERSION = 1
|
||||
_STORAGE_KEY = DOMAIN
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize a configuration store."""
|
||||
self._data = None
|
||||
self._data: dict[str, Any] | None = None
|
||||
self._hass = hass
|
||||
self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY)
|
||||
self._store: Store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY)
|
||||
|
||||
@property
|
||||
def authorized(self):
|
||||
def authorized(self) -> bool:
|
||||
"""Return authorization status."""
|
||||
return self._data[STORE_AUTHORIZED]
|
||||
assert self._data is not None
|
||||
return bool(self._data[STORE_AUTHORIZED])
|
||||
|
||||
@callback
|
||||
def set_authorized(self, authorized):
|
||||
def set_authorized(self, authorized: bool) -> None:
|
||||
"""Set authorization status."""
|
||||
if authorized != self._data[STORE_AUTHORIZED]:
|
||||
if self._data is not None and authorized != self._data[STORE_AUTHORIZED]:
|
||||
self._data[STORE_AUTHORIZED] = authorized
|
||||
self._store.async_delay_save(lambda: self._data, 1.0)
|
||||
|
||||
async def async_load(self):
|
||||
async def async_load(self) -> None:
|
||||
"""Load saved configuration from disk."""
|
||||
if data := await self._store.async_load():
|
||||
self._data = data
|
||||
|
@@ -69,7 +69,7 @@ API_TEMP_UNITS = {
|
||||
# Needs to be ordered dict for `async_api_set_thermostat_mode` which does a
|
||||
# reverse mapping of this dict and we want to map the first occurrence of OFF
|
||||
# back to HA state.
|
||||
API_THERMOSTAT_MODES = OrderedDict(
|
||||
API_THERMOSTAT_MODES: OrderedDict[str, str] = OrderedDict(
|
||||
[
|
||||
(climate.HVACMode.HEAT, "HEAT"),
|
||||
(climate.HVACMode.COOL, "COOL"),
|
||||
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator, Iterable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
@@ -274,22 +274,23 @@ class AlexaEntity:
|
||||
self.entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
def entity_id(self) -> str:
|
||||
"""Return the Entity ID."""
|
||||
return self.entity.entity_id
|
||||
|
||||
def friendly_name(self):
|
||||
def friendly_name(self) -> str:
|
||||
"""Return the Alexa API friendly name."""
|
||||
return self.entity_conf.get(CONF_NAME, self.entity.name).translate(
|
||||
TRANSLATION_TABLE
|
||||
)
|
||||
friendly_name: str = self.entity_conf.get(
|
||||
CONF_NAME, self.entity.name
|
||||
).translate(TRANSLATION_TABLE)
|
||||
return friendly_name
|
||||
|
||||
def description(self):
|
||||
def description(self) -> str:
|
||||
"""Return the Alexa API description."""
|
||||
description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id
|
||||
return f"{description} via Home Assistant".translate(TRANSLATION_TABLE)
|
||||
|
||||
def alexa_id(self):
|
||||
def alexa_id(self) -> str:
|
||||
"""Return the Alexa API entity id."""
|
||||
return generate_alexa_id(self.entity.entity_id)
|
||||
|
||||
@@ -317,7 +318,7 @@ class AlexaEntity:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize_properties(self):
|
||||
def serialize_properties(self) -> Generator[dict[str, Any], None, None]:
|
||||
"""Yield each supported property in API format."""
|
||||
for interface in self.interfaces():
|
||||
if not interface.properties_proactively_reported():
|
||||
@@ -325,9 +326,9 @@ class AlexaEntity:
|
||||
|
||||
yield from interface.serialize_properties()
|
||||
|
||||
def serialize_discovery(self):
|
||||
def serialize_discovery(self) -> dict[str, Any]:
|
||||
"""Serialize the entity for discovery."""
|
||||
result = {
|
||||
result: dict[str, Any] = {
|
||||
"displayCategories": self.display_categories(),
|
||||
"cookie": {},
|
||||
"endpointId": self.alexa_id(),
|
||||
@@ -366,7 +367,7 @@ def async_get_entities(
|
||||
hass: HomeAssistant, config: AbstractConfig
|
||||
) -> list[AlexaEntity]:
|
||||
"""Return all entities that are supported by Alexa."""
|
||||
entities = []
|
||||
entities: list[AlexaEntity] = []
|
||||
for state in hass.states.async_all():
|
||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
continue
|
||||
@@ -725,7 +726,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
class SceneCapabilities(AlexaEntity):
|
||||
"""Class to represent Scene capabilities."""
|
||||
|
||||
def description(self):
|
||||
def description(self) -> str:
|
||||
"""Return the Alexa API description."""
|
||||
description = AlexaEntity.description(self)
|
||||
if "scene" not in description.casefold():
|
||||
|
@@ -1,8 +1,9 @@
|
||||
"""Alexa related errors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import API_TEMP_UNITS
|
||||
@@ -29,7 +30,9 @@ class AlexaError(Exception):
|
||||
namespace: str | None = None
|
||||
error_type: str | None = None
|
||||
|
||||
def __init__(self, error_message, payload=None):
|
||||
def __init__(
|
||||
self, error_message: str, payload: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Initialize an alexa error."""
|
||||
Exception.__init__(self)
|
||||
self.error_message = error_message
|
||||
@@ -42,7 +45,7 @@ class AlexaInvalidEndpointError(AlexaError):
|
||||
namespace = "Alexa"
|
||||
error_type = "NO_SUCH_ENDPOINT"
|
||||
|
||||
def __init__(self, endpoint_id):
|
||||
def __init__(self, endpoint_id: str) -> None:
|
||||
"""Initialize invalid endpoint error."""
|
||||
msg = f"The endpoint {endpoint_id} does not exist"
|
||||
AlexaError.__init__(self, msg)
|
||||
@@ -93,7 +96,9 @@ class AlexaTempRangeError(AlexaError):
|
||||
namespace = "Alexa"
|
||||
error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE"
|
||||
|
||||
def __init__(self, hass, temp, min_temp, max_temp):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, temp: float, min_temp: float, max_temp: float
|
||||
) -> None:
|
||||
"""Initialize TempRange error."""
|
||||
unit = hass.config.units.temperature_unit
|
||||
temp_range = {
|
||||
|
@@ -4,10 +4,13 @@ from http import HTTPStatus
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from aiohttp.web_response import StreamResponse
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
@@ -32,7 +35,7 @@ FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass, flash_briefing_config):
|
||||
def async_setup(hass: HomeAssistant, flash_briefing_config: ConfigType) -> None:
|
||||
"""Activate Alexa component."""
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
|
||||
|
||||
@@ -44,14 +47,16 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
requires_auth = False
|
||||
name = "api:alexa:flash_briefings"
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
def __init__(self, hass: HomeAssistant, flash_briefings: ConfigType) -> None:
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
self.flash_briefings = flash_briefings
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
def get(
|
||||
self, request: http.HomeAssistantRequest, briefing_id: str
|
||||
) -> StreamResponse | tuple[bytes, HTTPStatus]:
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
|
||||
|
||||
|
@@ -123,7 +123,7 @@ async def async_api_accept_grant(
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
auth_code = directive.payload["grant"]["code"]
|
||||
auth_code: str = directive.payload["grant"]["code"]
|
||||
_LOGGER.debug("AcceptGrant code: %s", auth_code)
|
||||
|
||||
if config.supports_auth:
|
||||
@@ -339,8 +339,8 @@ async def async_api_decrease_color_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process a decrease color temperature request."""
|
||||
entity = directive.entity
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
||||
current = int(entity.attributes[light.ATTR_COLOR_TEMP])
|
||||
max_mireds = int(entity.attributes[light.ATTR_MAX_MIREDS])
|
||||
|
||||
value = min(max_mireds, current + 50)
|
||||
await hass.services.async_call(
|
||||
@@ -363,8 +363,8 @@ async def async_api_increase_color_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process an increase color temperature request."""
|
||||
entity = directive.entity
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
current = int(entity.attributes[light.ATTR_COLOR_TEMP])
|
||||
min_mireds = int(entity.attributes[light.ATTR_MIN_MIREDS])
|
||||
|
||||
value = max(min_mireds, current - 50)
|
||||
await hass.services.async_call(
|
||||
@@ -403,7 +403,7 @@ async def async_api_activate(
|
||||
context=context,
|
||||
)
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"cause": {"type": Cause.VOICE_INTERACTION},
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
}
|
||||
@@ -432,7 +432,7 @@ async def async_api_deactivate(
|
||||
context=context,
|
||||
)
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"cause": {"type": Cause.VOICE_INTERACTION},
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
}
|
||||
@@ -509,7 +509,7 @@ async def async_api_set_volume(
|
||||
volume = round(float(directive.payload["volume"] / 100), 2)
|
||||
entity = directive.entity
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
@@ -554,7 +554,7 @@ async def async_api_select_input(
|
||||
)
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
@@ -581,7 +581,7 @@ async def async_api_adjust_volume(
|
||||
volume_delta = int(directive.payload["volume"])
|
||||
|
||||
entity = directive.entity
|
||||
current_level = entity.attributes.get(media_player.const.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
|
||||
# read current state
|
||||
try:
|
||||
@@ -591,7 +591,7 @@ async def async_api_adjust_volume(
|
||||
|
||||
volume = float(max(0, volume_delta + current) / 100)
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
@@ -631,7 +631,7 @@ async def async_api_adjust_volume_step(
|
||||
if is_default:
|
||||
volume_int = default_steps
|
||||
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
for _ in range(abs(volume_int)):
|
||||
await hass.services.async_call(
|
||||
@@ -652,7 +652,7 @@ async def async_api_set_mute(
|
||||
"""Process a set mute request."""
|
||||
mute = bool(directive.payload["mute"])
|
||||
entity = directive.entity
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
@@ -673,7 +673,7 @@ async def async_api_play(
|
||||
) -> AlexaResponse:
|
||||
"""Process a play request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context
|
||||
@@ -691,7 +691,7 @@ async def async_api_pause(
|
||||
) -> AlexaResponse:
|
||||
"""Process a pause request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context
|
||||
@@ -709,7 +709,7 @@ async def async_api_stop(
|
||||
) -> AlexaResponse:
|
||||
"""Process a stop request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
|
||||
@@ -727,7 +727,7 @@ async def async_api_next(
|
||||
) -> AlexaResponse:
|
||||
"""Process a next request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context
|
||||
@@ -745,7 +745,7 @@ async def async_api_previous(
|
||||
) -> AlexaResponse:
|
||||
"""Process a previous request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
@@ -758,7 +758,9 @@ async def async_api_previous(
|
||||
return directive.response()
|
||||
|
||||
|
||||
def temperature_from_object(hass, temp_obj, interval=False):
|
||||
def temperature_from_object(
|
||||
hass: ha.HomeAssistant, temp_obj: dict[str, Any], interval: bool = False
|
||||
) -> float:
|
||||
"""Get temperature from Temperature object in requested unit."""
|
||||
to_unit = hass.config.units.temperature_unit
|
||||
from_unit = UnitOfTemperature.CELSIUS
|
||||
@@ -784,11 +786,11 @@ async def async_api_set_target_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process a set target temperature request."""
|
||||
entity = directive.entity
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
|
||||
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
|
||||
unit = hass.config.units.temperature_unit
|
||||
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
payload = directive.payload
|
||||
response = directive.response()
|
||||
@@ -848,9 +850,10 @@ async def async_api_adjust_target_temp(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process an adjust target temperature request."""
|
||||
data: dict[str, Any]
|
||||
entity = directive.entity
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
|
||||
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
|
||||
unit = hass.config.units.temperature_unit
|
||||
|
||||
temp_delta = temperature_from_object(
|
||||
@@ -861,7 +864,7 @@ async def async_api_adjust_target_temp(
|
||||
|
||||
current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||
current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||
if current_target_temp_high and current_target_temp_low:
|
||||
if current_target_temp_high is not None and current_target_temp_low is not None:
|
||||
target_temp_high = float(current_target_temp_high) + temp_delta
|
||||
if target_temp_high < min_temp or target_temp_high > max_temp:
|
||||
raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp)
|
||||
@@ -891,7 +894,7 @@ async def async_api_adjust_target_temp(
|
||||
}
|
||||
)
|
||||
else:
|
||||
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||
target_temp = float(entity.attributes[ATTR_TEMPERATURE]) + temp_delta
|
||||
|
||||
if target_temp < min_temp or target_temp > max_temp:
|
||||
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
|
||||
@@ -924,11 +927,13 @@ async def async_api_set_thermostat_mode(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process a set thermostat mode request."""
|
||||
operation_list: list[str]
|
||||
|
||||
entity = directive.entity
|
||||
mode = directive.payload["thermostatMode"]
|
||||
mode = mode if isinstance(mode, str) else mode["value"]
|
||||
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None)
|
||||
|
||||
@@ -943,7 +948,7 @@ async def async_api_set_thermostat_mode(
|
||||
data[climate.ATTR_PRESET_MODE] = ha_preset
|
||||
|
||||
elif mode == "CUSTOM":
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||
custom_mode = directive.payload["thermostatMode"]["customName"]
|
||||
custom_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode),
|
||||
@@ -959,9 +964,13 @@ async def async_api_set_thermostat_mode(
|
||||
data[climate.ATTR_HVAC_MODE] = custom_mode
|
||||
|
||||
else:
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
|
||||
ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode}
|
||||
ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None)
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||
ha_modes: dict[str, str] = {
|
||||
k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode
|
||||
}
|
||||
ha_mode: str | None = next(
|
||||
iter(set(ha_modes).intersection(operation_list)), None
|
||||
)
|
||||
if ha_mode not in operation_list:
|
||||
msg = f"The requested thermostat mode {mode} is not supported"
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
@@ -1006,7 +1015,7 @@ async def async_api_arm(
|
||||
entity = directive.entity
|
||||
service = None
|
||||
arm_state = directive.payload["armState"]
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.state != STATE_ALARM_DISARMED:
|
||||
msg = "You must disarm the system before you can set the requested arm state."
|
||||
@@ -1026,7 +1035,7 @@ async def async_api_arm(
|
||||
)
|
||||
|
||||
# return 0 until alarm integration supports an exit delay
|
||||
payload = {"exitDelayInSeconds": 0}
|
||||
payload: dict[str, Any] = {"exitDelayInSeconds": 0}
|
||||
|
||||
response = directive.response(
|
||||
name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload
|
||||
@@ -1052,7 +1061,7 @@ async def async_api_disarm(
|
||||
) -> AlexaResponse:
|
||||
"""Process a Security Panel Disarm request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
response = directive.response()
|
||||
|
||||
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
||||
@@ -1094,7 +1103,7 @@ async def async_api_set_mode(
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
mode = directive.payload["mode"]
|
||||
|
||||
# Fan Direction
|
||||
@@ -1107,8 +1116,11 @@ async def async_api_set_mode(
|
||||
# Fan preset_mode
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
||||
preset_mode = mode.split(".")[1]
|
||||
if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get(
|
||||
fan.ATTR_PRESET_MODES
|
||||
preset_modes: list[str] | None = entity.attributes.get(fan.ATTR_PRESET_MODES)
|
||||
if (
|
||||
preset_mode != PRESET_MODE_NA
|
||||
and preset_modes
|
||||
and preset_mode in preset_modes
|
||||
):
|
||||
service = fan.SERVICE_SET_PRESET_MODE
|
||||
data[fan.ATTR_PRESET_MODE] = preset_mode
|
||||
@@ -1119,9 +1131,8 @@ async def async_api_set_mode(
|
||||
# Humidifier mode
|
||||
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
|
||||
mode = mode.split(".")[1]
|
||||
if mode != PRESET_MODE_NA and mode in entity.attributes.get(
|
||||
humidifier.ATTR_AVAILABLE_MODES
|
||||
):
|
||||
modes: list[str] | None = entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES)
|
||||
if mode != PRESET_MODE_NA and modes and mode in modes:
|
||||
service = humidifier.SERVICE_SET_MODE
|
||||
data[humidifier.ATTR_MODE] = mode
|
||||
else:
|
||||
@@ -1194,7 +1205,7 @@ async def async_api_toggle_on(
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
fan.ATTR_OSCILLATING: True,
|
||||
}
|
||||
@@ -1233,7 +1244,7 @@ async def async_api_toggle_off(
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
fan.ATTR_OSCILLATING: False,
|
||||
}
|
||||
@@ -1267,7 +1278,7 @@ async def async_api_set_range(
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = directive.payload["rangeValue"]
|
||||
|
||||
# Cover Position
|
||||
@@ -1536,7 +1547,7 @@ async def async_api_changechannel(
|
||||
channel = metadata_payload["name"]
|
||||
payload_name = "callSign"
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
|
||||
@@ -1576,7 +1587,7 @@ async def async_api_skipchannel(
|
||||
channel = int(directive.payload["channelCount"])
|
||||
entity = directive.entity
|
||||
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if channel < 0:
|
||||
service_media = SERVICE_MEDIA_PREVIOUS_TRACK
|
||||
@@ -1623,7 +1634,7 @@ async def async_api_seek(
|
||||
if media_duration and 0 < int(media_duration) < seek_position:
|
||||
seek_position = media_duration
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_SEEK_POSITION: seek_position,
|
||||
}
|
||||
@@ -1639,7 +1650,9 @@ async def async_api_seek(
|
||||
# convert seconds to milliseconds for StateReport.
|
||||
seek_position = int(seek_position * 1000)
|
||||
|
||||
payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]}
|
||||
payload: dict[str, Any] = {
|
||||
"properties": [{"name": "positionMilliseconds", "value": seek_position}]
|
||||
}
|
||||
return directive.response(
|
||||
name="StateReport", namespace="Alexa.SeekController", payload=payload
|
||||
)
|
||||
@@ -1655,7 +1668,7 @@ async def async_api_set_eq_mode(
|
||||
"""Process a SetMode request for EqualizerController."""
|
||||
mode = directive.payload["mode"]
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||
@@ -1701,7 +1714,7 @@ async def async_api_hold(
|
||||
) -> AlexaResponse:
|
||||
"""Process a TimeHoldController Hold request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == timer.DOMAIN:
|
||||
service = timer.SERVICE_PAUSE
|
||||
@@ -1728,7 +1741,7 @@ async def async_api_resume(
|
||||
) -> AlexaResponse:
|
||||
"""Process a TimeHoldController Resume request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == timer.DOMAIN:
|
||||
service = timer.SERVICE_START
|
||||
@@ -1773,7 +1786,7 @@ async def async_api_initialize_camera_stream(
|
||||
"Failed to find suitable URL to serve to Alexa"
|
||||
) from err
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"cameraStreams": [
|
||||
{
|
||||
"uri": f"{external_url}{stream_source}",
|
||||
|
@@ -3,8 +3,10 @@ import enum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.web import Response
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.util.decorator import Registry
|
||||
@@ -18,7 +20,7 @@ HANDLERS = Registry() # type: ignore[var-annotated]
|
||||
INTENTS_API_ENDPOINT = "/api/alexa"
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
class SpeechType(enum.StrEnum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = "PlainText"
|
||||
@@ -28,7 +30,7 @@ class SpeechType(enum.Enum):
|
||||
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
class CardType(enum.StrEnum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = "Simple"
|
||||
@@ -36,12 +38,12 @@ class CardType(enum.Enum):
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass):
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Activate Alexa component."""
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
|
||||
|
||||
async def async_setup_intents(hass):
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""Do intents setup.
|
||||
|
||||
Right now this module does not expose any, but the intent component breaks
|
||||
@@ -60,15 +62,15 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = "api:alexa"
|
||||
|
||||
async def post(self, request):
|
||||
async def post(self, request: http.HomeAssistantRequest) -> Response | bytes:
|
||||
"""Handle Alexa."""
|
||||
hass = request.app["hass"]
|
||||
message = await request.json()
|
||||
hass: HomeAssistant = request.app["hass"]
|
||||
message: dict[str, Any] = await request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa request: %s", message)
|
||||
|
||||
try:
|
||||
response = await async_handle_message(hass, message)
|
||||
response: dict[str, Any] = await async_handle_message(hass, message)
|
||||
return b"" if response is None else self.json(response)
|
||||
except UnknownRequest as err:
|
||||
_LOGGER.warning(str(err))
|
||||
@@ -99,15 +101,19 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
)
|
||||
|
||||
|
||||
def intent_error_response(hass, message, error):
|
||||
def intent_error_response(
|
||||
hass: HomeAssistant, message: dict[str, Any], error: str
|
||||
) -> dict[str, Any]:
|
||||
"""Return an Alexa response that will speak the error message."""
|
||||
alexa_intent_info = message.get("request").get("intent")
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_intent_info = message["request"].get("intent")
|
||||
alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
|
||||
alexa_response.add_speech(SpeechType.plaintext, error)
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
async def async_handle_message(hass, message):
|
||||
async def async_handle_message(
|
||||
hass: HomeAssistant, message: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Handle an Alexa intent.
|
||||
|
||||
Raises:
|
||||
@@ -117,19 +123,22 @@ async def async_handle_message(hass, message):
|
||||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get("request")
|
||||
req = message["request"]
|
||||
req_type = req["type"]
|
||||
|
||||
if not (handler := HANDLERS.get(req_type)):
|
||||
raise UnknownRequest(f"Received unknown request {req_type}")
|
||||
|
||||
return await handler(hass, message)
|
||||
response: dict[str, Any] = await handler(hass, message)
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register("SessionEndedRequest")
|
||||
@HANDLERS.register("IntentRequest")
|
||||
@HANDLERS.register("LaunchRequest")
|
||||
async def async_handle_intent(hass, message):
|
||||
async def async_handle_intent(
|
||||
hass: HomeAssistant, message: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Handle an intent request.
|
||||
|
||||
Raises:
|
||||
@@ -138,9 +147,9 @@ async def async_handle_intent(hass, message):
|
||||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get("request")
|
||||
req = message["request"]
|
||||
alexa_intent_info = req.get("intent")
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
|
||||
|
||||
if req["type"] == "LaunchRequest":
|
||||
intent_name = (
|
||||
@@ -187,7 +196,7 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
|
||||
# passes the id and name of the nearest possible slot resolution. For
|
||||
# reference to the request object structure, see the Alexa docs:
|
||||
# https://tinyurl.com/ybvm7jhs
|
||||
resolved_data = {}
|
||||
resolved_data: dict[str, Any] = {}
|
||||
resolved_data["value"] = request["value"]
|
||||
resolved_data["id"] = ""
|
||||
|
||||
@@ -226,18 +235,18 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
|
||||
return resolved_data
|
||||
|
||||
|
||||
class AlexaResponse:
|
||||
class AlexaIntentResponse:
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent_info):
|
||||
def __init__(self, hass: HomeAssistant, intent_info: dict[str, Any] | None) -> None:
|
||||
"""Initialize the response."""
|
||||
self.hass = hass
|
||||
self.speech = None
|
||||
self.card = None
|
||||
self.reprompt = None
|
||||
self.session_attributes = {}
|
||||
self.speech: dict[str, Any] | None = None
|
||||
self.card: dict[str, Any] | None = None
|
||||
self.reprompt: dict[str, Any] | None = None
|
||||
self.session_attributes: dict[str, Any] = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
self.variables: dict[str, Any] = {}
|
||||
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
@@ -252,7 +261,7 @@ class AlexaResponse:
|
||||
self.variables[_key] = _slot_data["value"]
|
||||
self.variables[_key + "_Id"] = _slot_data["id"]
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
def add_card(self, card_type: CardType, title: str, content: str) -> None:
|
||||
"""Add a card to the response."""
|
||||
assert self.card is None
|
||||
|
||||
@@ -266,7 +275,7 @@ class AlexaResponse:
|
||||
card["content"] = content
|
||||
self.card = card
|
||||
|
||||
def add_speech(self, speech_type, text):
|
||||
def add_speech(self, speech_type: SpeechType, text: str) -> None:
|
||||
"""Add speech to the response."""
|
||||
assert self.speech is None
|
||||
|
||||
@@ -274,7 +283,7 @@ class AlexaResponse:
|
||||
|
||||
self.speech = {"type": speech_type.value, key: text}
|
||||
|
||||
def add_reprompt(self, speech_type, text):
|
||||
def add_reprompt(self, speech_type: SpeechType, text: str) -> None:
|
||||
"""Add reprompt if user does not answer."""
|
||||
assert self.reprompt is None
|
||||
|
||||
@@ -284,9 +293,9 @@ class AlexaResponse:
|
||||
|
||||
self.reprompt = {"type": speech_type.value, key: text}
|
||||
|
||||
def as_dict(self):
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return response in an Alexa valid dict."""
|
||||
response = {"shouldEndSession": self.should_end_session}
|
||||
response: dict[str, Any] = {"shouldEndSession": self.should_end_session}
|
||||
|
||||
if self.card is not None:
|
||||
response["card"] = self.card
|
||||
|
@@ -1,20 +1,26 @@
|
||||
"""Describe logbook events."""
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.logbook import (
|
||||
LOGBOOK_ENTRY_ENTITY_ID,
|
||||
LOGBOOK_ENTRY_MESSAGE,
|
||||
LOGBOOK_ENTRY_NAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
|
||||
from .const import DOMAIN, EVENT_ALEXA_SMART_HOME
|
||||
|
||||
|
||||
@callback
|
||||
def async_describe_events(hass, async_describe_event):
|
||||
def async_describe_events(
|
||||
hass: HomeAssistant,
|
||||
async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None],
|
||||
) -> None:
|
||||
"""Describe logbook events."""
|
||||
|
||||
@callback
|
||||
def async_describe_logbook_event(event):
|
||||
def async_describe_logbook_event(event: Event) -> dict[str, Any]:
|
||||
"""Describe a logbook event."""
|
||||
data = event.data
|
||||
|
||||
|
@@ -1,6 +1,9 @@
|
||||
"""Alexa Resources and Assets."""
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AlexaGlobalCatalog:
|
||||
"""The Global Alexa catalog.
|
||||
|
||||
@@ -207,36 +210,40 @@ class AlexaCapabilityResource:
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
|
||||
def __init__(self, labels):
|
||||
def __init__(self, labels: list[str]) -> None:
|
||||
"""Initialize an Alexa resource."""
|
||||
self._resource_labels = []
|
||||
for label in labels:
|
||||
self._resource_labels.append(label)
|
||||
|
||||
def serialize_capability_resources(self):
|
||||
def serialize_capability_resources(self) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return capabilityResources object serialized for an API response."""
|
||||
return self.serialize_labels(self._resource_labels)
|
||||
|
||||
def serialize_configuration(self):
|
||||
def serialize_configuration(self) -> dict[str, Any]:
|
||||
"""Return serialized configuration for an API response.
|
||||
|
||||
Return ModeResources, PresetResources friendlyNames serialized.
|
||||
"""
|
||||
return []
|
||||
raise NotImplementedError()
|
||||
|
||||
def serialize_labels(self, resources):
|
||||
def serialize_labels(self, resources: list[str]) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return serialized labels for an API response.
|
||||
|
||||
Returns resource label objects for friendlyNames serialized.
|
||||
"""
|
||||
labels = []
|
||||
labels: list[dict[str, Any]] = []
|
||||
label_dict: dict[str, Any]
|
||||
for label in resources:
|
||||
if label in AlexaGlobalCatalog.__dict__.values():
|
||||
label = {"@type": "asset", "value": {"assetId": label}}
|
||||
label_dict = {"@type": "asset", "value": {"assetId": label}}
|
||||
else:
|
||||
label = {"@type": "text", "value": {"text": label, "locale": "en-US"}}
|
||||
label_dict = {
|
||||
"@type": "text",
|
||||
"value": {"text": label, "locale": "en-US"},
|
||||
}
|
||||
|
||||
labels.append(label)
|
||||
labels.append(label_dict)
|
||||
|
||||
return {"friendlyNames": labels}
|
||||
|
||||
@@ -247,22 +254,22 @@ class AlexaModeResource(AlexaCapabilityResource):
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
|
||||
def __init__(self, labels, ordered=False):
|
||||
def __init__(self, labels: list[str], ordered: bool = False) -> None:
|
||||
"""Initialize an Alexa modeResource."""
|
||||
super().__init__(labels)
|
||||
self._supported_modes = []
|
||||
self._mode_ordered = ordered
|
||||
self._supported_modes: list[dict[str, Any]] = []
|
||||
self._mode_ordered: bool = ordered
|
||||
|
||||
def add_mode(self, value, labels):
|
||||
def add_mode(self, value: str, labels: list[str]) -> None:
|
||||
"""Add mode to the supportedModes object."""
|
||||
self._supported_modes.append({"value": value, "labels": labels})
|
||||
|
||||
def serialize_configuration(self):
|
||||
def serialize_configuration(self) -> dict[str, Any]:
|
||||
"""Return serialized configuration for an API response.
|
||||
|
||||
Returns configuration for ModeResources friendlyNames serialized.
|
||||
"""
|
||||
mode_resources = []
|
||||
mode_resources: list[dict[str, Any]] = []
|
||||
for mode in self._supported_modes:
|
||||
result = {
|
||||
"value": mode["value"],
|
||||
@@ -282,10 +289,17 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
|
||||
"""
|
||||
|
||||
def __init__(self, labels, min_value, max_value, precision, unit=None):
|
||||
def __init__(
|
||||
self,
|
||||
labels: list[str],
|
||||
min_value: int | float,
|
||||
max_value: int | float,
|
||||
precision: int | float,
|
||||
unit: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Alexa presetResource."""
|
||||
super().__init__(labels)
|
||||
self._presets = []
|
||||
self._presets: list[dict[str, Any]] = []
|
||||
self._minimum_value = min_value
|
||||
self._maximum_value = max_value
|
||||
self._precision = precision
|
||||
@@ -293,16 +307,16 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
||||
if unit in AlexaGlobalCatalog.__dict__.values():
|
||||
self._unit_of_measure = unit
|
||||
|
||||
def add_preset(self, value, labels):
|
||||
def add_preset(self, value: int | float, labels: list[str]) -> None:
|
||||
"""Add preset to configuration presets array."""
|
||||
self._presets.append({"value": value, "labels": labels})
|
||||
|
||||
def serialize_configuration(self):
|
||||
def serialize_configuration(self) -> dict[str, Any]:
|
||||
"""Return serialized configuration for an API response.
|
||||
|
||||
Returns configuration for PresetResources friendlyNames serialized.
|
||||
"""
|
||||
configuration = {
|
||||
configuration: dict[str, Any] = {
|
||||
"supportedRange": {
|
||||
"minimumValue": self._minimum_value,
|
||||
"maximumValue": self._maximum_value,
|
||||
@@ -372,26 +386,28 @@ class AlexaSemantics:
|
||||
DIRECTIVE_MODE_SET_MODE = "SetMode"
|
||||
DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize an Alexa modeResource."""
|
||||
self._action_mappings = []
|
||||
self._state_mappings = []
|
||||
self._action_mappings: list[dict[str, Any]] = []
|
||||
self._state_mappings: list[dict[str, Any]] = []
|
||||
|
||||
def _add_action_mapping(self, semantics):
|
||||
def _add_action_mapping(self, semantics: dict[str, Any]) -> None:
|
||||
"""Add action mapping between actions and interface directives."""
|
||||
self._action_mappings.append(semantics)
|
||||
|
||||
def _add_state_mapping(self, semantics):
|
||||
def _add_state_mapping(self, semantics: dict[str, Any]) -> None:
|
||||
"""Add state mapping between states and interface directives."""
|
||||
self._state_mappings.append(semantics)
|
||||
|
||||
def add_states_to_value(self, states, value):
|
||||
def add_states_to_value(self, states: list[str], value: Any) -> None:
|
||||
"""Add StatesToValue stateMappings."""
|
||||
self._add_state_mapping(
|
||||
{"@type": self.STATES_TO_VALUE, "states": states, "value": value}
|
||||
)
|
||||
|
||||
def add_states_to_range(self, states, min_value, max_value):
|
||||
def add_states_to_range(
|
||||
self, states: list[str], min_value: int | float, max_value: int | float
|
||||
) -> None:
|
||||
"""Add StatesToRange stateMappings."""
|
||||
self._add_state_mapping(
|
||||
{
|
||||
@@ -401,7 +417,9 @@ class AlexaSemantics:
|
||||
}
|
||||
)
|
||||
|
||||
def add_action_to_directive(self, actions, directive, payload):
|
||||
def add_action_to_directive(
|
||||
self, actions: list[str], directive: str, payload: dict[str, Any]
|
||||
) -> None:
|
||||
"""Add ActionsToDirective actionMappings."""
|
||||
self._add_action_mapping(
|
||||
{
|
||||
@@ -411,9 +429,9 @@ class AlexaSemantics:
|
||||
}
|
||||
)
|
||||
|
||||
def serialize_semantics(self):
|
||||
def serialize_semantics(self) -> dict[str, Any]:
|
||||
"""Return semantics object serialized for an API response."""
|
||||
semantics = {}
|
||||
semantics: dict[str, Any] = {}
|
||||
if self._action_mappings:
|
||||
semantics[self.MAPPINGS_ACTION] = self._action_mappings
|
||||
if self._state_mappings:
|
||||
|
@@ -1,7 +1,13 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.components.http import HomeAssistantRequest
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
@@ -23,15 +29,16 @@ from .errors import AlexaBridgeUnreachableError, AlexaError
|
||||
from .handlers import HANDLERS
|
||||
from .state_report import AlexaDirective
|
||||
|
||||
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
|
||||
|
||||
|
||||
class AlexaConfig(AbstractConfig):
|
||||
"""Alexa config."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
_auth: Auth | None
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Initialize Alexa config."""
|
||||
super().__init__(hass)
|
||||
self._config = config
|
||||
@@ -42,40 +49,40 @@ class AlexaConfig(AbstractConfig):
|
||||
self._auth = None
|
||||
|
||||
@property
|
||||
def supports_auth(self):
|
||||
def supports_auth(self) -> bool:
|
||||
"""Return if config supports auth."""
|
||||
return self._auth is not None
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
def should_report_state(self) -> bool:
|
||||
"""Return if we should proactively report states."""
|
||||
return self._auth is not None and self.authorized
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
def endpoint(self) -> str | URL | None:
|
||||
"""Endpoint for report state."""
|
||||
return self._config.get(CONF_ENDPOINT)
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
def entity_config(self) -> dict[str, Any]:
|
||||
"""Return entity config."""
|
||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
def locale(self) -> str | None:
|
||||
"""Return config locale."""
|
||||
return self._config.get(CONF_LOCALE)
|
||||
|
||||
@core.callback
|
||||
def user_identifier(self):
|
||||
def user_identifier(self) -> str:
|
||||
"""Return an identifier for the user that represents this config."""
|
||||
return ""
|
||||
|
||||
@core.callback
|
||||
def should_expose(self, entity_id):
|
||||
def should_expose(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
if not self._config[CONF_FILTER].empty_filter:
|
||||
return self._config[CONF_FILTER](entity_id)
|
||||
return bool(self._config[CONF_FILTER](entity_id))
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
if registry_entry := entity_registry.async_get(entity_id):
|
||||
@@ -88,16 +95,19 @@ class AlexaConfig(AbstractConfig):
|
||||
return not auxiliary_entity
|
||||
|
||||
@core.callback
|
||||
def async_invalidate_access_token(self):
|
||||
def async_invalidate_access_token(self) -> None:
|
||||
"""Invalidate access token."""
|
||||
assert self._auth is not None
|
||||
self._auth.async_invalidate_access_token()
|
||||
|
||||
async def async_get_access_token(self):
|
||||
async def async_get_access_token(self) -> str | None:
|
||||
"""Get an access token."""
|
||||
assert self._auth is not None
|
||||
return await self._auth.async_get_access_token()
|
||||
|
||||
async def async_accept_grant(self, code):
|
||||
async def async_accept_grant(self, code: str) -> str | None:
|
||||
"""Accept a grant."""
|
||||
assert self._auth is not None
|
||||
return await self._auth.async_do_auth(code)
|
||||
|
||||
|
||||
@@ -124,20 +134,20 @@ class SmartHomeView(HomeAssistantView):
|
||||
url = SMART_HOME_HTTP_ENDPOINT
|
||||
name = "api:alexa:smart_home"
|
||||
|
||||
def __init__(self, smart_home_config):
|
||||
def __init__(self, smart_home_config: AlexaConfig) -> None:
|
||||
"""Initialize."""
|
||||
self.smart_home_config = smart_home_config
|
||||
|
||||
async def post(self, request):
|
||||
async def post(self, request: HomeAssistantRequest) -> web.Response | bytes:
|
||||
"""Handle Alexa Smart Home requests.
|
||||
|
||||
The Smart Home API requires the endpoint to be implemented in AWS
|
||||
Lambda, which will need to forward the requests to here and pass back
|
||||
the response.
|
||||
"""
|
||||
hass = request.app["hass"]
|
||||
user = request["hass_user"]
|
||||
message = await request.json()
|
||||
hass: HomeAssistant = request.app["hass"]
|
||||
user: User = request["hass_user"]
|
||||
message: dict[str, Any] = await request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
|
||||
|
||||
@@ -148,7 +158,13 @@ class SmartHomeView(HomeAssistantView):
|
||||
return b"" if response is None else self.json(response)
|
||||
|
||||
|
||||
async def async_handle_message(hass, config, request, context=None, enabled=True):
|
||||
async def async_handle_message(
|
||||
hass: HomeAssistant,
|
||||
config: AbstractConfig,
|
||||
request: dict[str, Any],
|
||||
context: Context | None = None,
|
||||
enabled: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Handle incoming API messages.
|
||||
|
||||
If enabled is False, the response to all messages will be a
|
||||
@@ -185,7 +201,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
|
||||
response = directive.error()
|
||||
except AlexaError as err:
|
||||
response = directive.error(
|
||||
error_type=err.error_type,
|
||||
error_type=str(err.error_type),
|
||||
error_message=err.error_message,
|
||||
payload=err.payload,
|
||||
)
|
||||
@@ -198,9 +214,13 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
|
||||
)
|
||||
response = directive.error(error_message="Unknown error")
|
||||
|
||||
request_info = {"namespace": directive.namespace, "name": directive.name}
|
||||
request_info: dict[str, Any] = {
|
||||
"namespace": directive.namespace,
|
||||
"name": directive.name,
|
||||
}
|
||||
|
||||
if directive.has_endpoint:
|
||||
assert directive.entity_id is not None
|
||||
request_info["entity_id"] = directive.entity_id
|
||||
|
||||
hass.bus.async_fire(
|
||||
|
@@ -5,7 +5,8 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import aiohttp
|
||||
@@ -13,7 +14,7 @@ import async_timeout
|
||||
|
||||
from homeassistant.components import event
|
||||
from homeassistant.const import MATCH_ALL, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.significant_change import create_checker
|
||||
@@ -46,17 +47,22 @@ DEFAULT_TIMEOUT = 10
|
||||
class AlexaDirective:
|
||||
"""An incoming Alexa directive."""
|
||||
|
||||
def __init__(self, request):
|
||||
entity: State
|
||||
entity_id: str | None
|
||||
endpoint: AlexaEntity
|
||||
instance: str | None
|
||||
|
||||
def __init__(self, request: dict[str, Any]) -> None:
|
||||
"""Initialize a directive."""
|
||||
self._directive = request[API_DIRECTIVE]
|
||||
self.namespace = self._directive[API_HEADER]["namespace"]
|
||||
self.name = self._directive[API_HEADER]["name"]
|
||||
self.payload = self._directive[API_PAYLOAD]
|
||||
self.has_endpoint = API_ENDPOINT in self._directive
|
||||
self._directive: dict[str, Any] = request[API_DIRECTIVE]
|
||||
self.namespace: str = self._directive[API_HEADER]["namespace"]
|
||||
self.name: str = self._directive[API_HEADER]["name"]
|
||||
self.payload: dict[str, Any] = self._directive[API_PAYLOAD]
|
||||
self.has_endpoint: bool = API_ENDPOINT in self._directive
|
||||
self.instance = None
|
||||
self.entity_id = None
|
||||
|
||||
self.entity = self.entity_id = self.endpoint = self.instance = None
|
||||
|
||||
def load_entity(self, hass, config):
|
||||
def load_entity(self, hass: HomeAssistant, config: AbstractConfig) -> None:
|
||||
"""Set attributes related to the entity for this request.
|
||||
|
||||
Sets these attributes when self.has_endpoint is True:
|
||||
@@ -71,18 +77,24 @@ class AlexaDirective:
|
||||
Will raise AlexaInvalidEndpointError if the endpoint in the request is
|
||||
malformed or nonexistent.
|
||||
"""
|
||||
_endpoint_id = self._directive[API_ENDPOINT]["endpointId"]
|
||||
_endpoint_id: str = self._directive[API_ENDPOINT]["endpointId"]
|
||||
self.entity_id = _endpoint_id.replace("#", ".")
|
||||
|
||||
self.entity = hass.states.get(self.entity_id)
|
||||
if not self.entity or not config.should_expose(self.entity_id):
|
||||
entity: State | None = hass.states.get(self.entity_id)
|
||||
if not entity or not config.should_expose(self.entity_id):
|
||||
raise AlexaInvalidEndpointError(_endpoint_id)
|
||||
self.entity = entity
|
||||
|
||||
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
|
||||
if "instance" in self._directive[API_HEADER]:
|
||||
self.instance = self._directive[API_HEADER]["instance"]
|
||||
|
||||
def response(self, name="Response", namespace="Alexa", payload=None):
|
||||
def response(
|
||||
self,
|
||||
name: str = "Response",
|
||||
namespace: str = "Alexa",
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> AlexaResponse:
|
||||
"""Create an API formatted response.
|
||||
|
||||
Async friendly.
|
||||
@@ -100,11 +112,11 @@ class AlexaDirective:
|
||||
|
||||
def error(
|
||||
self,
|
||||
namespace="Alexa",
|
||||
error_type="INTERNAL_ERROR",
|
||||
error_message="",
|
||||
payload=None,
|
||||
):
|
||||
namespace: str = "Alexa",
|
||||
error_type: str = "INTERNAL_ERROR",
|
||||
error_message: str = "",
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> AlexaResponse:
|
||||
"""Create a API formatted error response.
|
||||
|
||||
Async friendly.
|
||||
@@ -127,10 +139,12 @@ class AlexaDirective:
|
||||
class AlexaResponse:
|
||||
"""Class to hold a response."""
|
||||
|
||||
def __init__(self, name, namespace, payload=None):
|
||||
def __init__(
|
||||
self, name: str, namespace: str, payload: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Initialize the response."""
|
||||
payload = payload or {}
|
||||
self._response = {
|
||||
self._response: dict[str, Any] = {
|
||||
API_EVENT: {
|
||||
API_HEADER: {
|
||||
"namespace": namespace,
|
||||
@@ -143,16 +157,18 @@ class AlexaResponse:
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of this response."""
|
||||
return self._response[API_EVENT][API_HEADER]["name"]
|
||||
name: str = self._response[API_EVENT][API_HEADER]["name"]
|
||||
return name
|
||||
|
||||
@property
|
||||
def namespace(self):
|
||||
def namespace(self) -> str:
|
||||
"""Return the namespace of this response."""
|
||||
return self._response[API_EVENT][API_HEADER]["namespace"]
|
||||
namespace: str = self._response[API_EVENT][API_HEADER]["namespace"]
|
||||
return namespace
|
||||
|
||||
def set_correlation_token(self, token):
|
||||
def set_correlation_token(self, token: str) -> None:
|
||||
"""Set the correlationToken.
|
||||
|
||||
This should normally mirror the value from a request, and is set by
|
||||
@@ -160,7 +176,9 @@ class AlexaResponse:
|
||||
"""
|
||||
self._response[API_EVENT][API_HEADER]["correlationToken"] = token
|
||||
|
||||
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
|
||||
def set_endpoint_full(
|
||||
self, bearer_token: str | None, endpoint_id: str | None
|
||||
) -> None:
|
||||
"""Set the endpoint dictionary.
|
||||
|
||||
This is used to send proactive messages to Alexa.
|
||||
@@ -172,10 +190,7 @@ class AlexaResponse:
|
||||
if endpoint_id is not None:
|
||||
self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id
|
||||
|
||||
if cookie is not None:
|
||||
self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie
|
||||
|
||||
def set_endpoint(self, endpoint):
|
||||
def set_endpoint(self, endpoint: dict[str, Any]) -> None:
|
||||
"""Set the endpoint.
|
||||
|
||||
This should normally mirror the value from a request, and is set by
|
||||
@@ -183,11 +198,12 @@ class AlexaResponse:
|
||||
"""
|
||||
self._response[API_EVENT][API_ENDPOINT] = endpoint
|
||||
|
||||
def _properties(self):
|
||||
context = self._response.setdefault(API_CONTEXT, {})
|
||||
return context.setdefault("properties", [])
|
||||
def _properties(self) -> list[dict[str, Any]]:
|
||||
context: dict[str, Any] = self._response.setdefault(API_CONTEXT, {})
|
||||
properties: list[dict[str, Any]] = context.setdefault("properties", [])
|
||||
return properties
|
||||
|
||||
def add_context_property(self, prop):
|
||||
def add_context_property(self, prop: dict[str, Any]) -> None:
|
||||
"""Add a property to the response context.
|
||||
|
||||
The Alexa response includes a list of properties which provides
|
||||
@@ -204,7 +220,7 @@ class AlexaResponse:
|
||||
"""
|
||||
self._properties().append(prop)
|
||||
|
||||
def merge_context_properties(self, endpoint):
|
||||
def merge_context_properties(self, endpoint: AlexaEntity) -> None:
|
||||
"""Add all properties from given endpoint if not already set.
|
||||
|
||||
Handlers should be using .add_context_property().
|
||||
@@ -216,12 +232,14 @@ class AlexaResponse:
|
||||
if (prop["namespace"], prop["name"]) not in already_set:
|
||||
self.add_context_property(prop)
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> dict[str, Any]:
|
||||
"""Return response as a JSON-able data structure."""
|
||||
return self._response
|
||||
|
||||
|
||||
async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
async def async_enable_proactive_mode(
|
||||
hass: HomeAssistant, smart_home_config: AbstractConfig
|
||||
) -> CALLBACK_TYPE | None:
|
||||
"""Enable the proactive mode.
|
||||
|
||||
Proactive mode makes this component report state changes to Alexa.
|
||||
@@ -233,12 +251,12 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
def extra_significant_check(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict,
|
||||
old_extra_arg: dict,
|
||||
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
|
||||
old_extra_arg: Any,
|
||||
new_state: str,
|
||||
new_attrs: dict,
|
||||
new_extra_arg: dict,
|
||||
):
|
||||
new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
|
||||
new_extra_arg: Any,
|
||||
) -> bool:
|
||||
"""Check if the serialized data has changed."""
|
||||
return old_extra_arg is not None and old_extra_arg != new_extra_arg
|
||||
|
||||
@@ -248,7 +266,7 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
changed_entity: str,
|
||||
old_state: State | None,
|
||||
new_state: State | None,
|
||||
):
|
||||
) -> None:
|
||||
if not hass.is_running:
|
||||
return
|
||||
|
||||
@@ -307,8 +325,13 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
|
||||
|
||||
async def async_send_changereport_message(
|
||||
hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True
|
||||
):
|
||||
hass: HomeAssistant,
|
||||
config: AbstractConfig,
|
||||
alexa_entity: AlexaEntity,
|
||||
alexa_properties: list[dict[str, Any]],
|
||||
*,
|
||||
invalidate_access_token: bool = True,
|
||||
) -> None:
|
||||
"""Send a ChangeReport message for an Alexa entity.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events
|
||||
@@ -322,11 +345,11 @@ async def async_send_changereport_message(
|
||||
)
|
||||
return
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
endpoint = alexa_entity.alexa_id()
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
API_CHANGE: {
|
||||
"cause": {"type": Cause.APP_INTERACTION},
|
||||
"properties": alexa_properties,
|
||||
@@ -339,6 +362,7 @@ async def async_send_changereport_message(
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
try:
|
||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(
|
||||
@@ -393,9 +417,9 @@ async def async_send_add_or_update_message(
|
||||
"""
|
||||
token = await config.async_get_access_token()
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
endpoints = []
|
||||
endpoints: list[dict[str, Any]] = []
|
||||
|
||||
for entity_id in entity_ids:
|
||||
if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS:
|
||||
@@ -407,7 +431,10 @@ async def async_send_add_or_update_message(
|
||||
alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state)
|
||||
endpoints.append(alexa_entity.serialize_discovery())
|
||||
|
||||
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}}
|
||||
payload: dict[str, Any] = {
|
||||
"endpoints": endpoints,
|
||||
"scope": {"type": "BearerToken", "token": token},
|
||||
}
|
||||
|
||||
message = AlexaResponse(
|
||||
name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload
|
||||
@@ -416,6 +443,7 @@ async def async_send_add_or_update_message(
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
return await session.post(
|
||||
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
|
||||
)
|
||||
@@ -430,9 +458,9 @@ async def async_send_delete_message(
|
||||
"""
|
||||
token = await config.async_get_access_token()
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
endpoints = []
|
||||
endpoints: list[dict[str, Any]] = []
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
@@ -442,7 +470,10 @@ async def async_send_delete_message(
|
||||
|
||||
endpoints.append({"endpointId": generate_alexa_id(entity_id)})
|
||||
|
||||
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}}
|
||||
payload: dict[str, Any] = {
|
||||
"endpoints": endpoints,
|
||||
"scope": {"type": "BearerToken", "token": token},
|
||||
}
|
||||
|
||||
message = AlexaResponse(
|
||||
name="DeleteReport", namespace="Alexa.Discovery", payload=payload
|
||||
@@ -451,19 +482,22 @@ async def async_send_delete_message(
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
return await session.post(
|
||||
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
|
||||
)
|
||||
|
||||
|
||||
async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||
async def async_send_doorbell_event_message(
|
||||
hass: HomeAssistant, config: AbstractConfig, alexa_entity: AlexaEntity
|
||||
) -> None:
|
||||
"""Send a DoorbellPress event message for an Alexa entity.
|
||||
|
||||
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html
|
||||
"""
|
||||
token = await config.async_get_access_token()
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
endpoint = alexa_entity.alexa_id()
|
||||
|
||||
@@ -481,6 +515,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
try:
|
||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(
|
||||
|
@@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
@@ -19,11 +19,12 @@ from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .const import (
|
||||
|
@@ -80,304 +80,303 @@ class AmbientBinarySensorDescription(
|
||||
BINARY_SENSOR_DESCRIPTIONS = (
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATTOUT,
|
||||
name="Battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT1,
|
||||
name="Battery 1",
|
||||
translation_key="battery_1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT2,
|
||||
name="Battery 2",
|
||||
translation_key="battery_2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT3,
|
||||
name="Battery 3",
|
||||
translation_key="battery_3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT4,
|
||||
name="Battery 4",
|
||||
translation_key="battery_4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT5,
|
||||
name="Battery 5",
|
||||
translation_key="battery_5",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT6,
|
||||
name="Battery 6",
|
||||
translation_key="battery_6",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT7,
|
||||
name="Battery 7",
|
||||
translation_key="battery_7",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT8,
|
||||
name="Battery 8",
|
||||
translation_key="battery_8",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT9,
|
||||
name="Battery 9",
|
||||
translation_key="battery_9",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATTIN,
|
||||
name="Interior battery",
|
||||
translation_key="interior_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT10,
|
||||
name="Battery 10",
|
||||
translation_key="battery_10",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK1,
|
||||
name="Leak detector battery 1",
|
||||
translation_key="leak_detector_battery_1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK2,
|
||||
name="Leak detector battery 2",
|
||||
translation_key="leak_detector_battery_2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK3,
|
||||
name="Leak detector battery 3",
|
||||
translation_key="leak_detector_battery_3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK4,
|
||||
name="Leak detector battery 4",
|
||||
translation_key="leak_detector_battery_4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM1,
|
||||
name="Soil monitor battery 1",
|
||||
translation_key="soil_monitor_battery_1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM2,
|
||||
name="Soil monitor battery 2",
|
||||
translation_key="soil_monitor_battery_2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM3,
|
||||
name="Soil monitor battery 3",
|
||||
translation_key="soil_monitor_battery_3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM4,
|
||||
name="Soil monitor battery 4",
|
||||
translation_key="soil_monitor_battery_4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM5,
|
||||
name="Soil monitor battery 5",
|
||||
translation_key="soil_monitor_battery_5",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM6,
|
||||
name="Soil monitor battery 6",
|
||||
translation_key="soil_monitor_battery_6",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM7,
|
||||
name="Soil monitor battery 7",
|
||||
translation_key="soil_monitor_battery_7",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM8,
|
||||
name="Soil monitor battery 8",
|
||||
translation_key="soil_monitor_battery_8",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM9,
|
||||
name="Soil monitor battery 9",
|
||||
translation_key="soil_monitor_battery_9",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM10,
|
||||
name="Soil monitor battery 10",
|
||||
translation_key="soil_monitor_battery_10",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_CO2,
|
||||
name="CO2 battery",
|
||||
translation_key="co2_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LIGHTNING,
|
||||
name="Lightning detector battery",
|
||||
translation_key="lightning_detector_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK1,
|
||||
name="Leak detector 1",
|
||||
translation_key="leak_detector_1",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK2,
|
||||
name="Leak detector 2",
|
||||
translation_key="leak_detector_2",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK3,
|
||||
name="Leak detector 3",
|
||||
translation_key="leak_detector_3",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK4,
|
||||
name="Leak detector 4",
|
||||
translation_key="leak_detector_4",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_PM25IN_BATT,
|
||||
name="PM25 indoor battery",
|
||||
translation_key="pm25_indoor_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_PM25_BATT,
|
||||
name="PM25 battery",
|
||||
translation_key="pm25_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY1,
|
||||
name="Relay 1",
|
||||
translation_key="relay_1",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY2,
|
||||
name="Relay 2",
|
||||
translation_key="relay_2",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY3,
|
||||
name="Relay 3",
|
||||
translation_key="relay_3",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY4,
|
||||
name="Relay 4",
|
||||
translation_key="relay_4",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY5,
|
||||
name="Relay 5",
|
||||
translation_key="relay_5",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY6,
|
||||
name="Relay 6",
|
||||
translation_key="relay_6",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY7,
|
||||
name="Relay 7",
|
||||
translation_key="relay_7",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY8,
|
||||
name="Relay 8",
|
||||
translation_key="relay_8",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY9,
|
||||
name="Relay 9",
|
||||
translation_key="relay_9",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY10,
|
||||
name="Relay 10",
|
||||
translation_key="relay_10",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
|
@@ -113,544 +113,536 @@ TYPE_YEARLYRAININ = "yearlyrainin"
|
||||
SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescription(
|
||||
key=TYPE_24HOURRAININ,
|
||||
name="24 hr rain",
|
||||
translation_key="24_hour_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25,
|
||||
name="AQI PM2.5",
|
||||
translation_key="pm25_aqi",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_24H,
|
||||
name="AQI PM2.5 24h avg",
|
||||
translation_key="pm25_aqi_24h_average",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_IN,
|
||||
name="AQI PM2.5 indoor",
|
||||
translation_key="pm25_indoor_aqi",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_IN_24H,
|
||||
name="AQI PM2.5 indoor 24h avg",
|
||||
translation_key="pm25_indoor_aqi_24h_average",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMABSIN,
|
||||
name="Abs pressure",
|
||||
translation_key="absolute_pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMRELIN,
|
||||
name="Rel pressure",
|
||||
translation_key="relative_pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_CO2,
|
||||
name="CO2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_DAILYRAININ,
|
||||
name="Daily rain",
|
||||
translation_key="daily_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_DEWPOINT,
|
||||
name="Dew point",
|
||||
translation_key="dew_point",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_EVENTRAININ,
|
||||
name="Event rain",
|
||||
translation_key="event_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_FEELSLIKE,
|
||||
name="Feels like",
|
||||
translation_key="feels_like",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HOURLYRAININ,
|
||||
name="Hourly rain rate",
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY10,
|
||||
name="Humidity 10",
|
||||
translation_key="humidity_10",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY1,
|
||||
name="Humidity 1",
|
||||
translation_key="humidity_1",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY2,
|
||||
name="Humidity 2",
|
||||
translation_key="humidity_2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY3,
|
||||
name="Humidity 3",
|
||||
translation_key="humidity_3",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY4,
|
||||
name="Humidity 4",
|
||||
translation_key="humidity_4",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY5,
|
||||
name="Humidity 5",
|
||||
translation_key="humidity_5",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY6,
|
||||
name="Humidity 6",
|
||||
translation_key="humidity_6",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY7,
|
||||
name="Humidity 7",
|
||||
translation_key="humidity_7",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY8,
|
||||
name="Humidity 8",
|
||||
translation_key="humidity_8",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY9,
|
||||
name="Humidity 9",
|
||||
translation_key="humidity_9",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITYIN,
|
||||
name="Humidity in",
|
||||
translation_key="humidity_indoor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LASTRAIN,
|
||||
name="Last rain",
|
||||
translation_key="last_rain",
|
||||
icon="mdi:water",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LIGHTNING_PER_DAY,
|
||||
name="Lightning strikes per day",
|
||||
translation_key="lightning_strikes_per_day",
|
||||
icon="mdi:lightning-bolt",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LIGHTNING_PER_HOUR,
|
||||
name="Lightning strikes per hour",
|
||||
translation_key="lightning_strikes_per_hour",
|
||||
icon="mdi:lightning-bolt",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MAXDAILYGUST,
|
||||
name="Max gust",
|
||||
translation_key="max_gust",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MONTHLYRAININ,
|
||||
name="Monthly rain",
|
||||
translation_key="monthly_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_24H,
|
||||
name="PM25 24h avg",
|
||||
translation_key="pm25_24h_average",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN,
|
||||
name="PM25 indoor",
|
||||
translation_key="pm25_indoor",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN_24H,
|
||||
name="PM25 indoor 24h avg",
|
||||
translation_key="pm25_indoor_24h_average",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25,
|
||||
name="PM25",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM10,
|
||||
name="Soil humidity 10",
|
||||
translation_key="soil_humidity_10",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM1,
|
||||
name="Soil humidity 1",
|
||||
translation_key="soil_humidity_1",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM2,
|
||||
name="Soil humidity 2",
|
||||
translation_key="soil_humidity_2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM3,
|
||||
name="Soil humidity 3",
|
||||
translation_key="soil_humidity_3",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM4,
|
||||
name="Soil humidity 4",
|
||||
translation_key="soil_humidity_4",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM5,
|
||||
name="Soil humidity 5",
|
||||
translation_key="soil_humidity_5",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM6,
|
||||
name="Soil humidity 6",
|
||||
translation_key="soil_humidity_6",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM7,
|
||||
name="Soil humidity 7",
|
||||
translation_key="soil_humidity_7",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM8,
|
||||
name="Soil humidity 8",
|
||||
translation_key="soil_humidity_8",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM9,
|
||||
name="Soil humidity 9",
|
||||
translation_key="soil_humidity_9",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP10F,
|
||||
name="Soil temp 10",
|
||||
translation_key="soil_temperature_10",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP1F,
|
||||
name="Soil temp 1",
|
||||
translation_key="soil_temperature_1",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP2F,
|
||||
name="Soil temp 2",
|
||||
translation_key="soil_temperature_2",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP3F,
|
||||
name="Soil temp 3",
|
||||
translation_key="soil_temperature_3",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP4F,
|
||||
name="Soil temp 4",
|
||||
translation_key="soil_temperature_4",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP5F,
|
||||
name="Soil temp 5",
|
||||
translation_key="soil_temperature_5",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP6F,
|
||||
name="Soil temp 6",
|
||||
translation_key="soil_temperature_6",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP7F,
|
||||
name="Soil temp 7",
|
||||
translation_key="soil_temperature_7",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP8F,
|
||||
name="Soil temp 8",
|
||||
translation_key="soil_temperature_8",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP9F,
|
||||
name="Soil temp 9",
|
||||
translation_key="soil_temperature_9",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOLARRADIATION,
|
||||
name="Solar rad",
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
device_class=SensorDeviceClass.IRRADIANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOLARRADIATION_LX,
|
||||
name="Solar rad",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP10F,
|
||||
name="Temp 10",
|
||||
translation_key="temperature_10",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP1F,
|
||||
name="Temp 1",
|
||||
translation_key="temperature_1",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP2F,
|
||||
name="Temp 2",
|
||||
translation_key="temperature_2",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP3F,
|
||||
name="Temp 3",
|
||||
translation_key="temperature_3",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP4F,
|
||||
name="Temp 4",
|
||||
translation_key="temperature_4",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP5F,
|
||||
name="Temp 5",
|
||||
translation_key="temperature_5",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP6F,
|
||||
name="Temp 6",
|
||||
translation_key="temperature_6",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP7F,
|
||||
name="Temp 7",
|
||||
translation_key="temperature_7",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP8F,
|
||||
name="Temp 8",
|
||||
translation_key="temperature_8",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP9F,
|
||||
name="Temp 9",
|
||||
translation_key="temperature_9",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMPF,
|
||||
name="Temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMPINF,
|
||||
name="Inside temp",
|
||||
translation_key="inside_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TOTALRAININ,
|
||||
name="Lifetime rain",
|
||||
translation_key="lifetime_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_UV,
|
||||
name="UV index",
|
||||
translation_key="uv_index",
|
||||
native_unit_of_measurement="Index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WEEKLYRAININ,
|
||||
name="Weekly rain",
|
||||
translation_key="weekly_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR,
|
||||
name="Wind dir",
|
||||
translation_key="wind_direction",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG10M,
|
||||
name="Wind dir avg 10m",
|
||||
translation_key="wind_direction_average_10m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG2M,
|
||||
name="Wind dir avg 2m",
|
||||
translation_key="wind_direction_average_2m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTDIR,
|
||||
name="Gust dir",
|
||||
translation_key="wind_gust_direction",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTMPH,
|
||||
name="Wind gust",
|
||||
translation_key="wind_gust",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPDMPH_AVG10M,
|
||||
name="Wind avg 10m",
|
||||
translation_key="wind_average_10m",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPDMPH_AVG2M,
|
||||
name="Wind avg 2m",
|
||||
translation_key="wind_average_2m",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPEEDMPH,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_YEARLYRAININ,
|
||||
name="Yearly rain",
|
||||
translation_key="yearly_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
|
@@ -16,5 +16,356 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"battery_1": {
|
||||
"name": "Battery 1"
|
||||
},
|
||||
"battery_2": {
|
||||
"name": "Battery 2"
|
||||
},
|
||||
"battery_3": {
|
||||
"name": "Battery 3"
|
||||
},
|
||||
"battery_4": {
|
||||
"name": "Battery 4"
|
||||
},
|
||||
"battery_5": {
|
||||
"name": "Battery 5"
|
||||
},
|
||||
"battery_6": {
|
||||
"name": "Battery 6"
|
||||
},
|
||||
"battery_7": {
|
||||
"name": "Battery 7"
|
||||
},
|
||||
"battery_8": {
|
||||
"name": "Battery 8"
|
||||
},
|
||||
"battery_9": {
|
||||
"name": "Battery 9"
|
||||
},
|
||||
"battery_10": {
|
||||
"name": "Battery 10"
|
||||
},
|
||||
"interior_battery": {
|
||||
"name": "Interior battery"
|
||||
},
|
||||
"leak_detector_battery_1": {
|
||||
"name": "Leak detector battery 1"
|
||||
},
|
||||
"leak_detector_battery_2": {
|
||||
"name": "Leak detector battery 2"
|
||||
},
|
||||
"leak_detector_battery_3": {
|
||||
"name": "Leak detector battery 3"
|
||||
},
|
||||
"leak_detector_battery_4": {
|
||||
"name": "Leak detector battery 4"
|
||||
},
|
||||
"soil_monitor_battery_1": {
|
||||
"name": "Soil monitor battery 1"
|
||||
},
|
||||
"soil_monitor_battery_2": {
|
||||
"name": "Soil monitor battery 2"
|
||||
},
|
||||
"soil_monitor_battery_3": {
|
||||
"name": "Soil monitor battery 3"
|
||||
},
|
||||
"soil_monitor_battery_4": {
|
||||
"name": "Soil monitor battery 4"
|
||||
},
|
||||
"soil_monitor_battery_5": {
|
||||
"name": "Soil monitor battery 5"
|
||||
},
|
||||
"soil_monitor_battery_6": {
|
||||
"name": "Soil monitor battery 6"
|
||||
},
|
||||
"soil_monitor_battery_7": {
|
||||
"name": "Soil monitor battery 7"
|
||||
},
|
||||
"soil_monitor_battery_8": {
|
||||
"name": "Soil monitor battery 8"
|
||||
},
|
||||
"soil_monitor_battery_9": {
|
||||
"name": "Soil monitor battery 9"
|
||||
},
|
||||
"soil_monitor_battery_10": {
|
||||
"name": "Soil monitor battery 10"
|
||||
},
|
||||
"co2_battery": {
|
||||
"name": "Carbon dioxide battery"
|
||||
},
|
||||
"lightning_detector_battery": {
|
||||
"name": "Lightning detector battery"
|
||||
},
|
||||
"leak_detector_1": {
|
||||
"name": "Leak detector 1"
|
||||
},
|
||||
"leak_detector_2": {
|
||||
"name": "Leak detector 2"
|
||||
},
|
||||
"leak_detector_3": {
|
||||
"name": "Leak detector 3"
|
||||
},
|
||||
"leak_detector_4": {
|
||||
"name": "Leak detector 4"
|
||||
},
|
||||
"pm25_indoor_battery": {
|
||||
"name": "PM25 indoor battery"
|
||||
},
|
||||
"pm25_battery": {
|
||||
"name": "PM25 battery"
|
||||
},
|
||||
"relay_1": {
|
||||
"name": "Relay 1"
|
||||
},
|
||||
"relay_2": {
|
||||
"name": "Relay 2"
|
||||
},
|
||||
"relay_3": {
|
||||
"name": "Relay 3"
|
||||
},
|
||||
"relay_4": {
|
||||
"name": "Relay 4"
|
||||
},
|
||||
"relay_5": {
|
||||
"name": "Relay 5"
|
||||
},
|
||||
"relay_6": {
|
||||
"name": "Relay 6"
|
||||
},
|
||||
"relay_7": {
|
||||
"name": "Relay 7"
|
||||
},
|
||||
"relay_8": {
|
||||
"name": "Relay 8"
|
||||
},
|
||||
"relay_9": {
|
||||
"name": "Relay 9"
|
||||
},
|
||||
"relay_10": {
|
||||
"name": "Relay 10"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"24_hour_rain": {
|
||||
"name": "Rain 24 hours"
|
||||
},
|
||||
"pm25_aqi": {
|
||||
"name": "PM2.5 AQI"
|
||||
},
|
||||
"pm25_aqi_24h_average": {
|
||||
"name": "PM2.5 AQI 24 hour average"
|
||||
},
|
||||
"pm25_indoor_aqi": {
|
||||
"name": "PM2.5 indoor AQI"
|
||||
},
|
||||
"pm25_indoor_aqi_24h_average": {
|
||||
"name": "PM2.5 indoor AQI"
|
||||
},
|
||||
"absolute_pressure": {
|
||||
"name": "Absolute pressure"
|
||||
},
|
||||
"relative_pressure": {
|
||||
"name": "Relative pressure"
|
||||
},
|
||||
"daily_rain": {
|
||||
"name": "Daily rain"
|
||||
},
|
||||
"dew_point": {
|
||||
"name": "Dew point"
|
||||
},
|
||||
"event_rain": {
|
||||
"name": "Event rain"
|
||||
},
|
||||
"feels_like": {
|
||||
"name": "Feels like"
|
||||
},
|
||||
"humidity_1": {
|
||||
"name": "Humidity 1"
|
||||
},
|
||||
"humidity_2": {
|
||||
"name": "Humidity 2"
|
||||
},
|
||||
"humidity_3": {
|
||||
"name": "Humidity 3"
|
||||
},
|
||||
"humidity_4": {
|
||||
"name": "Humidity 4"
|
||||
},
|
||||
"humidity_5": {
|
||||
"name": "Humidity 5"
|
||||
},
|
||||
"humidity_6": {
|
||||
"name": "Humidity 6"
|
||||
},
|
||||
"humidity_7": {
|
||||
"name": "Humidity 7"
|
||||
},
|
||||
"humidity_8": {
|
||||
"name": "Humidity 8"
|
||||
},
|
||||
"humidity_9": {
|
||||
"name": "Humidity 9"
|
||||
},
|
||||
"humidity_10": {
|
||||
"name": "Humidity 10"
|
||||
},
|
||||
"humidity_indoor": {
|
||||
"name": "Humidity indoor"
|
||||
},
|
||||
"last_rain": {
|
||||
"name": "Last rain"
|
||||
},
|
||||
"lightning_strikes_per_day": {
|
||||
"name": "Lightning strikes per day"
|
||||
},
|
||||
"lightning_strikes_per_hour": {
|
||||
"name": "Lightning strikes per hour"
|
||||
},
|
||||
"max_gust": {
|
||||
"name": "Max gust"
|
||||
},
|
||||
"monthly_rain": {
|
||||
"name": "Monthly rain"
|
||||
},
|
||||
"pm25_24h_average": {
|
||||
"name": "PM2.5 24 hour average"
|
||||
},
|
||||
"pm25_indoor": {
|
||||
"name": "PM2.5 indoor"
|
||||
},
|
||||
"pm25_indoor_24h_average": {
|
||||
"name": "PM2.5 indoor 24 hour average"
|
||||
},
|
||||
"soil_humidity_1": {
|
||||
"name": "Soil humidity 1"
|
||||
},
|
||||
"soil_humidity_2": {
|
||||
"name": "Soil humidity 2"
|
||||
},
|
||||
"soil_humidity_3": {
|
||||
"name": "Soil humidity 3"
|
||||
},
|
||||
"soil_humidity_4": {
|
||||
"name": "Soil humidity 4"
|
||||
},
|
||||
"soil_humidity_5": {
|
||||
"name": "Soil humidity 5"
|
||||
},
|
||||
"soil_humidity_6": {
|
||||
"name": "Soil humidity 6"
|
||||
},
|
||||
"soil_humidity_7": {
|
||||
"name": "Soil humidity 7"
|
||||
},
|
||||
"soil_humidity_8": {
|
||||
"name": "Soil humidity 8"
|
||||
},
|
||||
"soil_humidity_9": {
|
||||
"name": "Soil humidity 9"
|
||||
},
|
||||
"soil_humidity_10": {
|
||||
"name": "Soil humidity 10"
|
||||
},
|
||||
"soil_temperature_1": {
|
||||
"name": "Soil temperature 1"
|
||||
},
|
||||
"soil_temperature_2": {
|
||||
"name": "Soil temperature 2"
|
||||
},
|
||||
"soil_temperature_3": {
|
||||
"name": "Soil temperature 3"
|
||||
},
|
||||
"soil_temperature_4": {
|
||||
"name": "Soil temperature 4"
|
||||
},
|
||||
"soil_temperature_5": {
|
||||
"name": "Soil temperature 5"
|
||||
},
|
||||
"soil_temperature_6": {
|
||||
"name": "Soil temperature 6"
|
||||
},
|
||||
"soil_temperature_7": {
|
||||
"name": "Soil temperature 7"
|
||||
},
|
||||
"soil_temperature_8": {
|
||||
"name": "Soil temperature 8"
|
||||
},
|
||||
"soil_temperature_9": {
|
||||
"name": "Soil temperature 9"
|
||||
},
|
||||
"soil_temperature_10": {
|
||||
"name": "Soil temperature 10"
|
||||
},
|
||||
"temperature_1": {
|
||||
"name": "Temperature 1"
|
||||
},
|
||||
"temperature_2": {
|
||||
"name": "Temperature 2"
|
||||
},
|
||||
"temperature_3": {
|
||||
"name": "Temperature 3"
|
||||
},
|
||||
"temperature_4": {
|
||||
"name": "Temperature 4"
|
||||
},
|
||||
"temperature_5": {
|
||||
"name": "Temperature 5"
|
||||
},
|
||||
"temperature_6": {
|
||||
"name": "Temperature 6"
|
||||
},
|
||||
"temperature_7": {
|
||||
"name": "Temperature 7"
|
||||
},
|
||||
"temperature_8": {
|
||||
"name": "Temperature 8"
|
||||
},
|
||||
"temperature_9": {
|
||||
"name": "Temperature 9"
|
||||
},
|
||||
"temperature_10": {
|
||||
"name": "Temperature 10"
|
||||
},
|
||||
"inside_temperature": {
|
||||
"name": "Inside temperature"
|
||||
},
|
||||
"lifetime_rain": {
|
||||
"name": "Lifetime rain"
|
||||
},
|
||||
"uv_index": {
|
||||
"name": "UV index"
|
||||
},
|
||||
"weekly_rain": {
|
||||
"name": "Weekly rain"
|
||||
},
|
||||
"wind_direction": {
|
||||
"name": "Wind direction"
|
||||
},
|
||||
"wind_direction_average_10m": {
|
||||
"name": "Wind direction average 10 minutes"
|
||||
},
|
||||
"wind_direction_average_2m": {
|
||||
"name": "Wind direction average 2 minutes"
|
||||
},
|
||||
"wind_gust_direction": {
|
||||
"name": "Wind gust direction"
|
||||
},
|
||||
"wind_gust": {
|
||||
"name": "Wind gust"
|
||||
},
|
||||
"wind_average_10m": {
|
||||
"name": "Wind average 10 minutes"
|
||||
},
|
||||
"wind_average_2m": {
|
||||
"name": "Wind average 2 minutes"
|
||||
},
|
||||
"yearly_rain": {
|
||||
"name": "Yearly rain"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,9 +22,7 @@ from homeassistant.components.recorder import (
|
||||
get_instance as get_recorder_instance,
|
||||
)
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.const import (
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Base class for Android IP Webcam entities."""
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -32,9 +32,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
@@ -58,6 +58,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert self.host
|
||||
api = create_api(self.hass, self.host, enable_ime=False)
|
||||
try:
|
||||
await api.async_generate_cert_if_missing()
|
||||
self.name, self.mac = await api.async_get_name_and_mac()
|
||||
assert self.mac
|
||||
await self.async_set_unique_id(format_mac(self.mac))
|
||||
|
@@ -7,8 +7,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@@ -6,7 +6,7 @@ from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -15,8 +15,8 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ANTHEMAV_UDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -26,11 +26,12 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
|
||||
|
||||
|
@@ -31,7 +31,7 @@ from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -3,9 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import (
|
||||
DEVICE_TRIGGER_BASE_SCHEMA,
|
||||
)
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE_ID,
|
||||
|
@@ -19,8 +19,8 @@ from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .config_flow import get_entry_client
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Aseko entity."""
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@@ -18,6 +18,7 @@ from .pipeline import (
|
||||
PipelineInput,
|
||||
PipelineRun,
|
||||
PipelineStage,
|
||||
WakeWordSettings,
|
||||
async_create_default_pipeline,
|
||||
async_get_pipeline,
|
||||
async_get_pipelines,
|
||||
@@ -35,6 +36,7 @@ __all__ = (
|
||||
"PipelineEvent",
|
||||
"PipelineEventType",
|
||||
"PipelineNotFound",
|
||||
"WakeWordSettings",
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
@@ -57,7 +59,10 @@ async def async_pipeline_from_audio_stream(
|
||||
pipeline_id: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
tts_audio_output: str | None = None,
|
||||
wake_word_settings: WakeWordSettings | None = None,
|
||||
device_id: str | None = None,
|
||||
start_stage: PipelineStage = PipelineStage.STT,
|
||||
end_stage: PipelineStage = PipelineStage.TTS,
|
||||
) -> None:
|
||||
"""Create an audio pipeline from an audio stream.
|
||||
|
||||
@@ -72,10 +77,11 @@ async def async_pipeline_from_audio_stream(
|
||||
hass,
|
||||
context=context,
|
||||
pipeline=async_get_pipeline(hass, pipeline_id=pipeline_id),
|
||||
start_stage=PipelineStage.STT,
|
||||
end_stage=PipelineStage.TTS,
|
||||
start_stage=start_stage,
|
||||
end_stage=end_stage,
|
||||
event_callback=event_callback,
|
||||
tts_audio_output=tts_audio_output,
|
||||
wake_word_settings=wake_word_settings,
|
||||
),
|
||||
)
|
||||
await pipeline_input.validate()
|
||||
|
@@ -18,6 +18,14 @@ class PipelineNotFound(PipelineError):
|
||||
"""Unspecified pipeline picked."""
|
||||
|
||||
|
||||
class WakeWordDetectionError(PipelineError):
|
||||
"""Error in wake-word-detection portion of pipeline."""
|
||||
|
||||
|
||||
class WakeWordTimeoutError(WakeWordDetectionError):
|
||||
"""Timeout when wake word was not detected."""
|
||||
|
||||
|
||||
class SpeechToTextError(PipelineError):
|
||||
"""Error in speech-to-text portion of pipeline."""
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "assist_pipeline",
|
||||
"name": "Assist pipeline",
|
||||
"codeowners": ["@balloob", "@synesthesiam"],
|
||||
"dependencies": ["conversation", "stt", "tts"],
|
||||
"dependencies": ["conversation", "stt", "tts", "wake_word"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_pipeline",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
|
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable, Callable, Iterable
|
||||
from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
@@ -10,7 +10,14 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation, media_source, stt, tts, websocket_api
|
||||
from homeassistant.components import (
|
||||
conversation,
|
||||
media_source,
|
||||
stt,
|
||||
tts,
|
||||
wake_word,
|
||||
websocket_api,
|
||||
)
|
||||
from homeassistant.components.tts.media_source import (
|
||||
generate_media_source_id as tts_generate_media_source_id,
|
||||
)
|
||||
@@ -39,7 +46,10 @@ from .error import (
|
||||
PipelineNotFound,
|
||||
SpeechToTextError,
|
||||
TextToSpeechError,
|
||||
WakeWordDetectionError,
|
||||
WakeWordTimeoutError,
|
||||
)
|
||||
from .vad import VoiceActivityTimeout, VoiceCommandSegmenter
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -241,6 +251,8 @@ class PipelineEventType(StrEnum):
|
||||
|
||||
RUN_START = "run-start"
|
||||
RUN_END = "run-end"
|
||||
WAKE_WORD_START = "wake_word-start"
|
||||
WAKE_WORD_END = "wake_word-end"
|
||||
STT_START = "stt-start"
|
||||
STT_END = "stt-end"
|
||||
INTENT_START = "intent-start"
|
||||
@@ -297,12 +309,14 @@ class Pipeline:
|
||||
class PipelineStage(StrEnum):
|
||||
"""Stages of a pipeline."""
|
||||
|
||||
WAKE_WORD = "wake_word"
|
||||
STT = "stt"
|
||||
INTENT = "intent"
|
||||
TTS = "tts"
|
||||
|
||||
|
||||
PIPELINE_STAGE_ORDER = [
|
||||
PipelineStage.WAKE_WORD,
|
||||
PipelineStage.STT,
|
||||
PipelineStage.INTENT,
|
||||
PipelineStage.TTS,
|
||||
@@ -327,6 +341,17 @@ class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WakeWordSettings:
|
||||
"""Settings for wake word detection."""
|
||||
|
||||
timeout: float | None = None
|
||||
"""Seconds of silence before detection times out."""
|
||||
|
||||
audio_seconds_to_buffer: float = 0
|
||||
"""Seconds of audio to buffer before detection and forward to STT."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineRun:
|
||||
"""Running context for a pipeline."""
|
||||
@@ -341,17 +366,20 @@ class PipelineRun:
|
||||
runner_data: Any | None = None
|
||||
intent_agent: str | None = None
|
||||
tts_audio_output: str | None = None
|
||||
wake_word_settings: WakeWordSettings | None = None
|
||||
|
||||
id: str = field(default_factory=ulid_util.ulid)
|
||||
stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False)
|
||||
tts_engine: str = field(init=False)
|
||||
tts_options: dict | None = field(init=False, default=None)
|
||||
wake_word_engine: str = field(init=False)
|
||||
wake_word_provider: wake_word.WakeWordDetectionEntity = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Set language for pipeline."""
|
||||
self.language = self.pipeline.language or self.hass.config.language
|
||||
|
||||
# stt -> intent -> tts
|
||||
# wake -> stt -> intent -> tts
|
||||
if PIPELINE_STAGE_ORDER.index(self.end_stage) < PIPELINE_STAGE_ORDER.index(
|
||||
self.start_stage
|
||||
):
|
||||
@@ -393,6 +421,141 @@ class PipelineRun:
|
||||
)
|
||||
)
|
||||
|
||||
async def prepare_wake_word_detection(self) -> None:
|
||||
"""Prepare wake-word-detection."""
|
||||
# Need to add to pipeline store
|
||||
engine = wake_word.async_default_engine(self.hass)
|
||||
if engine is None:
|
||||
raise WakeWordDetectionError(
|
||||
code="wake-engine-missing",
|
||||
message="No wake word engine",
|
||||
)
|
||||
|
||||
wake_word_provider = wake_word.async_get_wake_word_detection_entity(
|
||||
self.hass, engine
|
||||
)
|
||||
if wake_word_provider is None:
|
||||
raise WakeWordDetectionError(
|
||||
code="wake-provider-missing",
|
||||
message=f"No wake-word-detection provider for: {engine}",
|
||||
)
|
||||
|
||||
self.wake_word_engine = engine
|
||||
self.wake_word_provider = wake_word_provider
|
||||
|
||||
async def wake_word_detection(
|
||||
self,
|
||||
stream: AsyncIterable[bytes],
|
||||
audio_buffer: list[bytes],
|
||||
) -> wake_word.DetectionResult | None:
|
||||
"""Run wake-word-detection portion of pipeline. Returns detection result."""
|
||||
metadata_dict = asdict(
|
||||
stt.SpeechMetadata(
|
||||
language="",
|
||||
format=stt.AudioFormats.WAV,
|
||||
codec=stt.AudioCodecs.PCM,
|
||||
bit_rate=stt.AudioBitRates.BITRATE_16,
|
||||
sample_rate=stt.AudioSampleRates.SAMPLERATE_16000,
|
||||
channel=stt.AudioChannels.CHANNEL_MONO,
|
||||
)
|
||||
)
|
||||
|
||||
# Remove language since it doesn't apply to wake words yet
|
||||
metadata_dict.pop("language", None)
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.WAKE_WORD_START,
|
||||
{
|
||||
"engine": self.wake_word_engine,
|
||||
"metadata": metadata_dict,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
wake_word_settings = self.wake_word_settings or WakeWordSettings()
|
||||
|
||||
wake_word_vad: VoiceActivityTimeout | None = None
|
||||
if (wake_word_settings.timeout is not None) and (
|
||||
wake_word_settings.timeout > 0
|
||||
):
|
||||
# Use VAD to determine timeout
|
||||
wake_word_vad = VoiceActivityTimeout(wake_word_settings.timeout)
|
||||
|
||||
# Audio chunk buffer.
|
||||
audio_bytes_to_buffer = int(
|
||||
wake_word_settings.audio_seconds_to_buffer * 16000 * 2
|
||||
)
|
||||
audio_ring_buffer = b""
|
||||
|
||||
async def timestamped_stream() -> AsyncIterable[tuple[bytes, int]]:
|
||||
"""Yield audio with timestamps (milliseconds since start of stream)."""
|
||||
nonlocal audio_ring_buffer
|
||||
|
||||
timestamp_ms = 0
|
||||
async for chunk in stream:
|
||||
yield chunk, timestamp_ms
|
||||
timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz
|
||||
|
||||
# Keeping audio right before wake word detection allows the
|
||||
# voice command to be spoken immediately after the wake word.
|
||||
if audio_bytes_to_buffer > 0:
|
||||
audio_ring_buffer += chunk
|
||||
if len(audio_ring_buffer) > audio_bytes_to_buffer:
|
||||
# A proper ring buffer would be far more efficient
|
||||
audio_ring_buffer = audio_ring_buffer[
|
||||
len(audio_ring_buffer) - audio_bytes_to_buffer :
|
||||
]
|
||||
|
||||
if (wake_word_vad is not None) and (not wake_word_vad.process(chunk)):
|
||||
raise WakeWordTimeoutError(
|
||||
code="wake-word-timeout", message="Wake word was not detected"
|
||||
)
|
||||
|
||||
try:
|
||||
# Detect wake word(s)
|
||||
result = await self.wake_word_provider.async_process_audio_stream(
|
||||
timestamped_stream()
|
||||
)
|
||||
|
||||
if audio_ring_buffer:
|
||||
# All audio kept from right before the wake word was detected as
|
||||
# a single chunk.
|
||||
audio_buffer.append(audio_ring_buffer)
|
||||
except WakeWordTimeoutError:
|
||||
_LOGGER.debug("Timeout during wake word detection")
|
||||
raise
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during wake-word-detection")
|
||||
raise WakeWordDetectionError(
|
||||
code="wake-stream-failed",
|
||||
message="Unexpected error during wake-word-detection",
|
||||
) from src_error
|
||||
|
||||
_LOGGER.debug("wake-word-detection result %s", result)
|
||||
|
||||
if result is None:
|
||||
wake_word_output: dict[str, Any] = {}
|
||||
else:
|
||||
if result.queued_audio:
|
||||
# Add audio that was pending at detection
|
||||
for chunk_ts in result.queued_audio:
|
||||
audio_buffer.append(chunk_ts[0])
|
||||
|
||||
wake_word_output = asdict(result)
|
||||
|
||||
# Remove non-JSON fields
|
||||
wake_word_output.pop("queued_audio", None)
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.WAKE_WORD_END,
|
||||
{"wake_word_output": wake_word_output},
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def prepare_speech_to_text(self, metadata: stt.SpeechMetadata) -> None:
|
||||
"""Prepare speech-to-text."""
|
||||
# pipeline.stt_engine can't be None or this function is not called
|
||||
@@ -443,9 +606,21 @@ class PipelineRun:
|
||||
)
|
||||
|
||||
try:
|
||||
segmenter = VoiceCommandSegmenter()
|
||||
|
||||
async def segment_stream(
|
||||
stream: AsyncIterable[bytes],
|
||||
) -> AsyncGenerator[bytes, None]:
|
||||
"""Stop stream when voice command is finished."""
|
||||
async for chunk in stream:
|
||||
if not segmenter.process(chunk):
|
||||
break
|
||||
|
||||
yield chunk
|
||||
|
||||
# Transcribe audio stream
|
||||
result = await self.stt_provider.async_process_audio_stream(
|
||||
metadata, stream
|
||||
metadata, segment_stream(stream)
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during speech-to-text")
|
||||
@@ -663,17 +838,45 @@ class PipelineInput:
|
||||
async def execute(self) -> None:
|
||||
"""Run pipeline."""
|
||||
self.run.start()
|
||||
current_stage = self.run.start_stage
|
||||
current_stage: PipelineStage | None = self.run.start_stage
|
||||
audio_buffer: list[bytes] = []
|
||||
|
||||
try:
|
||||
if current_stage == PipelineStage.WAKE_WORD:
|
||||
assert self.stt_stream is not None
|
||||
detect_result = await self.run.wake_word_detection(
|
||||
self.stt_stream, audio_buffer
|
||||
)
|
||||
if detect_result is None:
|
||||
# No wake word. Abort the rest of the pipeline.
|
||||
self.run.end()
|
||||
return
|
||||
|
||||
current_stage = PipelineStage.STT
|
||||
|
||||
# speech-to-text
|
||||
intent_input = self.intent_input
|
||||
if current_stage == PipelineStage.STT:
|
||||
assert self.stt_metadata is not None
|
||||
assert self.stt_stream is not None
|
||||
|
||||
if audio_buffer:
|
||||
|
||||
async def buffered_stream() -> AsyncGenerator[bytes, None]:
|
||||
for chunk in audio_buffer:
|
||||
yield chunk
|
||||
|
||||
assert self.stt_stream is not None
|
||||
async for chunk in self.stt_stream:
|
||||
yield chunk
|
||||
|
||||
stt_stream = cast(AsyncIterable[bytes], buffered_stream())
|
||||
else:
|
||||
stt_stream = self.stt_stream
|
||||
|
||||
intent_input = await self.run.speech_to_text(
|
||||
self.stt_metadata,
|
||||
self.stt_stream,
|
||||
stt_stream,
|
||||
)
|
||||
current_stage = PipelineStage.INTENT
|
||||
|
||||
@@ -707,7 +910,7 @@ class PipelineInput:
|
||||
|
||||
async def validate(self) -> None:
|
||||
"""Validate pipeline input against start stage."""
|
||||
if self.run.start_stage == PipelineStage.STT:
|
||||
if self.run.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
||||
if self.run.pipeline.stt_engine is None:
|
||||
raise PipelineRunValidationError(
|
||||
"the pipeline does not support speech-to-text"
|
||||
@@ -741,6 +944,13 @@ class PipelineInput:
|
||||
|
||||
prepare_tasks = []
|
||||
|
||||
if (
|
||||
start_stage_index
|
||||
<= PIPELINE_STAGE_ORDER.index(PipelineStage.WAKE_WORD)
|
||||
<= end_stage_index
|
||||
):
|
||||
prepare_tasks.append(self.run.prepare_wake_word_detection())
|
||||
|
||||
if (
|
||||
start_stage_index
|
||||
<= PIPELINE_STAGE_ORDER.index(PipelineStage.STT)
|
||||
|
@@ -88,7 +88,7 @@ class VoiceCommandSegmenter:
|
||||
self.in_command = False
|
||||
|
||||
def process(self, samples: bytes) -> bool:
|
||||
"""Process a 16-bit 16Khz mono audio samples.
|
||||
"""Process 16-bit 16Khz mono audio samples.
|
||||
|
||||
Returns False when command is done.
|
||||
"""
|
||||
@@ -148,3 +148,94 @@ class VoiceCommandSegmenter:
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class VoiceActivityTimeout:
|
||||
"""Detects silence in audio until a timeout is reached."""
|
||||
|
||||
silence_seconds: float
|
||||
"""Seconds of silence before timeout."""
|
||||
|
||||
reset_seconds: float = 0.5
|
||||
"""Seconds of speech before resetting timeout."""
|
||||
|
||||
vad_mode: int = 3
|
||||
"""Aggressiveness in filtering out non-speech. 3 is the most aggressive."""
|
||||
|
||||
vad_frames: int = 480 # 30 ms
|
||||
"""Must be 10, 20, or 30 ms at 16Khz."""
|
||||
|
||||
_silence_seconds_left: float = 0.0
|
||||
"""Seconds left before considering voice command as stopped."""
|
||||
|
||||
_reset_seconds_left: float = 0.0
|
||||
"""Seconds left before resetting start/stop time counters."""
|
||||
|
||||
_vad: webrtcvad.Vad = None
|
||||
_audio_buffer: bytes = field(default_factory=bytes)
|
||||
_bytes_per_chunk: int = 480 * 2 # 16-bit samples
|
||||
_seconds_per_chunk: float = 0.03 # 30 ms
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Initialize VAD."""
|
||||
self._vad = webrtcvad.Vad(self.vad_mode)
|
||||
self._bytes_per_chunk = self.vad_frames * 2
|
||||
self._seconds_per_chunk = self.vad_frames / _SAMPLE_RATE
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all counters and state."""
|
||||
self._audio_buffer = b""
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
|
||||
def process(self, samples: bytes) -> bool:
|
||||
"""Process 16-bit 16Khz mono audio samples.
|
||||
|
||||
Returns False when timeout is reached.
|
||||
"""
|
||||
self._audio_buffer += samples
|
||||
|
||||
# Process in 10, 20, or 30 ms chunks.
|
||||
num_chunks = len(self._audio_buffer) // self._bytes_per_chunk
|
||||
for chunk_idx in range(num_chunks):
|
||||
chunk_offset = chunk_idx * self._bytes_per_chunk
|
||||
chunk = self._audio_buffer[
|
||||
chunk_offset : chunk_offset + self._bytes_per_chunk
|
||||
]
|
||||
if not self._process_chunk(chunk):
|
||||
return False
|
||||
|
||||
if num_chunks > 0:
|
||||
# Remove from buffer
|
||||
self._audio_buffer = self._audio_buffer[
|
||||
num_chunks * self._bytes_per_chunk :
|
||||
]
|
||||
|
||||
return True
|
||||
|
||||
def _process_chunk(self, chunk: bytes) -> bool:
|
||||
"""Process a single chunk of 16-bit 16Khz mono audio.
|
||||
|
||||
Returns False when timeout is reached.
|
||||
"""
|
||||
if self._vad.is_speech(chunk, _SAMPLE_RATE):
|
||||
# Speech
|
||||
self._reset_seconds_left -= self._seconds_per_chunk
|
||||
if self._reset_seconds_left <= 0:
|
||||
# Reset timeout
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
else:
|
||||
# Silence
|
||||
self._silence_seconds_left -= self._seconds_per_chunk
|
||||
if self._silence_seconds_left <= 0:
|
||||
# Timeout reached
|
||||
return False
|
||||
|
||||
# Slowly build reset counter back up
|
||||
self._reset_seconds_left = min(
|
||||
self.reset_seconds, self._reset_seconds_left + self._seconds_per_chunk
|
||||
)
|
||||
|
||||
return True
|
||||
|
@@ -26,11 +26,12 @@ from .pipeline import (
|
||||
PipelineInput,
|
||||
PipelineRun,
|
||||
PipelineStage,
|
||||
WakeWordSettings,
|
||||
async_get_pipeline,
|
||||
)
|
||||
from .vad import VoiceCommandSegmenter
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
DEFAULT_WAKE_WORD_TIMEOUT = 3
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,6 +64,18 @@ def async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
cv.key_value_schemas(
|
||||
"start_stage",
|
||||
{
|
||||
PipelineStage.WAKE_WORD: vol.Schema(
|
||||
{
|
||||
vol.Required("input"): {
|
||||
vol.Required("sample_rate"): int,
|
||||
vol.Optional("timeout"): vol.Any(float, int),
|
||||
vol.Optional("audio_seconds_to_buffer"): vol.Any(
|
||||
float, int
|
||||
),
|
||||
}
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
PipelineStage.STT: vol.Schema(
|
||||
{vol.Required("input"): {vol.Required("sample_rate"): int}},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
@@ -102,6 +115,7 @@ async def websocket_run(
|
||||
end_stage = PipelineStage(msg["end_stage"])
|
||||
handler_id: int | None = None
|
||||
unregister_handler: Callable[[], None] | None = None
|
||||
wake_word_settings: WakeWordSettings | None = None
|
||||
|
||||
# Arguments to PipelineInput
|
||||
input_args: dict[str, Any] = {
|
||||
@@ -109,24 +123,26 @@ async def websocket_run(
|
||||
"device_id": msg.get("device_id"),
|
||||
}
|
||||
|
||||
if start_stage == PipelineStage.STT:
|
||||
if start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
||||
# Audio pipeline that will receive audio as binary websocket messages
|
||||
audio_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
||||
incoming_sample_rate = msg["input"]["sample_rate"]
|
||||
|
||||
if start_stage == PipelineStage.WAKE_WORD:
|
||||
wake_word_settings = WakeWordSettings(
|
||||
timeout=msg["input"].get("timeout", DEFAULT_WAKE_WORD_TIMEOUT),
|
||||
audio_seconds_to_buffer=msg["input"].get("audio_seconds_to_buffer", 0),
|
||||
)
|
||||
|
||||
async def stt_stream() -> AsyncGenerator[bytes, None]:
|
||||
state = None
|
||||
segmenter = VoiceCommandSegmenter()
|
||||
|
||||
# Yield until we receive an empty chunk
|
||||
while chunk := await audio_queue.get():
|
||||
if incoming_sample_rate != 16000:
|
||||
chunk, state = audioop.ratecv(
|
||||
chunk, 2, 1, incoming_sample_rate, 16000, state
|
||||
)
|
||||
if not segmenter.process(chunk):
|
||||
# Voice command is finished
|
||||
break
|
||||
|
||||
yield chunk
|
||||
|
||||
def handle_binary(
|
||||
@@ -169,6 +185,7 @@ async def websocket_run(
|
||||
"stt_binary_handler_id": handler_id,
|
||||
"timeout": timeout,
|
||||
},
|
||||
wake_word_settings=wake_word_settings,
|
||||
)
|
||||
|
||||
pipeline_input = PipelineInput(**input_args)
|
||||
|
@@ -15,9 +15,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
@@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
|
@@ -16,8 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@@ -3,8 +3,10 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import ValuesView
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from yalexs.const import DEFAULT_BRAND
|
||||
@@ -238,14 +240,18 @@ class AugustData(AugustSubscriberMixin):
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_pubnub_message(self, device_id, date_time, message):
|
||||
def async_pubnub_message(
|
||||
self, device_id: str, date_time: datetime, message: dict[str, Any]
|
||||
) -> None:
|
||||
"""Process a pubnub message."""
|
||||
device = self.get_device_detail(device_id)
|
||||
activities = activities_from_pubnub_message(device, date_time, message)
|
||||
activity_stream = self.activity_stream
|
||||
assert activity_stream is not None
|
||||
if activities:
|
||||
self.activity_stream.async_process_newer_device_activities(activities)
|
||||
activity_stream.async_process_newer_device_activities(activities)
|
||||
self.async_signal_device_id_update(device.device_id)
|
||||
self.activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
|
||||
@callback
|
||||
def async_stop(self):
|
||||
|
@@ -1,16 +1,21 @@
|
||||
"""Consume the august activity stream."""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from yalexs.activity import Activity, ActivityType
|
||||
from yalexs.api_async import ApiAsync
|
||||
from yalexs.pubnub_async import AugustPubNub
|
||||
from yalexs.util import get_latest_activity
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import ACTIVITY_UPDATE_INTERVAL
|
||||
from .gateway import AugustGateway
|
||||
from .subscriber import AugustSubscriberMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -18,29 +23,50 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
||||
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
|
||||
|
||||
# If there is a storm of activity (ie lock, unlock, door open, door close, etc)
|
||||
# we want to debounce the updates so we don't hammer the activity api too much.
|
||||
ACTIVITY_DEBOUNCE_COOLDOWN = 3
|
||||
|
||||
|
||||
@callback
|
||||
def _async_cancel_future_scheduled_updates(cancels: list[CALLBACK_TYPE]) -> None:
|
||||
"""Cancel future scheduled updates."""
|
||||
for cancel in cancels:
|
||||
cancel()
|
||||
cancels.clear()
|
||||
|
||||
|
||||
class ActivityStream(AugustSubscriberMixin):
|
||||
"""August activity stream handler."""
|
||||
|
||||
def __init__(self, hass, api, august_gateway, house_ids, pubnub):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api: ApiAsync,
|
||||
august_gateway: AugustGateway,
|
||||
house_ids: set[str],
|
||||
pubnub: AugustPubNub,
|
||||
) -> None:
|
||||
"""Init August activity stream object."""
|
||||
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
|
||||
self._hass = hass
|
||||
self._schedule_updates = {}
|
||||
self._schedule_updates: dict[str, list[CALLBACK_TYPE]] = {}
|
||||
self._august_gateway = august_gateway
|
||||
self._api = api
|
||||
self._house_ids = house_ids
|
||||
self._latest_activities = {}
|
||||
self._last_update_time = None
|
||||
self._latest_activities: dict[str, dict[ActivityType, Activity]] = {}
|
||||
self._did_first_update = False
|
||||
self.pubnub = pubnub
|
||||
self._update_debounce = {}
|
||||
self._update_debounce: dict[str, Debouncer] = {}
|
||||
|
||||
async def async_setup(self):
|
||||
"""Token refresh check and catch up the activity stream."""
|
||||
for house_id in self._house_ids:
|
||||
self._update_debounce[house_id] = self._async_create_debouncer(house_id)
|
||||
|
||||
self._update_debounce = {
|
||||
house_id: self._async_create_debouncer(house_id)
|
||||
for house_id in self._house_ids
|
||||
}
|
||||
await self._async_refresh(utcnow())
|
||||
self._did_first_update = True
|
||||
|
||||
@callback
|
||||
def _async_create_debouncer(self, house_id):
|
||||
@@ -52,7 +78,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
return Debouncer(
|
||||
self._hass,
|
||||
_LOGGER,
|
||||
cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(),
|
||||
cooldown=ACTIVITY_DEBOUNCE_COOLDOWN,
|
||||
immediate=True,
|
||||
function=_async_update_house_id,
|
||||
)
|
||||
@@ -62,73 +88,73 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
"""Cleanup any debounces."""
|
||||
for debouncer in self._update_debounce.values():
|
||||
debouncer.async_cancel()
|
||||
for house_id, updater in self._schedule_updates.items():
|
||||
if updater is not None:
|
||||
updater()
|
||||
self._schedule_updates[house_id] = None
|
||||
for cancels in self._schedule_updates.values():
|
||||
_async_cancel_future_scheduled_updates(cancels)
|
||||
|
||||
def get_latest_device_activity(self, device_id, activity_types):
|
||||
def get_latest_device_activity(
|
||||
self, device_id: str, activity_types: set[ActivityType]
|
||||
) -> Activity | None:
|
||||
"""Return latest activity that is one of the activity_types."""
|
||||
if device_id not in self._latest_activities:
|
||||
if not (latest_device_activities := self._latest_activities.get(device_id)):
|
||||
return None
|
||||
|
||||
latest_device_activities = self._latest_activities[device_id]
|
||||
latest_activity = None
|
||||
latest_activity: Activity | None = None
|
||||
|
||||
for activity_type in activity_types:
|
||||
if activity_type in latest_device_activities:
|
||||
if activity := latest_device_activities.get(activity_type):
|
||||
if (
|
||||
latest_activity is not None
|
||||
and latest_device_activities[activity_type].activity_start_time
|
||||
latest_activity
|
||||
and activity.activity_start_time
|
||||
<= latest_activity.activity_start_time
|
||||
):
|
||||
continue
|
||||
latest_activity = latest_device_activities[activity_type]
|
||||
latest_activity = activity
|
||||
|
||||
return latest_activity
|
||||
|
||||
async def _async_refresh(self, time):
|
||||
async def _async_refresh(self, time: datetime) -> None:
|
||||
"""Update the activity stream from August."""
|
||||
# This is the only place we refresh the api token
|
||||
await self._august_gateway.async_refresh_access_token_if_needed()
|
||||
if self.pubnub.connected:
|
||||
_LOGGER.debug("Skipping update because pubnub is connected")
|
||||
return
|
||||
await self._async_update_device_activities(time)
|
||||
|
||||
async def _async_update_device_activities(self, time):
|
||||
_LOGGER.debug("Start retrieving device activities")
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self._update_debounce[house_id].async_call()
|
||||
for house_id in self._house_ids
|
||||
*(debouncer.async_call() for debouncer in self._update_debounce.values())
|
||||
)
|
||||
)
|
||||
self._last_update_time = time
|
||||
|
||||
@callback
|
||||
def async_schedule_house_id_refresh(self, house_id):
|
||||
def async_schedule_house_id_refresh(self, house_id: str) -> None:
|
||||
"""Update for a house activities now and once in the future."""
|
||||
if self._schedule_updates.get(house_id):
|
||||
self._schedule_updates[house_id]()
|
||||
self._schedule_updates[house_id] = None
|
||||
if cancels := self._schedule_updates.get(house_id):
|
||||
_async_cancel_future_scheduled_updates(cancels)
|
||||
|
||||
async def _update_house_activities(_):
|
||||
await self._update_debounce[house_id].async_call()
|
||||
debouncer = self._update_debounce[house_id]
|
||||
|
||||
self._hass.async_create_task(self._update_debounce[house_id].async_call())
|
||||
# Schedule an update past the debounce to ensure
|
||||
# we catch the case where the lock operator is
|
||||
# not updated or the lock failed
|
||||
self._schedule_updates[house_id] = async_call_later(
|
||||
self._hass.async_create_task(debouncer.async_call())
|
||||
# Schedule two updates past the debounce time
|
||||
# to ensure we catch the case where the activity
|
||||
# api does not update right away and we need to poll
|
||||
# it again. Sometimes the lock operator or a doorbell
|
||||
# will not show up in the activity stream right away.
|
||||
future_updates = self._schedule_updates.setdefault(house_id, [])
|
||||
|
||||
async def _update_house_activities(now: datetime) -> None:
|
||||
await debouncer.async_call()
|
||||
|
||||
for step in (1, 2):
|
||||
future_updates.append(
|
||||
async_call_later(
|
||||
self._hass,
|
||||
ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1,
|
||||
(step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1,
|
||||
_update_house_activities,
|
||||
)
|
||||
)
|
||||
|
||||
async def _async_update_house_id(self, house_id):
|
||||
async def _async_update_house_id(self, house_id: str) -> None:
|
||||
"""Update device activities for a house."""
|
||||
if self._last_update_time:
|
||||
if self._did_first_update:
|
||||
limit = ACTIVITY_STREAM_FETCH_LIMIT
|
||||
else:
|
||||
limit = ACTIVITY_CATCH_UP_FETCH_LIMIT
|
||||
@@ -150,36 +176,34 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
_LOGGER.debug(
|
||||
"Completed retrieving device activities for house id %s", house_id
|
||||
)
|
||||
|
||||
updated_device_ids = self.async_process_newer_device_activities(activities)
|
||||
|
||||
if not updated_device_ids:
|
||||
return
|
||||
|
||||
for device_id in updated_device_ids:
|
||||
for device_id in self.async_process_newer_device_activities(activities):
|
||||
_LOGGER.debug(
|
||||
"async_signal_device_id_update (from activity stream): %s",
|
||||
device_id,
|
||||
)
|
||||
self.async_signal_device_id_update(device_id)
|
||||
|
||||
def async_process_newer_device_activities(self, activities):
|
||||
def async_process_newer_device_activities(
|
||||
self, activities: list[Activity]
|
||||
) -> set[str]:
|
||||
"""Process activities if they are newer than the last one."""
|
||||
updated_device_ids = set()
|
||||
latest_activities = self._latest_activities
|
||||
for activity in activities:
|
||||
device_id = activity.device_id
|
||||
activity_type = activity.activity_type
|
||||
device_activities = self._latest_activities.setdefault(device_id, {})
|
||||
device_activities = latest_activities.setdefault(device_id, {})
|
||||
# Ignore activities that are older than the latest one unless it is a non
|
||||
# locking or unlocking activity with the exact same start time.
|
||||
if (
|
||||
get_latest_activity(activity, device_activities.get(activity_type))
|
||||
!= activity
|
||||
):
|
||||
last_activity = device_activities.get(activity_type)
|
||||
# The activity stream can have duplicate activities. So we need
|
||||
# to call get_latest_activity to figure out if if the activity
|
||||
# is actually newer than the last one.
|
||||
latest_activity = get_latest_activity(activity, last_activity)
|
||||
if latest_activity != activity:
|
||||
continue
|
||||
|
||||
device_activities[activity_type] = activity
|
||||
|
||||
updated_device_ids.add(device_id)
|
||||
|
||||
return updated_device_ids
|
||||
|
@@ -13,7 +13,7 @@ from yalexs.activity import (
|
||||
ActivityType,
|
||||
)
|
||||
from yalexs.doorbell import Doorbell, DoorbellDetail
|
||||
from yalexs.lock import Lock, LockDoorStatus
|
||||
from yalexs.lock import Lock, LockDetail, LockDoorStatus
|
||||
from yalexs.util import update_lock_detail_from_activity
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -39,13 +39,16 @@ TIME_TO_RECHECK_DETECTION = timedelta(
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_online_state(data: AugustData, detail: DoorbellDetail) -> bool:
|
||||
def _retrieve_online_state(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
# The doorbell will go into standby mode when there is no motion
|
||||
# for a short while. It will wake by itself when needed so we need
|
||||
# to consider is available or we will not report motion or dings
|
||||
|
||||
if isinstance(detail, DoorbellDetail):
|
||||
return detail.is_online or detail.is_standby
|
||||
return detail.bridge_is_online
|
||||
|
||||
|
||||
def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool:
|
||||
@@ -72,7 +75,7 @@ def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> b
|
||||
return _activity_time_based_state(latest)
|
||||
|
||||
|
||||
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool:
|
||||
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool:
|
||||
assert data.activity_stream is not None
|
||||
latest = data.activity_stream.get_latest_device_activity(
|
||||
detail.device_id, {ActivityType.DOORBELL_DING}
|
||||
@@ -135,15 +138,7 @@ SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription(
|
||||
name="Open",
|
||||
)
|
||||
|
||||
|
||||
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_ding",
|
||||
name="Ding",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
value_fn=_retrieve_ding_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_motion",
|
||||
name="Motion",
|
||||
@@ -169,6 +164,17 @@ SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] =
|
||||
)
|
||||
|
||||
|
||||
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="doorbell_ding",
|
||||
name="Ding",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
value_fn=_retrieve_ding_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -193,8 +199,17 @@ async def async_setup_entry(
|
||||
_LOGGER.debug("Adding sensor class door for %s", door.device_name)
|
||||
entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR))
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
if detail.doorbell:
|
||||
for description in SENSOR_TYPES_DOORBELL:
|
||||
_LOGGER.debug(
|
||||
"Adding doorbell sensor class %s for %s",
|
||||
description.device_class,
|
||||
door.device_name,
|
||||
)
|
||||
entities.append(AugustDoorbellBinarySensor(data, door, description))
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL:
|
||||
_LOGGER.debug(
|
||||
"Adding doorbell sensor class %s for %s",
|
||||
description.device_class,
|
||||
@@ -261,7 +276,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
data: AugustData,
|
||||
device: Doorbell,
|
||||
device: Doorbell | Lock,
|
||||
description: AugustDoorbellBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
@@ -6,7 +6,8 @@ from yalexs.lock import Lock
|
||||
from yalexs.util import get_configuration_url
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import DOMAIN, AugustData
|
||||
from .const import MANUFACTURER
|
||||
|
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.3"]
|
||||
"requirements": ["yalexs==1.5.2", "yalexs-ble==2.2.3"]
|
||||
}
|
||||
|
@@ -1,25 +1,30 @@
|
||||
"""Base class for August entity."""
|
||||
|
||||
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
|
||||
class AugustSubscriberMixin:
|
||||
"""Base implementation for a subscriber."""
|
||||
|
||||
def __init__(self, hass, update_interval):
|
||||
def __init__(self, hass: HomeAssistant, update_interval: timedelta) -> None:
|
||||
"""Initialize an subscriber."""
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
self._update_interval = update_interval
|
||||
self._subscriptions = {}
|
||||
self._unsub_interval = None
|
||||
self._stop_interval = None
|
||||
self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {}
|
||||
self._unsub_interval: CALLBACK_TYPE | None = None
|
||||
self._stop_interval: CALLBACK_TYPE | None = None
|
||||
|
||||
@callback
|
||||
def async_subscribe_device_id(self, device_id, update_callback):
|
||||
def async_subscribe_device_id(
|
||||
self, device_id: str, update_callback: CALLBACK_TYPE
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Add an callback subscriber.
|
||||
|
||||
Returns a callable that can be used to unsubscribe.
|
||||
@@ -34,8 +39,12 @@ class AugustSubscriberMixin:
|
||||
|
||||
return _unsubscribe
|
||||
|
||||
@abstractmethod
|
||||
async def _async_refresh(self, time: datetime) -> None:
|
||||
"""Refresh data."""
|
||||
|
||||
@callback
|
||||
def _async_setup_listeners(self):
|
||||
def _async_setup_listeners(self) -> None:
|
||||
"""Create interval and stop listeners."""
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self._hass,
|
||||
@@ -54,7 +63,9 @@ class AugustSubscriberMixin:
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_unsubscribe_device_id(self, device_id, update_callback):
|
||||
def async_unsubscribe_device_id(
|
||||
self, device_id: str, update_callback: CALLBACK_TYPE
|
||||
) -> None:
|
||||
"""Remove a callback subscriber."""
|
||||
self._subscriptions[device_id].remove(update_callback)
|
||||
if not self._subscriptions[device_id]:
|
||||
@@ -63,6 +74,7 @@ class AugustSubscriberMixin:
|
||||
if self._subscriptions:
|
||||
return
|
||||
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
if self._stop_interval:
|
||||
@@ -70,7 +82,7 @@ class AugustSubscriberMixin:
|
||||
self._stop_interval = None
|
||||
|
||||
@callback
|
||||
def async_signal_device_id_update(self, device_id):
|
||||
def async_signal_device_id_update(self, device_id: str) -> None:
|
||||
"""Call the callbacks for a device_id."""
|
||||
if not self._subscriptions.get(device_id):
|
||||
return
|
||||
|
@@ -7,10 +7,7 @@ from aiohttp import ClientError
|
||||
from auroranoaa import AuroraForecast
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -2,16 +2,10 @@
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .coordinator import AuroraDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -7,7 +7,8 @@ from typing import Any
|
||||
|
||||
from aurorapy.client import AuroraSerialClient
|
||||
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
ATTR_DEVICE_NAME,
|
||||
|
@@ -15,8 +15,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfInformation, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
@@ -13,11 +13,7 @@ from python_awair.devices import AwairBaseDevice, AwairLocalDevice
|
||||
from python_awair.exceptions import AuthError, AwairError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_HOST,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
@@ -27,7 +27,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
@@ -5,8 +5,9 @@ from abc import abstractmethod
|
||||
from axis.models.event import Event, EventTopic
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
from .device import AxisNetworkDevice
|
||||
|
@@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
|
@@ -5,8 +5,8 @@ from aiobafi6 import Device
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
class BAFEntity(Entity):
|
||||
|
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
|
||||
from pybalboa import EVENT_UPDATE, SpaClient
|
||||
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@@ -12,7 +12,8 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT
|
||||
|
||||
|
@@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
|
@@ -9,7 +9,7 @@ from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER
|
||||
|
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
|
||||
|
@@ -637,7 +637,7 @@ class BluetoothManager:
|
||||
else:
|
||||
# We could write out every item in the typed dict here
|
||||
# but that would be a bit inefficient and verbose.
|
||||
callback_matcher.update(matcher) # type: ignore[typeddict-item]
|
||||
callback_matcher.update(matcher)
|
||||
callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
|
||||
|
||||
connectable = callback_matcher[CONNECTABLE]
|
||||
|
@@ -18,7 +18,7 @@
|
||||
"bleak-retry-connector==3.1.1",
|
||||
"bluetooth-adapters==0.16.0",
|
||||
"bluetooth-auto-recovery==1.2.1",
|
||||
"bluetooth-data-tools==1.7.0",
|
||||
"dbus-fast==1.90.1"
|
||||
"bluetooth-data-tools==1.8.0",
|
||||
"dbus-fast==1.91.2"
|
||||
]
|
||||
}
|
||||
|
@@ -16,14 +16,9 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import (
|
||||
DeviceInfo,
|
||||
Entity,
|
||||
EntityDescription,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_platform import async_get_current_platform
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
@@ -12,14 +12,8 @@ from .api import (
|
||||
async_register_callback,
|
||||
async_track_unavailable,
|
||||
)
|
||||
from .match import (
|
||||
BluetoothCallbackMatcher,
|
||||
)
|
||||
from .models import (
|
||||
BluetoothChange,
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
)
|
||||
from .match import BluetoothCallbackMatcher
|
||||
from .models import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
|
||||
|
||||
|
||||
class BasePassiveBluetoothCoordinator(ABC):
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platf
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import discovery, entity_registry as er
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user