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

This commit is contained in:
Jack Boswell
2023-08-12 12:01:30 +12:00
committed by GitHub
1088 changed files with 21901 additions and 5793 deletions

View File

@@ -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/**

View File

@@ -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

View File

@@ -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 \

View File

@@ -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.*

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 (

View File

@@ -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(

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2022.12.1"]
"requirements": ["pyairvisual==2023.08.1"]
}

View File

@@ -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,

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2022.12.1"]
"requirements": ["pyairvisual==2023.08.1"]
}

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"),

View File

@@ -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():

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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}",

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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__)

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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",

View File

@@ -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)

View File

@@ -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

View File

@@ -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():
chunk, state = audioop.ratecv(
chunk, 2, 1, incoming_sample_rate, 16000, state
)
if not segmenter.process(chunk):
# Voice command is finished
break
if incoming_sample_rate != 16000:
chunk, state = audioop.ratecv(
chunk, 2, 1, incoming_sample_rate, 16000, state
)
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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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):

View File

@@ -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,
ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1,
_update_house_activities,
)
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 _async_update_house_id(self, 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,
(step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1,
_update_house_activities,
)
)
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

View File

@@ -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
return detail.is_online or detail.is_standby
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))
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:
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."""

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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,14 +74,15 @@ class AugustSubscriberMixin:
if self._subscriptions:
return
self._unsub_interval()
self._unsub_interval = None
if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
if self._stop_interval:
self._stop_interval()
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

View File

@@ -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__)

View File

@@ -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__)

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 (

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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"
]
}

View File

@@ -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

View File

@@ -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):

View File

@@ -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