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/lovelace/**
- homeassistant/components/media_source/** - homeassistant/components/media_source/**
- homeassistant/components/mjpeg/** - homeassistant/components/mjpeg/**
- homeassistant/components/modbus/**
- homeassistant/components/mqtt/** - homeassistant/components/mqtt/**
- homeassistant/components/network/** - homeassistant/components/network/**
- homeassistant/components/onboarding/** - homeassistant/components/onboarding/**

View File

@@ -302,8 +302,11 @@ omit =
homeassistant/components/enocean/sensor.py homeassistant/components/enocean/sensor.py
homeassistant/components/enocean/switch.py homeassistant/components/enocean/switch.py
homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/__init__.py
homeassistant/components/enphase_envoy/binary_sensor.py
homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/coordinator.py
homeassistant/components/enphase_envoy/entity.py
homeassistant/components/enphase_envoy/sensor.py homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/enphase_envoy/switch.py
homeassistant/components/entur_public_transport/* homeassistant/components/entur_public_transport/*
homeassistant/components/environment_canada/__init__.py homeassistant/components/environment_canada/__init__.py
homeassistant/components/environment_canada/camera.py homeassistant/components/environment_canada/camera.py
@@ -419,6 +422,7 @@ omit =
homeassistant/components/garadget/cover.py homeassistant/components/garadget/cover.py
homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/binary_sensor.py
homeassistant/components/garages_amsterdam/entity.py
homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/garages_amsterdam/sensor.py
homeassistant/components/gc100/* homeassistant/components/gc100/*
homeassistant/components/geniushub/* homeassistant/components/geniushub/*
@@ -772,6 +776,7 @@ omit =
homeassistant/components/neato/sensor.py homeassistant/components/neato/sensor.py
homeassistant/components/neato/switch.py homeassistant/components/neato/switch.py
homeassistant/components/neato/vacuum.py homeassistant/components/neato/vacuum.py
homeassistant/components/neato/button.py
homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/netdata/sensor.py homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/__init__.py homeassistant/components/netgear/__init__.py
@@ -1342,6 +1347,7 @@ omit =
homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/__init__.py
homeassistant/components/trafikverket_train/coordinator.py homeassistant/components/trafikverket_train/coordinator.py
homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_train/util.py
homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/__init__.py
homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/coordinator.py
homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py

View File

@@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.06.1 uses: home-assistant/builder@2023.08.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@@ -251,6 +251,7 @@ jobs:
- raspberrypi4-64 - raspberrypi4-64
- tinker - tinker
- yellow - yellow
- green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.5.3
@@ -274,7 +275,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.06.1 uses: home-assistant/builder@2023.08.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \

View File

@@ -53,6 +53,7 @@ homeassistant.components.airzone_cloud.*
homeassistant.components.aladdin_connect.* homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.* homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.* homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.ambient_station.* homeassistant.components.ambient_station.*
homeassistant.components.amcrest.* homeassistant.components.amcrest.*

View File

@@ -343,8 +343,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/enigma2/ @fbradyirl /homeassistant/components/enigma2/ @fbradyirl
/homeassistant/components/enocean/ @bdurrer /homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @gtdiehl /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
/tests/components/enphase_envoy/ @gtdiehl /tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
/homeassistant/components/entur_public_transport/ @hfurubotten /homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/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 /tests/components/vulcan/ @Antoni-Czaplicki
/homeassistant/components/wake_on_lan/ @ntilley905 /homeassistant/components/wake_on_lan/ @ntilley905
/tests/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 /homeassistant/components/wallbox/ @hesselonline
/tests/components/wallbox/ @hesselonline /tests/components/wallbox/ @hesselonline
/homeassistant/components/waqi/ @andrey-git /homeassistant/components/waqi/ @andrey-git

View File

@@ -28,6 +28,7 @@ from homeassistant.const import (
from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
@@ -320,9 +321,9 @@ class AbodeDevice(AbodeEntity):
} }
@property @property
def device_info(self) -> entity.DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity.""" """Return device registry information for this entity."""
return entity.DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device.id)}, identifiers={(DOMAIN, self._device.id)},
manufacturer="Abode", manufacturer="Abode",
model=self._device.type, 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER

View File

@@ -68,9 +68,6 @@ class AccuWeatherEntity(
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) 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_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_pressure_unit = UnitOfPressure.HPA
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS

View File

@@ -74,9 +74,9 @@ class AcmedaBase(entity.Entity):
return self.roller.id return self.roller.id
@property @property
def device_info(self) -> entity.DeviceInfo: def device_info(self) -> dr.DeviceInfo:
"""Return the device info.""" """Return the device info."""
return entity.DeviceInfo( return dr.DeviceInfo(
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Rollease Acmeda", manufacturer="Rollease Acmeda",
name=self.roller.name, name=self.roller.name,

View File

@@ -23,7 +23,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.entity_platform import AddEntitiesCallback
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL 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 adguardhome import AdGuardHome, AdGuardHomeError
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER

View File

@@ -4,7 +4,7 @@ from typing import Any
from advantage_air import ApiError from advantage_air import ApiError
from homeassistant.exceptions import HomeAssistantError 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN 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.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN 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.components.update import UpdateEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
) )
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN 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.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
AddEntitiesCallback, AddEntitiesCallback,
async_get_current_platform, async_get_current_platform,

View File

@@ -20,8 +20,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity

View File

@@ -17,8 +17,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL

View File

@@ -21,7 +21,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (

View File

@@ -34,8 +34,12 @@ class Discovery:
def get_name(device: AirthingsDevice) -> str: def get_name(device: AirthingsDevice) -> str:
"""Generate name with identifier for device.""" """Generate name with model and identifier for device."""
return f"{device.name} ({device.identifier})"
name = device.friendly_name()
if identifier := device.identifier:
name += f" ({identifier})"
return name
class AirthingsDeviceUpdateError(Exception): class AirthingsDeviceUpdateError(Exception):
@@ -156,7 +160,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")
titles = { titles = {
address: get_name(discovery.device) address: discovery.device.name
for (address, discovery) in self._discovered_devices.items() for (address, discovery) in self._discovered_devices.items()
} }
return self.async_show_form( return self.async_show_form(

View File

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling", "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, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
@@ -162,10 +161,11 @@ class AirthingsSensor(
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = entity_description 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._attr_unique_id = f"{name}_{entity_description.key}"
self._id = airthings_device.address self._id = airthings_device.address
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
connections={ connections={
@@ -175,9 +175,10 @@ class AirthingsSensor(
) )
}, },
name=name, name=name,
manufacturer="Airthings", manufacturer=airthings_device.manufacturer,
hw_version=airthings_device.hw_version, hw_version=airthings_device.hw_version,
sw_version=airthings_device.sw_version, sw_version=airthings_device.sw_version,
model=airthings_device.model,
) )
@property @property

View File

@@ -18,7 +18,7 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity

View File

@@ -8,5 +8,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"], "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.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady 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 ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,

View File

@@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyairvisual", "pysmb"], "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.config_entries import ConfigEntry
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER from .const import DOMAIN, MANUFACTURER

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.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 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER 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.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady 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 homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aladdin_connect"], "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.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN

View File

@@ -1,15 +1,16 @@
"""Support for Alexa skill auth.""" """Support for Alexa skill auth."""
import asyncio import asyncio
from datetime import timedelta from datetime import datetime, timedelta
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging import logging
from typing import Any
import aiohttp import aiohttp
import async_timeout import async_timeout
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -30,24 +31,24 @@ STORAGE_REFRESH_TOKEN = "refresh_token"
class Auth: class Auth:
"""Handle authentication to send events to Alexa.""" """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.""" """Initialize the Auth class."""
self.hass = hass self.hass = hass
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
self._prefs = None self._prefs: dict[str, Any] | None = None
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._get_token_lock = asyncio.Lock() 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.""" """Do authentication with an AcceptGrant code."""
# access token not retrieved yet for the first time, so this should # access token not retrieved yet for the first time, so this should
# be an access token request # be an access token request
lwa_params = { lwa_params: dict[str, str] = {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": accept_grant_code, "code": accept_grant_code,
CONF_CLIENT_ID: self.client_id, CONF_CLIENT_ID: self.client_id,
@@ -61,25 +62,28 @@ class Auth:
return await self._async_request_new_token(lwa_params) return await self._async_request_new_token(lwa_params)
@callback @callback
def async_invalidate_access_token(self): def async_invalidate_access_token(self) -> None:
"""Invalidate access token.""" """Invalidate access token."""
assert self._prefs is not None
self._prefs[STORAGE_ACCESS_TOKEN] = 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.""" """Perform access token or token refresh request."""
async with self._get_token_lock: async with self._get_token_lock:
if self._prefs is None: if self._prefs is None:
await self.async_load_preferences() await self.async_load_preferences()
assert self._prefs is not None
if self.is_token_valid(): if self.is_token_valid():
_LOGGER.debug("Token still valid, using it") _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: if self._prefs[STORAGE_REFRESH_TOKEN] is None:
_LOGGER.debug("Token invalid and no refresh token available") _LOGGER.debug("Token invalid and no refresh token available")
return None return None
lwa_params = { lwa_params: dict[str, str] = {
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN], "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
CONF_CLIENT_ID: self.client_id, CONF_CLIENT_ID: self.client_id,
@@ -90,19 +94,23 @@ class Auth:
return await self._async_request_new_token(lwa_params) return await self._async_request_new_token(lwa_params)
@callback @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.""" """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]: if not self._prefs[STORAGE_ACCESS_TOKEN]:
return False 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( preemptive_expire_time = expire_time - timedelta(
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
) )
return dt_util.utcnow() < preemptive_expire_time 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: try:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
async with async_timeout.timeout(10): async with async_timeout.timeout(10):
@@ -127,9 +135,9 @@ class Auth:
response_json = await response.json() response_json = await response.json()
_LOGGER.debug("LWA response body : %s", response_json) _LOGGER.debug("LWA response body : %s", response_json)
access_token = response_json["access_token"] access_token: str = response_json["access_token"]
refresh_token = response_json["refresh_token"] refresh_token: str = response_json["refresh_token"]
expires_in = response_json["expires_in"] expires_in: int = response_json["expires_in"]
expire_time = dt_util.utcnow() + timedelta(seconds=expires_in) expire_time = dt_util.utcnow() + timedelta(seconds=expires_in)
await self._async_update_preferences( await self._async_update_preferences(
@@ -138,7 +146,7 @@ class Auth:
return access_token return access_token
async def async_load_preferences(self): async def async_load_preferences(self) -> None:
"""Load preferences with stored tokens.""" """Load preferences with stored tokens."""
self._prefs = await self._store.async_load() self._prefs = await self._store.async_load()
@@ -149,10 +157,13 @@ class Auth:
STORAGE_EXPIRE_TIME: None, 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.""" """Update user preferences."""
if self._prefs is None: if self._prefs is None:
await self.async_load_preferences() await self.async_load_preferences()
assert self._prefs is not None
if access_token is not None: if access_token is not None:
self._prefs[STORAGE_ACCESS_TOKEN] = access_token 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 from abc import ABC, abstractmethod
import asyncio import asyncio
import logging import logging
from typing import Any
from yarl import URL
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
@@ -33,38 +36,38 @@ class AbstractConfig(ABC):
await self._store.async_load() await self._store.async_load()
@property @property
def supports_auth(self): def supports_auth(self) -> bool:
"""Return if config supports auth.""" """Return if config supports auth."""
return False return False
@property @property
def should_report_state(self): def should_report_state(self) -> bool:
"""Return if states should be proactively reported.""" """Return if states should be proactively reported."""
return False return False
@property @property
def endpoint(self): @abstractmethod
def endpoint(self) -> str | URL | None:
"""Endpoint for report state.""" """Endpoint for report state."""
return None
@property @property
@abstractmethod @abstractmethod
def locale(self): def locale(self) -> str | None:
"""Return config locale.""" """Return config locale."""
@property @property
def entity_config(self): def entity_config(self) -> dict[str, Any]:
"""Return entity config.""" """Return entity config."""
return {} return {}
@property @property
def is_reporting_states(self): def is_reporting_states(self) -> bool:
"""Return if proactive mode is enabled.""" """Return if proactive mode is enabled."""
return self._unsub_proactive_report is not None return self._unsub_proactive_report is not None
@callback @callback
@abstractmethod @abstractmethod
def user_identifier(self): def user_identifier(self) -> str:
"""Return an identifier for the user that represents this config.""" """Return an identifier for the user that represents this config."""
async def async_enable_proactive_mode(self) -> None: async def async_enable_proactive_mode(self) -> None:
@@ -85,29 +88,29 @@ class AbstractConfig(ABC):
self._unsub_proactive_report = None self._unsub_proactive_report = None
@callback @callback
def should_expose(self, entity_id): def should_expose(self, entity_id: str) -> bool:
"""If an entity should be exposed.""" """If an entity should be exposed."""
return False return False
@callback @callback
def async_invalidate_access_token(self): def async_invalidate_access_token(self) -> None:
"""Invalidate access token.""" """Invalidate access token."""
raise NotImplementedError raise NotImplementedError
async def async_get_access_token(self): async def async_get_access_token(self) -> str | None:
"""Get an access token.""" """Get an access token."""
raise NotImplementedError raise NotImplementedError
async def async_accept_grant(self, code): async def async_accept_grant(self, code: str) -> str | None:
"""Accept a grant.""" """Accept a grant."""
raise NotImplementedError raise NotImplementedError
@property @property
def authorized(self): def authorized(self) -> bool:
"""Return authorization status.""" """Return authorization status."""
return self._store.authorized return self._store.authorized
async def set_authorized(self, authorized) -> None: async def set_authorized(self, authorized: bool) -> None:
"""Set authorization status. """Set authorization status.
- Set when an incoming message is received from Alexa. - Set when an incoming message is received from Alexa.
@@ -132,25 +135,26 @@ class AlexaConfigStore:
_STORAGE_VERSION = 1 _STORAGE_VERSION = 1
_STORAGE_KEY = DOMAIN _STORAGE_KEY = DOMAIN
def __init__(self, hass): def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a configuration store.""" """Initialize a configuration store."""
self._data = None self._data: dict[str, Any] | None = None
self._hass = hass 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 @property
def authorized(self): def authorized(self) -> bool:
"""Return authorization status.""" """Return authorization status."""
return self._data[STORE_AUTHORIZED] assert self._data is not None
return bool(self._data[STORE_AUTHORIZED])
@callback @callback
def set_authorized(self, authorized): def set_authorized(self, authorized: bool) -> None:
"""Set authorization status.""" """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._data[STORE_AUTHORIZED] = authorized
self._store.async_delay_save(lambda: self._data, 1.0) 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.""" """Load saved configuration from disk."""
if data := await self._store.async_load(): if data := await self._store.async_load():
self._data = data 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 # 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 # reverse mapping of this dict and we want to map the first occurrence of OFF
# back to HA state. # back to HA state.
API_THERMOSTAT_MODES = OrderedDict( API_THERMOSTAT_MODES: OrderedDict[str, str] = OrderedDict(
[ [
(climate.HVACMode.HEAT, "HEAT"), (climate.HVACMode.HEAT, "HEAT"),
(climate.HVACMode.COOL, "COOL"), (climate.HVACMode.COOL, "COOL"),

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Generator, Iterable from collections.abc import Generator, Iterable
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from homeassistant.components import ( from homeassistant.components import (
alarm_control_panel, alarm_control_panel,
@@ -274,22 +274,23 @@ class AlexaEntity:
self.entity_conf = config.entity_config.get(entity.entity_id, {}) self.entity_conf = config.entity_config.get(entity.entity_id, {})
@property @property
def entity_id(self): def entity_id(self) -> str:
"""Return the Entity ID.""" """Return the Entity ID."""
return self.entity.entity_id return self.entity.entity_id
def friendly_name(self): def friendly_name(self) -> str:
"""Return the Alexa API friendly name.""" """Return the Alexa API friendly name."""
return self.entity_conf.get(CONF_NAME, self.entity.name).translate( friendly_name: str = self.entity_conf.get(
TRANSLATION_TABLE CONF_NAME, self.entity.name
) ).translate(TRANSLATION_TABLE)
return friendly_name
def description(self): def description(self) -> str:
"""Return the Alexa API description.""" """Return the Alexa API description."""
description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id
return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) 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 the Alexa API entity id."""
return generate_alexa_id(self.entity.entity_id) return generate_alexa_id(self.entity.entity_id)
@@ -317,7 +318,7 @@ class AlexaEntity:
""" """
raise NotImplementedError raise NotImplementedError
def serialize_properties(self): def serialize_properties(self) -> Generator[dict[str, Any], None, None]:
"""Yield each supported property in API format.""" """Yield each supported property in API format."""
for interface in self.interfaces(): for interface in self.interfaces():
if not interface.properties_proactively_reported(): if not interface.properties_proactively_reported():
@@ -325,9 +326,9 @@ class AlexaEntity:
yield from interface.serialize_properties() yield from interface.serialize_properties()
def serialize_discovery(self): def serialize_discovery(self) -> dict[str, Any]:
"""Serialize the entity for discovery.""" """Serialize the entity for discovery."""
result = { result: dict[str, Any] = {
"displayCategories": self.display_categories(), "displayCategories": self.display_categories(),
"cookie": {}, "cookie": {},
"endpointId": self.alexa_id(), "endpointId": self.alexa_id(),
@@ -366,7 +367,7 @@ def async_get_entities(
hass: HomeAssistant, config: AbstractConfig hass: HomeAssistant, config: AbstractConfig
) -> list[AlexaEntity]: ) -> list[AlexaEntity]:
"""Return all entities that are supported by Alexa.""" """Return all entities that are supported by Alexa."""
entities = [] entities: list[AlexaEntity] = []
for state in hass.states.async_all(): for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue continue
@@ -725,7 +726,7 @@ class MediaPlayerCapabilities(AlexaEntity):
class SceneCapabilities(AlexaEntity): class SceneCapabilities(AlexaEntity):
"""Class to represent Scene capabilities.""" """Class to represent Scene capabilities."""
def description(self): def description(self) -> str:
"""Return the Alexa API description.""" """Return the Alexa API description."""
description = AlexaEntity.description(self) description = AlexaEntity.description(self)
if "scene" not in description.casefold(): if "scene" not in description.casefold():

View File

@@ -1,8 +1,9 @@
"""Alexa related errors.""" """Alexa related errors."""
from __future__ import annotations from __future__ import annotations
from typing import Literal from typing import Any, Literal
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .const import API_TEMP_UNITS from .const import API_TEMP_UNITS
@@ -29,7 +30,9 @@ class AlexaError(Exception):
namespace: str | None = None namespace: str | None = None
error_type: 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.""" """Initialize an alexa error."""
Exception.__init__(self) Exception.__init__(self)
self.error_message = error_message self.error_message = error_message
@@ -42,7 +45,7 @@ class AlexaInvalidEndpointError(AlexaError):
namespace = "Alexa" namespace = "Alexa"
error_type = "NO_SUCH_ENDPOINT" error_type = "NO_SUCH_ENDPOINT"
def __init__(self, endpoint_id): def __init__(self, endpoint_id: str) -> None:
"""Initialize invalid endpoint error.""" """Initialize invalid endpoint error."""
msg = f"The endpoint {endpoint_id} does not exist" msg = f"The endpoint {endpoint_id} does not exist"
AlexaError.__init__(self, msg) AlexaError.__init__(self, msg)
@@ -93,7 +96,9 @@ class AlexaTempRangeError(AlexaError):
namespace = "Alexa" namespace = "Alexa"
error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE" 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.""" """Initialize TempRange error."""
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
temp_range = { temp_range = {

View File

@@ -4,10 +4,13 @@ from http import HTTPStatus
import logging import logging
import uuid import uuid
from aiohttp.web_response import StreamResponse
from homeassistant.components import http from homeassistant.components import http
from homeassistant.const import CONF_PASSWORD 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 import template
from homeassistant.helpers.typing import ConfigType
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import ( from .const import (
@@ -32,7 +35,7 @@ FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
@callback @callback
def async_setup(hass, flash_briefing_config): def async_setup(hass: HomeAssistant, flash_briefing_config: ConfigType) -> None:
"""Activate Alexa component.""" """Activate Alexa component."""
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config)) hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
@@ -44,14 +47,16 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
requires_auth = False requires_auth = False
name = "api:alexa:flash_briefings" name = "api:alexa:flash_briefings"
def __init__(self, hass, flash_briefings): def __init__(self, hass: HomeAssistant, flash_briefings: ConfigType) -> None:
"""Initialize Alexa view.""" """Initialize Alexa view."""
super().__init__() super().__init__()
self.flash_briefings = flash_briefings self.flash_briefings = flash_briefings
template.attach(hass, self.flash_briefings) template.attach(hass, self.flash_briefings)
@callback @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.""" """Handle Alexa Flash Briefing request."""
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) _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. Async friendly.
""" """
auth_code = directive.payload["grant"]["code"] auth_code: str = directive.payload["grant"]["code"]
_LOGGER.debug("AcceptGrant code: %s", auth_code) _LOGGER.debug("AcceptGrant code: %s", auth_code)
if config.supports_auth: if config.supports_auth:
@@ -339,8 +339,8 @@ async def async_api_decrease_color_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a decrease color temperature request.""" """Process a decrease color temperature request."""
entity = directive.entity entity = directive.entity
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes[light.ATTR_COLOR_TEMP])
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) max_mireds = int(entity.attributes[light.ATTR_MAX_MIREDS])
value = min(max_mireds, current + 50) value = min(max_mireds, current + 50)
await hass.services.async_call( await hass.services.async_call(
@@ -363,8 +363,8 @@ async def async_api_increase_color_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an increase color temperature request.""" """Process an increase color temperature request."""
entity = directive.entity entity = directive.entity
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes[light.ATTR_COLOR_TEMP])
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) min_mireds = int(entity.attributes[light.ATTR_MIN_MIREDS])
value = max(min_mireds, current - 50) value = max(min_mireds, current - 50)
await hass.services.async_call( await hass.services.async_call(
@@ -403,7 +403,7 @@ async def async_api_activate(
context=context, context=context,
) )
payload = { payload: dict[str, Any] = {
"cause": {"type": Cause.VOICE_INTERACTION}, "cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT), "timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
} }
@@ -432,7 +432,7 @@ async def async_api_deactivate(
context=context, context=context,
) )
payload = { payload: dict[str, Any] = {
"cause": {"type": Cause.VOICE_INTERACTION}, "cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT), "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) volume = round(float(directive.payload["volume"] / 100), 2)
entity = directive.entity entity = directive.entity
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
} }
@@ -554,7 +554,7 @@ async def async_api_select_input(
) )
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_INPUT_SOURCE: media_input, media_player.const.ATTR_INPUT_SOURCE: media_input,
} }
@@ -581,7 +581,7 @@ async def async_api_adjust_volume(
volume_delta = int(directive.payload["volume"]) volume_delta = int(directive.payload["volume"])
entity = directive.entity 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 # read current state
try: try:
@@ -591,7 +591,7 @@ async def async_api_adjust_volume(
volume = float(max(0, volume_delta + current) / 100) volume = float(max(0, volume_delta + current) / 100)
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
} }
@@ -631,7 +631,7 @@ async def async_api_adjust_volume_step(
if is_default: if is_default:
volume_int = default_steps 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)): for _ in range(abs(volume_int)):
await hass.services.async_call( await hass.services.async_call(
@@ -652,7 +652,7 @@ async def async_api_set_mute(
"""Process a set mute request.""" """Process a set mute request."""
mute = bool(directive.payload["mute"]) mute = bool(directive.payload["mute"])
entity = directive.entity entity = directive.entity
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
} }
@@ -673,7 +673,7 @@ async def async_api_play(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a play request.""" """Process a play request."""
entity = directive.entity 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( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context
@@ -691,7 +691,7 @@ async def async_api_pause(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a pause request.""" """Process a pause request."""
entity = directive.entity 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( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context
@@ -709,7 +709,7 @@ async def async_api_stop(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a stop request.""" """Process a stop request."""
entity = directive.entity 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( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
@@ -727,7 +727,7 @@ async def async_api_next(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a next request.""" """Process a next request."""
entity = directive.entity 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( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context
@@ -745,7 +745,7 @@ async def async_api_previous(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a previous request.""" """Process a previous request."""
entity = directive.entity 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( await hass.services.async_call(
entity.domain, entity.domain,
@@ -758,7 +758,9 @@ async def async_api_previous(
return directive.response() 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.""" """Get temperature from Temperature object in requested unit."""
to_unit = hass.config.units.temperature_unit to_unit = hass.config.units.temperature_unit
from_unit = UnitOfTemperature.CELSIUS from_unit = UnitOfTemperature.CELSIUS
@@ -784,11 +786,11 @@ async def async_api_set_target_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a set target temperature request.""" """Process a set target temperature request."""
entity = directive.entity entity = directive.entity
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
unit = hass.config.units.temperature_unit 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 payload = directive.payload
response = directive.response() response = directive.response()
@@ -848,9 +850,10 @@ async def async_api_adjust_target_temp(
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an adjust target temperature request.""" """Process an adjust target temperature request."""
data: dict[str, Any]
entity = directive.entity entity = directive.entity
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
temp_delta = temperature_from_object( 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_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) 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 target_temp_high = float(current_target_temp_high) + temp_delta
if target_temp_high < min_temp or target_temp_high > max_temp: if target_temp_high < min_temp or target_temp_high > max_temp:
raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp) raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp)
@@ -891,7 +894,7 @@ async def async_api_adjust_target_temp(
} }
) )
else: 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: if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_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, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a set thermostat mode request.""" """Process a set thermostat mode request."""
operation_list: list[str]
entity = directive.entity entity = directive.entity
mode = directive.payload["thermostatMode"] mode = directive.payload["thermostatMode"]
mode = mode if isinstance(mode, str) else mode["value"] 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) 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 data[climate.ATTR_PRESET_MODE] = ha_preset
elif mode == "CUSTOM": 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 = directive.payload["thermostatMode"]["customName"]
custom_mode = next( custom_mode = next(
(k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), (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 data[climate.ATTR_HVAC_MODE] = custom_mode
else: else:
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) 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_modes: dict[str, str] = {
ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) 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: if ha_mode not in operation_list:
msg = f"The requested thermostat mode {mode} is not supported" msg = f"The requested thermostat mode {mode} is not supported"
raise AlexaUnsupportedThermostatModeError(msg) raise AlexaUnsupportedThermostatModeError(msg)
@@ -1006,7 +1015,7 @@ async def async_api_arm(
entity = directive.entity entity = directive.entity
service = None service = None
arm_state = directive.payload["armState"] 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: if entity.state != STATE_ALARM_DISARMED:
msg = "You must disarm the system before you can set the requested arm state." 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 # return 0 until alarm integration supports an exit delay
payload = {"exitDelayInSeconds": 0} payload: dict[str, Any] = {"exitDelayInSeconds": 0}
response = directive.response( response = directive.response(
name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload
@@ -1052,7 +1061,7 @@ async def async_api_disarm(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a Security Panel Disarm request.""" """Process a Security Panel Disarm request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
response = directive.response() response = directive.response()
# Per Alexa Documentation: If you receive a Disarm directive, and the # Per Alexa Documentation: If you receive a Disarm directive, and the
@@ -1094,7 +1103,7 @@ async def async_api_set_mode(
instance = directive.instance instance = directive.instance
domain = entity.domain domain = entity.domain
service = None service = None
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
mode = directive.payload["mode"] mode = directive.payload["mode"]
# Fan Direction # Fan Direction
@@ -1107,8 +1116,11 @@ async def async_api_set_mode(
# Fan preset_mode # Fan preset_mode
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
preset_mode = mode.split(".")[1] preset_mode = mode.split(".")[1]
if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get( preset_modes: list[str] | None = entity.attributes.get(fan.ATTR_PRESET_MODES)
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 service = fan.SERVICE_SET_PRESET_MODE
data[fan.ATTR_PRESET_MODE] = preset_mode data[fan.ATTR_PRESET_MODE] = preset_mode
@@ -1119,9 +1131,8 @@ async def async_api_set_mode(
# Humidifier mode # Humidifier mode
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
mode = mode.split(".")[1] mode = mode.split(".")[1]
if mode != PRESET_MODE_NA and mode in entity.attributes.get( modes: list[str] | None = entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES)
humidifier.ATTR_AVAILABLE_MODES if mode != PRESET_MODE_NA and modes and mode in modes:
):
service = humidifier.SERVICE_SET_MODE service = humidifier.SERVICE_SET_MODE
data[humidifier.ATTR_MODE] = mode data[humidifier.ATTR_MODE] = mode
else: else:
@@ -1194,7 +1205,7 @@ async def async_api_toggle_on(
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
service = fan.SERVICE_OSCILLATE service = fan.SERVICE_OSCILLATE
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: True, fan.ATTR_OSCILLATING: True,
} }
@@ -1233,7 +1244,7 @@ async def async_api_toggle_off(
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
service = fan.SERVICE_OSCILLATE service = fan.SERVICE_OSCILLATE
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: False, fan.ATTR_OSCILLATING: False,
} }
@@ -1267,7 +1278,7 @@ async def async_api_set_range(
instance = directive.instance instance = directive.instance
domain = entity.domain domain = entity.domain
service = None service = None
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
range_value = directive.payload["rangeValue"] range_value = directive.payload["rangeValue"]
# Cover Position # Cover Position
@@ -1536,7 +1547,7 @@ async def async_api_changechannel(
channel = metadata_payload["name"] channel = metadata_payload["name"]
payload_name = "callSign" payload_name = "callSign"
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_CONTENT_ID: channel, media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
media_player.const.ATTR_MEDIA_CONTENT_TYPE: ( media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
@@ -1576,7 +1587,7 @@ async def async_api_skipchannel(
channel = int(directive.payload["channelCount"]) channel = int(directive.payload["channelCount"])
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if channel < 0: if channel < 0:
service_media = SERVICE_MEDIA_PREVIOUS_TRACK 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: if media_duration and 0 < int(media_duration) < seek_position:
seek_position = media_duration seek_position = media_duration
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_SEEK_POSITION: seek_position, media_player.ATTR_MEDIA_SEEK_POSITION: seek_position,
} }
@@ -1639,7 +1650,9 @@ async def async_api_seek(
# convert seconds to milliseconds for StateReport. # convert seconds to milliseconds for StateReport.
seek_position = int(seek_position * 1000) 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( return directive.response(
name="StateReport", namespace="Alexa.SeekController", payload=payload name="StateReport", namespace="Alexa.SeekController", payload=payload
) )
@@ -1655,7 +1668,7 @@ async def async_api_set_eq_mode(
"""Process a SetMode request for EqualizerController.""" """Process a SetMode request for EqualizerController."""
mode = directive.payload["mode"] mode = directive.payload["mode"]
entity = directive.entity 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) sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
if sound_mode_list and mode.lower() in sound_mode_list: if sound_mode_list and mode.lower() in sound_mode_list:
@@ -1701,7 +1714,7 @@ async def async_api_hold(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a TimeHoldController Hold request.""" """Process a TimeHoldController Hold request."""
entity = directive.entity 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: if entity.domain == timer.DOMAIN:
service = timer.SERVICE_PAUSE service = timer.SERVICE_PAUSE
@@ -1728,7 +1741,7 @@ async def async_api_resume(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a TimeHoldController Resume request.""" """Process a TimeHoldController Resume request."""
entity = directive.entity 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: if entity.domain == timer.DOMAIN:
service = timer.SERVICE_START service = timer.SERVICE_START
@@ -1773,7 +1786,7 @@ async def async_api_initialize_camera_stream(
"Failed to find suitable URL to serve to Alexa" "Failed to find suitable URL to serve to Alexa"
) from err ) from err
payload = { payload: dict[str, Any] = {
"cameraStreams": [ "cameraStreams": [
{ {
"uri": f"{external_url}{stream_source}", "uri": f"{external_url}{stream_source}",

View File

@@ -3,8 +3,10 @@ import enum
import logging import logging
from typing import Any from typing import Any
from aiohttp.web import Response
from homeassistant.components import http from homeassistant.components import http
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@@ -18,7 +20,7 @@ HANDLERS = Registry() # type: ignore[var-annotated]
INTENTS_API_ENDPOINT = "/api/alexa" INTENTS_API_ENDPOINT = "/api/alexa"
class SpeechType(enum.Enum): class SpeechType(enum.StrEnum):
"""The Alexa speech types.""" """The Alexa speech types."""
plaintext = "PlainText" plaintext = "PlainText"
@@ -28,7 +30,7 @@ class SpeechType(enum.Enum):
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml} SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
class CardType(enum.Enum): class CardType(enum.StrEnum):
"""The Alexa card types.""" """The Alexa card types."""
simple = "Simple" simple = "Simple"
@@ -36,12 +38,12 @@ class CardType(enum.Enum):
@callback @callback
def async_setup(hass): def async_setup(hass: HomeAssistant) -> None:
"""Activate Alexa component.""" """Activate Alexa component."""
hass.http.register_view(AlexaIntentsView) hass.http.register_view(AlexaIntentsView)
async def async_setup_intents(hass): async def async_setup_intents(hass: HomeAssistant) -> None:
"""Do intents setup. """Do intents setup.
Right now this module does not expose any, but the intent component breaks 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 url = INTENTS_API_ENDPOINT
name = "api:alexa" name = "api:alexa"
async def post(self, request): async def post(self, request: http.HomeAssistantRequest) -> Response | bytes:
"""Handle Alexa.""" """Handle Alexa."""
hass = request.app["hass"] hass: HomeAssistant = request.app["hass"]
message = await request.json() message: dict[str, Any] = await request.json()
_LOGGER.debug("Received Alexa request: %s", message) _LOGGER.debug("Received Alexa request: %s", message)
try: 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) return b"" if response is None else self.json(response)
except UnknownRequest as err: except UnknownRequest as err:
_LOGGER.warning(str(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.""" """Return an Alexa response that will speak the error message."""
alexa_intent_info = message.get("request").get("intent") alexa_intent_info = message["request"].get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error) alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict() 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. """Handle an Alexa intent.
Raises: Raises:
@@ -117,19 +123,22 @@ async def async_handle_message(hass, message):
- intent.IntentError - intent.IntentError
""" """
req = message.get("request") req = message["request"]
req_type = req["type"] req_type = req["type"]
if not (handler := HANDLERS.get(req_type)): if not (handler := HANDLERS.get(req_type)):
raise UnknownRequest(f"Received unknown request {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("SessionEndedRequest")
@HANDLERS.register("IntentRequest") @HANDLERS.register("IntentRequest")
@HANDLERS.register("LaunchRequest") @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. """Handle an intent request.
Raises: Raises:
@@ -138,9 +147,9 @@ async def async_handle_intent(hass, message):
- intent.IntentError - intent.IntentError
""" """
req = message.get("request") req = message["request"]
alexa_intent_info = req.get("intent") alexa_intent_info = req.get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
if req["type"] == "LaunchRequest": if req["type"] == "LaunchRequest":
intent_name = ( 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 # passes the id and name of the nearest possible slot resolution. For
# reference to the request object structure, see the Alexa docs: # reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs # https://tinyurl.com/ybvm7jhs
resolved_data = {} resolved_data: dict[str, Any] = {}
resolved_data["value"] = request["value"] resolved_data["value"] = request["value"]
resolved_data["id"] = "" resolved_data["id"] = ""
@@ -226,18 +235,18 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
return resolved_data return resolved_data
class AlexaResponse: class AlexaIntentResponse:
"""Help generating the response for Alexa.""" """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.""" """Initialize the response."""
self.hass = hass self.hass = hass
self.speech = None self.speech: dict[str, Any] | None = None
self.card = None self.card: dict[str, Any] | None = None
self.reprompt = None self.reprompt: dict[str, Any] | None = None
self.session_attributes = {} self.session_attributes: dict[str, Any] = {}
self.should_end_session = True self.should_end_session = True
self.variables = {} self.variables: dict[str, Any] = {}
# Intent is None if request was a LaunchRequest or SessionEndedRequest # Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None: if intent_info is not None:
@@ -252,7 +261,7 @@ class AlexaResponse:
self.variables[_key] = _slot_data["value"] self.variables[_key] = _slot_data["value"]
self.variables[_key + "_Id"] = _slot_data["id"] 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.""" """Add a card to the response."""
assert self.card is None assert self.card is None
@@ -266,7 +275,7 @@ class AlexaResponse:
card["content"] = content card["content"] = content
self.card = card 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.""" """Add speech to the response."""
assert self.speech is None assert self.speech is None
@@ -274,7 +283,7 @@ class AlexaResponse:
self.speech = {"type": speech_type.value, key: text} 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.""" """Add reprompt if user does not answer."""
assert self.reprompt is None assert self.reprompt is None
@@ -284,9 +293,9 @@ class AlexaResponse:
self.reprompt = {"type": speech_type.value, key: text} 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.""" """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: if self.card is not None:
response["card"] = self.card response["card"] = self.card

View File

@@ -1,20 +1,26 @@
"""Describe logbook events.""" """Describe logbook events."""
from collections.abc import Callable
from typing import Any
from homeassistant.components.logbook import ( from homeassistant.components.logbook import (
LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ENTITY_ID,
LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_MESSAGE,
LOGBOOK_ENTRY_NAME, LOGBOOK_ENTRY_NAME,
) )
from homeassistant.core import callback from homeassistant.core import Event, HomeAssistant, callback
from .const import DOMAIN, EVENT_ALEXA_SMART_HOME from .const import DOMAIN, EVENT_ALEXA_SMART_HOME
@callback @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.""" """Describe logbook events."""
@callback @callback
def async_describe_logbook_event(event): def async_describe_logbook_event(event: Event) -> dict[str, Any]:
"""Describe a logbook event.""" """Describe a logbook event."""
data = event.data data = event.data

View File

@@ -1,6 +1,9 @@
"""Alexa Resources and Assets.""" """Alexa Resources and Assets."""
from typing import Any
class AlexaGlobalCatalog: class AlexaGlobalCatalog:
"""The Global Alexa catalog. """The Global Alexa catalog.
@@ -207,36 +210,40 @@ class AlexaCapabilityResource:
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources 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.""" """Initialize an Alexa resource."""
self._resource_labels = [] self._resource_labels = []
for label in labels: for label in labels:
self._resource_labels.append(label) 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 capabilityResources object serialized for an API response."""
return self.serialize_labels(self._resource_labels) 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 serialized configuration for an API response.
Return ModeResources, PresetResources friendlyNames serialized. 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. """Return serialized labels for an API response.
Returns resource label objects for friendlyNames serialized. Returns resource label objects for friendlyNames serialized.
""" """
labels = [] labels: list[dict[str, Any]] = []
label_dict: dict[str, Any]
for label in resources: for label in resources:
if label in AlexaGlobalCatalog.__dict__.values(): if label in AlexaGlobalCatalog.__dict__.values():
label = {"@type": "asset", "value": {"assetId": label}} label_dict = {"@type": "asset", "value": {"assetId": label}}
else: 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} return {"friendlyNames": labels}
@@ -247,22 +254,22 @@ class AlexaModeResource(AlexaCapabilityResource):
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources 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.""" """Initialize an Alexa modeResource."""
super().__init__(labels) super().__init__(labels)
self._supported_modes = [] self._supported_modes: list[dict[str, Any]] = []
self._mode_ordered = ordered 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.""" """Add mode to the supportedModes object."""
self._supported_modes.append({"value": value, "labels": labels}) 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. """Return serialized configuration for an API response.
Returns configuration for ModeResources friendlyNames serialized. Returns configuration for ModeResources friendlyNames serialized.
""" """
mode_resources = [] mode_resources: list[dict[str, Any]] = []
for mode in self._supported_modes: for mode in self._supported_modes:
result = { result = {
"value": mode["value"], "value": mode["value"],
@@ -282,10 +289,17 @@ class AlexaPresetResource(AlexaCapabilityResource):
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources 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.""" """Initialize an Alexa presetResource."""
super().__init__(labels) super().__init__(labels)
self._presets = [] self._presets: list[dict[str, Any]] = []
self._minimum_value = min_value self._minimum_value = min_value
self._maximum_value = max_value self._maximum_value = max_value
self._precision = precision self._precision = precision
@@ -293,16 +307,16 @@ class AlexaPresetResource(AlexaCapabilityResource):
if unit in AlexaGlobalCatalog.__dict__.values(): if unit in AlexaGlobalCatalog.__dict__.values():
self._unit_of_measure = unit 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.""" """Add preset to configuration presets array."""
self._presets.append({"value": value, "labels": labels}) 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. """Return serialized configuration for an API response.
Returns configuration for PresetResources friendlyNames serialized. Returns configuration for PresetResources friendlyNames serialized.
""" """
configuration = { configuration: dict[str, Any] = {
"supportedRange": { "supportedRange": {
"minimumValue": self._minimum_value, "minimumValue": self._minimum_value,
"maximumValue": self._maximum_value, "maximumValue": self._maximum_value,
@@ -372,26 +386,28 @@ class AlexaSemantics:
DIRECTIVE_MODE_SET_MODE = "SetMode" DIRECTIVE_MODE_SET_MODE = "SetMode"
DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode"
def __init__(self): def __init__(self) -> None:
"""Initialize an Alexa modeResource.""" """Initialize an Alexa modeResource."""
self._action_mappings = [] self._action_mappings: list[dict[str, Any]] = []
self._state_mappings = [] 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.""" """Add action mapping between actions and interface directives."""
self._action_mappings.append(semantics) 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.""" """Add state mapping between states and interface directives."""
self._state_mappings.append(semantics) 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.""" """Add StatesToValue stateMappings."""
self._add_state_mapping( self._add_state_mapping(
{"@type": self.STATES_TO_VALUE, "states": states, "value": value} {"@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.""" """Add StatesToRange stateMappings."""
self._add_state_mapping( 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.""" """Add ActionsToDirective actionMappings."""
self._add_action_mapping( 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.""" """Return semantics object serialized for an API response."""
semantics = {} semantics: dict[str, Any] = {}
if self._action_mappings: if self._action_mappings:
semantics[self.MAPPINGS_ACTION] = self._action_mappings semantics[self.MAPPINGS_ACTION] = self._action_mappings
if self._state_mappings: if self._state_mappings:

View File

@@ -1,7 +1,13 @@
"""Support for alexa Smart Home Skill API.""" """Support for alexa Smart Home Skill API."""
import logging import logging
from typing import Any
from aiohttp import web
from yarl import URL
from homeassistant import core 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.components.http.view import HomeAssistantView
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import Context, HomeAssistant from homeassistant.core import Context, HomeAssistant
@@ -23,15 +29,16 @@ from .errors import AlexaBridgeUnreachableError, AlexaError
from .handlers import HANDLERS from .handlers import HANDLERS
from .state_report import AlexaDirective from .state_report import AlexaDirective
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
class AlexaConfig(AbstractConfig): class AlexaConfig(AbstractConfig):
"""Alexa config.""" """Alexa config."""
def __init__(self, hass, config): _auth: Auth | None
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
"""Initialize Alexa config.""" """Initialize Alexa config."""
super().__init__(hass) super().__init__(hass)
self._config = config self._config = config
@@ -42,40 +49,40 @@ class AlexaConfig(AbstractConfig):
self._auth = None self._auth = None
@property @property
def supports_auth(self): def supports_auth(self) -> bool:
"""Return if config supports auth.""" """Return if config supports auth."""
return self._auth is not None return self._auth is not None
@property @property
def should_report_state(self): def should_report_state(self) -> bool:
"""Return if we should proactively report states.""" """Return if we should proactively report states."""
return self._auth is not None and self.authorized return self._auth is not None and self.authorized
@property @property
def endpoint(self): def endpoint(self) -> str | URL | None:
"""Endpoint for report state.""" """Endpoint for report state."""
return self._config.get(CONF_ENDPOINT) return self._config.get(CONF_ENDPOINT)
@property @property
def entity_config(self): def entity_config(self) -> dict[str, Any]:
"""Return entity config.""" """Return entity config."""
return self._config.get(CONF_ENTITY_CONFIG) or {} return self._config.get(CONF_ENTITY_CONFIG) or {}
@property @property
def locale(self): def locale(self) -> str | None:
"""Return config locale.""" """Return config locale."""
return self._config.get(CONF_LOCALE) return self._config.get(CONF_LOCALE)
@core.callback @core.callback
def user_identifier(self): def user_identifier(self) -> str:
"""Return an identifier for the user that represents this config.""" """Return an identifier for the user that represents this config."""
return "" return ""
@core.callback @core.callback
def should_expose(self, entity_id): def should_expose(self, entity_id: str) -> bool:
"""If an entity should be exposed.""" """If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter: 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) entity_registry = er.async_get(self.hass)
if registry_entry := entity_registry.async_get(entity_id): if registry_entry := entity_registry.async_get(entity_id):
@@ -88,16 +95,19 @@ class AlexaConfig(AbstractConfig):
return not auxiliary_entity return not auxiliary_entity
@core.callback @core.callback
def async_invalidate_access_token(self): def async_invalidate_access_token(self) -> None:
"""Invalidate access token.""" """Invalidate access token."""
assert self._auth is not None
self._auth.async_invalidate_access_token() 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.""" """Get an access token."""
assert self._auth is not None
return await self._auth.async_get_access_token() 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.""" """Accept a grant."""
assert self._auth is not None
return await self._auth.async_do_auth(code) return await self._auth.async_do_auth(code)
@@ -124,20 +134,20 @@ class SmartHomeView(HomeAssistantView):
url = SMART_HOME_HTTP_ENDPOINT url = SMART_HOME_HTTP_ENDPOINT
name = "api:alexa:smart_home" name = "api:alexa:smart_home"
def __init__(self, smart_home_config): def __init__(self, smart_home_config: AlexaConfig) -> None:
"""Initialize.""" """Initialize."""
self.smart_home_config = smart_home_config 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. """Handle Alexa Smart Home requests.
The Smart Home API requires the endpoint to be implemented in AWS 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 Lambda, which will need to forward the requests to here and pass back
the response. the response.
""" """
hass = request.app["hass"] hass: HomeAssistant = request.app["hass"]
user = request["hass_user"] user: User = request["hass_user"]
message = await request.json() message: dict[str, Any] = await request.json()
_LOGGER.debug("Received Alexa Smart Home request: %s", message) _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) 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. """Handle incoming API messages.
If enabled is False, the response to all messages will be a 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() response = directive.error()
except AlexaError as err: except AlexaError as err:
response = directive.error( response = directive.error(
error_type=err.error_type, error_type=str(err.error_type),
error_message=err.error_message, error_message=err.error_message,
payload=err.payload, 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") 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: if directive.has_endpoint:
assert directive.entity_id is not None
request_info["entity_id"] = directive.entity_id request_info["entity_id"] = directive.entity_id
hass.bus.async_fire( hass.bus.async_fire(

View File

@@ -5,7 +5,8 @@ import asyncio
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging import logging
from typing import TYPE_CHECKING, cast from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4 from uuid import uuid4
import aiohttp import aiohttp
@@ -13,7 +14,7 @@ import async_timeout
from homeassistant.components import event from homeassistant.components import event
from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.const import MATCH_ALL, STATE_ON
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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.significant_change import create_checker from homeassistant.helpers.significant_change import create_checker
@@ -46,17 +47,22 @@ DEFAULT_TIMEOUT = 10
class AlexaDirective: class AlexaDirective:
"""An incoming Alexa directive.""" """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.""" """Initialize a directive."""
self._directive = request[API_DIRECTIVE] self._directive: dict[str, Any] = request[API_DIRECTIVE]
self.namespace = self._directive[API_HEADER]["namespace"] self.namespace: str = self._directive[API_HEADER]["namespace"]
self.name = self._directive[API_HEADER]["name"] self.name: str = self._directive[API_HEADER]["name"]
self.payload = self._directive[API_PAYLOAD] self.payload: dict[str, Any] = self._directive[API_PAYLOAD]
self.has_endpoint = API_ENDPOINT in self._directive 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: HomeAssistant, config: AbstractConfig) -> None:
def load_entity(self, hass, config):
"""Set attributes related to the entity for this request. """Set attributes related to the entity for this request.
Sets these attributes when self.has_endpoint is True: 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 Will raise AlexaInvalidEndpointError if the endpoint in the request is
malformed or nonexistent. 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_id = _endpoint_id.replace("#", ".")
self.entity = hass.states.get(self.entity_id) entity: State | None = hass.states.get(self.entity_id)
if not self.entity or not config.should_expose(self.entity_id): if not entity or not config.should_expose(self.entity_id):
raise AlexaInvalidEndpointError(_endpoint_id) raise AlexaInvalidEndpointError(_endpoint_id)
self.entity = entity
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
if "instance" in self._directive[API_HEADER]: if "instance" in self._directive[API_HEADER]:
self.instance = self._directive[API_HEADER]["instance"] 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. """Create an API formatted response.
Async friendly. Async friendly.
@@ -100,11 +112,11 @@ class AlexaDirective:
def error( def error(
self, self,
namespace="Alexa", namespace: str = "Alexa",
error_type="INTERNAL_ERROR", error_type: str = "INTERNAL_ERROR",
error_message="", error_message: str = "",
payload=None, payload: dict[str, Any] | None = None,
): ) -> AlexaResponse:
"""Create a API formatted error response. """Create a API formatted error response.
Async friendly. Async friendly.
@@ -127,10 +139,12 @@ class AlexaDirective:
class AlexaResponse: class AlexaResponse:
"""Class to hold a response.""" """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.""" """Initialize the response."""
payload = payload or {} payload = payload or {}
self._response = { self._response: dict[str, Any] = {
API_EVENT: { API_EVENT: {
API_HEADER: { API_HEADER: {
"namespace": namespace, "namespace": namespace,
@@ -143,16 +157,18 @@ class AlexaResponse:
} }
@property @property
def name(self): def name(self) -> str:
"""Return the name of this response.""" """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 @property
def namespace(self): def namespace(self) -> str:
"""Return the namespace of this response.""" """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. """Set the correlationToken.
This should normally mirror the value from a request, and is set by 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 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. """Set the endpoint dictionary.
This is used to send proactive messages to Alexa. This is used to send proactive messages to Alexa.
@@ -172,10 +190,7 @@ class AlexaResponse:
if endpoint_id is not None: if endpoint_id is not None:
self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id
if cookie is not None: def set_endpoint(self, endpoint: dict[str, Any]) -> None:
self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie
def set_endpoint(self, endpoint):
"""Set the endpoint. """Set the endpoint.
This should normally mirror the value from a request, and is set by 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 self._response[API_EVENT][API_ENDPOINT] = endpoint
def _properties(self): def _properties(self) -> list[dict[str, Any]]:
context = self._response.setdefault(API_CONTEXT, {}) context: dict[str, Any] = self._response.setdefault(API_CONTEXT, {})
return context.setdefault("properties", []) 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. """Add a property to the response context.
The Alexa response includes a list of properties which provides The Alexa response includes a list of properties which provides
@@ -204,7 +220,7 @@ class AlexaResponse:
""" """
self._properties().append(prop) 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. """Add all properties from given endpoint if not already set.
Handlers should be using .add_context_property(). Handlers should be using .add_context_property().
@@ -216,12 +232,14 @@ class AlexaResponse:
if (prop["namespace"], prop["name"]) not in already_set: if (prop["namespace"], prop["name"]) not in already_set:
self.add_context_property(prop) self.add_context_property(prop)
def serialize(self): def serialize(self) -> dict[str, Any]:
"""Return response as a JSON-able data structure.""" """Return response as a JSON-able data structure."""
return self._response 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. """Enable the proactive mode.
Proactive mode makes this component report state changes to Alexa. 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( def extra_significant_check(
hass: HomeAssistant, hass: HomeAssistant,
old_state: str, old_state: str,
old_attrs: dict, old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
old_extra_arg: dict, old_extra_arg: Any,
new_state: str, new_state: str,
new_attrs: dict, new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
new_extra_arg: dict, new_extra_arg: Any,
): ) -> bool:
"""Check if the serialized data has changed.""" """Check if the serialized data has changed."""
return old_extra_arg is not None and old_extra_arg != new_extra_arg 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, changed_entity: str,
old_state: State | None, old_state: State | None,
new_state: State | None, new_state: State | None,
): ) -> None:
if not hass.is_running: if not hass.is_running:
return return
@@ -307,8 +325,13 @@ async def async_enable_proactive_mode(hass, smart_home_config):
async def async_send_changereport_message( 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. """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 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 return
headers = {"Authorization": f"Bearer {token}"} headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
endpoint = alexa_entity.alexa_id() endpoint = alexa_entity.alexa_id()
payload = { payload: dict[str, Any] = {
API_CHANGE: { API_CHANGE: {
"cause": {"type": Cause.APP_INTERACTION}, "cause": {"type": Cause.APP_INTERACTION},
"properties": alexa_properties, "properties": alexa_properties,
@@ -339,6 +362,7 @@ async def async_send_changereport_message(
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): async with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post( response = await session.post(
@@ -393,9 +417,9 @@ async def async_send_add_or_update_message(
""" """
token = await config.async_get_access_token() 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: for entity_id in entity_ids:
if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS: 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) alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state)
endpoints.append(alexa_entity.serialize_discovery()) 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( message = AlexaResponse(
name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload
@@ -416,6 +443,7 @@ async def async_send_add_or_update_message(
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
return await session.post( return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True 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() 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: for entity_id in entity_ids:
domain = entity_id.split(".", 1)[0] domain = entity_id.split(".", 1)[0]
@@ -442,7 +470,10 @@ async def async_send_delete_message(
endpoints.append({"endpointId": generate_alexa_id(entity_id)}) 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( message = AlexaResponse(
name="DeleteReport", namespace="Alexa.Discovery", payload=payload name="DeleteReport", namespace="Alexa.Discovery", payload=payload
@@ -451,19 +482,22 @@ async def async_send_delete_message(
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
return await session.post( return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True 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. """Send a DoorbellPress event message for an Alexa entity.
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html
""" """
token = await config.async_get_access_token() 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() endpoint = alexa_entity.alexa_id()
@@ -481,6 +515,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): async with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post( response = await session.post(

View File

@@ -24,7 +24,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, 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 import homeassistant.helpers.entity_registry as er
from .const import ( from .const import (

View File

@@ -80,304 +80,303 @@ class AmbientBinarySensorDescription(
BINARY_SENSOR_DESCRIPTIONS = ( BINARY_SENSOR_DESCRIPTIONS = (
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATTOUT, key=TYPE_BATTOUT,
name="Battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT1, key=TYPE_BATT1,
name="Battery 1", translation_key="battery_1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT2, key=TYPE_BATT2,
name="Battery 2", translation_key="battery_2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT3, key=TYPE_BATT3,
name="Battery 3", translation_key="battery_3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT4, key=TYPE_BATT4,
name="Battery 4", translation_key="battery_4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT5, key=TYPE_BATT5,
name="Battery 5", translation_key="battery_5",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT6, key=TYPE_BATT6,
name="Battery 6", translation_key="battery_6",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT7, key=TYPE_BATT7,
name="Battery 7", translation_key="battery_7",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT8, key=TYPE_BATT8,
name="Battery 8", translation_key="battery_8",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT9, key=TYPE_BATT9,
name="Battery 9", translation_key="battery_9",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATTIN, key=TYPE_BATTIN,
name="Interior battery", translation_key="interior_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT10, key=TYPE_BATT10,
name="Battery 10", translation_key="battery_10",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK1, key=TYPE_BATT_LEAK1,
name="Leak detector battery 1", translation_key="leak_detector_battery_1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK2, key=TYPE_BATT_LEAK2,
name="Leak detector battery 2", translation_key="leak_detector_battery_2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK3, key=TYPE_BATT_LEAK3,
name="Leak detector battery 3", translation_key="leak_detector_battery_3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK4, key=TYPE_BATT_LEAK4,
name="Leak detector battery 4", translation_key="leak_detector_battery_4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM1, key=TYPE_BATT_SM1,
name="Soil monitor battery 1", translation_key="soil_monitor_battery_1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM2, key=TYPE_BATT_SM2,
name="Soil monitor battery 2", translation_key="soil_monitor_battery_2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM3, key=TYPE_BATT_SM3,
name="Soil monitor battery 3", translation_key="soil_monitor_battery_3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM4, key=TYPE_BATT_SM4,
name="Soil monitor battery 4", translation_key="soil_monitor_battery_4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM5, key=TYPE_BATT_SM5,
name="Soil monitor battery 5", translation_key="soil_monitor_battery_5",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM6, key=TYPE_BATT_SM6,
name="Soil monitor battery 6", translation_key="soil_monitor_battery_6",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM7, key=TYPE_BATT_SM7,
name="Soil monitor battery 7", translation_key="soil_monitor_battery_7",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM8, key=TYPE_BATT_SM8,
name="Soil monitor battery 8", translation_key="soil_monitor_battery_8",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM9, key=TYPE_BATT_SM9,
name="Soil monitor battery 9", translation_key="soil_monitor_battery_9",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM10, key=TYPE_BATT_SM10,
name="Soil monitor battery 10", translation_key="soil_monitor_battery_10",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_CO2, key=TYPE_BATT_CO2,
name="CO2 battery", translation_key="co2_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LIGHTNING, key=TYPE_BATT_LIGHTNING,
name="Lightning detector battery", translation_key="lightning_detector_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK1, key=TYPE_LEAK1,
name="Leak detector 1", translation_key="leak_detector_1",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK2, key=TYPE_LEAK2,
name="Leak detector 2", translation_key="leak_detector_2",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK3, key=TYPE_LEAK3,
name="Leak detector 3", translation_key="leak_detector_3",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK4, key=TYPE_LEAK4,
name="Leak detector 4", translation_key="leak_detector_4",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_PM25IN_BATT, key=TYPE_PM25IN_BATT,
name="PM25 indoor battery", translation_key="pm25_indoor_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_PM25_BATT, key=TYPE_PM25_BATT,
name="PM25 battery", translation_key="pm25_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY1, key=TYPE_RELAY1,
name="Relay 1", translation_key="relay_1",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY2, key=TYPE_RELAY2,
name="Relay 2", translation_key="relay_2",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY3, key=TYPE_RELAY3,
name="Relay 3", translation_key="relay_3",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY4, key=TYPE_RELAY4,
name="Relay 4", translation_key="relay_4",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY5, key=TYPE_RELAY5,
name="Relay 5", translation_key="relay_5",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY6, key=TYPE_RELAY6,
name="Relay 6", translation_key="relay_6",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY7, key=TYPE_RELAY7,
name="Relay 7", translation_key="relay_7",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY8, key=TYPE_RELAY8,
name="Relay 8", translation_key="relay_8",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY9, key=TYPE_RELAY9,
name="Relay 9", translation_key="relay_9",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY10, key=TYPE_RELAY10,
name="Relay 10", translation_key="relay_10",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,

View File

@@ -113,544 +113,536 @@ TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_DESCRIPTIONS = ( SENSOR_DESCRIPTIONS = (
SensorEntityDescription( SensorEntityDescription(
key=TYPE_24HOURRAININ, key=TYPE_24HOURRAININ,
name="24 hr rain", translation_key="24_hour_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25, key=TYPE_AQI_PM25,
name="AQI PM2.5", translation_key="pm25_aqi",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25_24H, key=TYPE_AQI_PM25_24H,
name="AQI PM2.5 24h avg", translation_key="pm25_aqi_24h_average",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25_IN, key=TYPE_AQI_PM25_IN,
name="AQI PM2.5 indoor", translation_key="pm25_indoor_aqi",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25_IN_24H, key=TYPE_AQI_PM25_IN_24H,
name="AQI PM2.5 indoor 24h avg", translation_key="pm25_indoor_aqi_24h_average",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_BAROMABSIN, key=TYPE_BAROMABSIN,
name="Abs pressure", translation_key="absolute_pressure",
native_unit_of_measurement=UnitOfPressure.INHG, native_unit_of_measurement=UnitOfPressure.INHG,
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_BAROMRELIN, key=TYPE_BAROMRELIN,
name="Rel pressure", translation_key="relative_pressure",
native_unit_of_measurement=UnitOfPressure.INHG, native_unit_of_measurement=UnitOfPressure.INHG,
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_CO2, key=TYPE_CO2,
name="CO2",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2, device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_DAILYRAININ, key=TYPE_DAILYRAININ,
name="Daily rain", translation_key="daily_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_DEWPOINT, key=TYPE_DEWPOINT,
name="Dew point", translation_key="dew_point",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_EVENTRAININ, key=TYPE_EVENTRAININ,
name="Event rain", translation_key="event_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_FEELSLIKE, key=TYPE_FEELSLIKE,
name="Feels like", translation_key="feels_like",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HOURLYRAININ, key=TYPE_HOURLYRAININ,
name="Hourly rain rate",
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY10, key=TYPE_HUMIDITY10,
name="Humidity 10", translation_key="humidity_10",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY1, key=TYPE_HUMIDITY1,
name="Humidity 1", translation_key="humidity_1",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY2, key=TYPE_HUMIDITY2,
name="Humidity 2", translation_key="humidity_2",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY3, key=TYPE_HUMIDITY3,
name="Humidity 3", translation_key="humidity_3",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY4, key=TYPE_HUMIDITY4,
name="Humidity 4", translation_key="humidity_4",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY5, key=TYPE_HUMIDITY5,
name="Humidity 5", translation_key="humidity_5",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY6, key=TYPE_HUMIDITY6,
name="Humidity 6", translation_key="humidity_6",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY7, key=TYPE_HUMIDITY7,
name="Humidity 7", translation_key="humidity_7",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY8, key=TYPE_HUMIDITY8,
name="Humidity 8", translation_key="humidity_8",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY9, key=TYPE_HUMIDITY9,
name="Humidity 9", translation_key="humidity_9",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY, key=TYPE_HUMIDITY,
name="Humidity",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITYIN, key=TYPE_HUMIDITYIN,
name="Humidity in", translation_key="humidity_indoor",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_LASTRAIN, key=TYPE_LASTRAIN,
name="Last rain", translation_key="last_rain",
icon="mdi:water", icon="mdi:water",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_LIGHTNING_PER_DAY, key=TYPE_LIGHTNING_PER_DAY,
name="Lightning strikes per day", translation_key="lightning_strikes_per_day",
icon="mdi:lightning-bolt", icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes", native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_LIGHTNING_PER_HOUR, key=TYPE_LIGHTNING_PER_HOUR,
name="Lightning strikes per hour", translation_key="lightning_strikes_per_hour",
icon="mdi:lightning-bolt", icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes", native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_MAXDAILYGUST, key=TYPE_MAXDAILYGUST,
name="Max gust", translation_key="max_gust",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_MONTHLYRAININ, key=TYPE_MONTHLYRAININ,
name="Monthly rain", translation_key="monthly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25_24H, key=TYPE_PM25_24H,
name="PM25 24h avg", translation_key="pm25_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25_IN, key=TYPE_PM25_IN,
name="PM25 indoor", translation_key="pm25_indoor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25_IN_24H, 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, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25, key=TYPE_PM25,
name="PM25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM10, key=TYPE_SOILHUM10,
name="Soil humidity 10", translation_key="soil_humidity_10",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM1, key=TYPE_SOILHUM1,
name="Soil humidity 1", translation_key="soil_humidity_1",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM2, key=TYPE_SOILHUM2,
name="Soil humidity 2", translation_key="soil_humidity_2",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM3, key=TYPE_SOILHUM3,
name="Soil humidity 3", translation_key="soil_humidity_3",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM4, key=TYPE_SOILHUM4,
name="Soil humidity 4", translation_key="soil_humidity_4",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM5, key=TYPE_SOILHUM5,
name="Soil humidity 5", translation_key="soil_humidity_5",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM6, key=TYPE_SOILHUM6,
name="Soil humidity 6", translation_key="soil_humidity_6",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM7, key=TYPE_SOILHUM7,
name="Soil humidity 7", translation_key="soil_humidity_7",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM8, key=TYPE_SOILHUM8,
name="Soil humidity 8", translation_key="soil_humidity_8",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM9, key=TYPE_SOILHUM9,
name="Soil humidity 9", translation_key="soil_humidity_9",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP10F, key=TYPE_SOILTEMP10F,
name="Soil temp 10", translation_key="soil_temperature_10",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP1F, key=TYPE_SOILTEMP1F,
name="Soil temp 1", translation_key="soil_temperature_1",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP2F, key=TYPE_SOILTEMP2F,
name="Soil temp 2", translation_key="soil_temperature_2",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP3F, key=TYPE_SOILTEMP3F,
name="Soil temp 3", translation_key="soil_temperature_3",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP4F, key=TYPE_SOILTEMP4F,
name="Soil temp 4", translation_key="soil_temperature_4",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP5F, key=TYPE_SOILTEMP5F,
name="Soil temp 5", translation_key="soil_temperature_5",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP6F, key=TYPE_SOILTEMP6F,
name="Soil temp 6", translation_key="soil_temperature_6",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP7F, key=TYPE_SOILTEMP7F,
name="Soil temp 7", translation_key="soil_temperature_7",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP8F, key=TYPE_SOILTEMP8F,
name="Soil temp 8", translation_key="soil_temperature_8",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP9F, key=TYPE_SOILTEMP9F,
name="Soil temp 9", translation_key="soil_temperature_9",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOLARRADIATION, key=TYPE_SOLARRADIATION,
name="Solar rad",
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
device_class=SensorDeviceClass.IRRADIANCE, device_class=SensorDeviceClass.IRRADIANCE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOLARRADIATION_LX, key=TYPE_SOLARRADIATION_LX,
name="Solar rad",
native_unit_of_measurement=LIGHT_LUX, native_unit_of_measurement=LIGHT_LUX,
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP10F, key=TYPE_TEMP10F,
name="Temp 10", translation_key="temperature_10",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP1F, key=TYPE_TEMP1F,
name="Temp 1", translation_key="temperature_1",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP2F, key=TYPE_TEMP2F,
name="Temp 2", translation_key="temperature_2",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP3F, key=TYPE_TEMP3F,
name="Temp 3", translation_key="temperature_3",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP4F, key=TYPE_TEMP4F,
name="Temp 4", translation_key="temperature_4",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP5F, key=TYPE_TEMP5F,
name="Temp 5", translation_key="temperature_5",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP6F, key=TYPE_TEMP6F,
name="Temp 6", translation_key="temperature_6",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP7F, key=TYPE_TEMP7F,
name="Temp 7", translation_key="temperature_7",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP8F, key=TYPE_TEMP8F,
name="Temp 8", translation_key="temperature_8",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP9F, key=TYPE_TEMP9F,
name="Temp 9", translation_key="temperature_9",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMPF, key=TYPE_TEMPF,
name="Temp",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMPINF, key=TYPE_TEMPINF,
name="Inside temp", translation_key="inside_temperature",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TOTALRAININ, key=TYPE_TOTALRAININ,
name="Lifetime rain", translation_key="lifetime_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_UV, key=TYPE_UV,
name="UV index", translation_key="uv_index",
native_unit_of_measurement="Index", native_unit_of_measurement="Index",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WEEKLYRAININ, key=TYPE_WEEKLYRAININ,
name="Weekly rain", translation_key="weekly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR, key=TYPE_WINDDIR,
name="Wind dir", translation_key="wind_direction",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR_AVG10M, key=TYPE_WINDDIR_AVG10M,
name="Wind dir avg 10m", translation_key="wind_direction_average_10m",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR_AVG2M, key=TYPE_WINDDIR_AVG2M,
name="Wind dir avg 2m", translation_key="wind_direction_average_2m",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDGUSTDIR, key=TYPE_WINDGUSTDIR,
name="Gust dir", translation_key="wind_gust_direction",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDGUSTMPH, key=TYPE_WINDGUSTMPH,
name="Wind gust", translation_key="wind_gust",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDSPDMPH_AVG10M, key=TYPE_WINDSPDMPH_AVG10M,
name="Wind avg 10m", translation_key="wind_average_10m",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDSPDMPH_AVG2M, key=TYPE_WINDSPDMPH_AVG2M,
name="Wind avg 2m", translation_key="wind_average_2m",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDSPEEDMPH, key=TYPE_WINDSPEEDMPH,
name="Wind speed",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_YEARLYRAININ, key=TYPE_YEARLYRAININ,
name="Yearly rain", translation_key="yearly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,

View File

@@ -16,5 +16,356 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]" "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, get_instance as get_recorder_instance,
) )
import homeassistant.config as conf_util import homeassistant.config as conf_util
from homeassistant.config_entries import ( from homeassistant.config_entries import SOURCE_IGNORE
SOURCE_IGNORE,
)
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError

View File

@@ -11,7 +11,7 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
) )
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN

View File

@@ -1,7 +1,7 @@
"""Base class for Android IP Webcam entities.""" """Base class for Android IP Webcam entities."""
from homeassistant.const import CONF_HOST, CONF_NAME 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN

View File

@@ -32,9 +32,8 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util import Throttle

View File

@@ -58,6 +58,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self.host assert self.host
api = create_api(self.hass, self.host, enable_ime=False) api = create_api(self.hass, self.host, enable_ime=False)
try: try:
await api.async_generate_cert_if_missing()
self.name, self.mac = await api.async_get_name_and_mac() self.name, self.mac = await api.async_get_name_and_mac()
assert self.mac assert self.mac
await self.async_set_unique_id(format_mac(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.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN

View File

@@ -6,7 +6,7 @@ from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate
import async_timeout import async_timeout
from homeassistant.core import HomeAssistant, callback 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN from .const import DOMAIN

View File

@@ -15,8 +15,8 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.const import CONF_MAC, CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ANTHEMAV_UDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER 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.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv 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 from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@@ -26,11 +26,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, 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 from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN

View File

@@ -31,7 +31,7 @@ from homeassistant.const import (
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN

View File

@@ -3,9 +3,7 @@ from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_automation import ( from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
DEVICE_TRIGGER_BASE_SCHEMA,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
CONF_DEVICE_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.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .config_flow import get_entry_client from .config_flow import get_entry_client

View File

@@ -1,7 +1,7 @@
"""Aseko entity.""" """Aseko entity."""
from aioaseko import Unit 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN

View File

@@ -18,6 +18,7 @@ from .pipeline import (
PipelineInput, PipelineInput,
PipelineRun, PipelineRun,
PipelineStage, PipelineStage,
WakeWordSettings,
async_create_default_pipeline, async_create_default_pipeline,
async_get_pipeline, async_get_pipeline,
async_get_pipelines, async_get_pipelines,
@@ -35,6 +36,7 @@ __all__ = (
"PipelineEvent", "PipelineEvent",
"PipelineEventType", "PipelineEventType",
"PipelineNotFound", "PipelineNotFound",
"WakeWordSettings",
) )
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@@ -57,7 +59,10 @@ async def async_pipeline_from_audio_stream(
pipeline_id: str | None = None, pipeline_id: str | None = None,
conversation_id: str | None = None, conversation_id: str | None = None,
tts_audio_output: str | None = None, tts_audio_output: str | None = None,
wake_word_settings: WakeWordSettings | None = None,
device_id: str | None = None, device_id: str | None = None,
start_stage: PipelineStage = PipelineStage.STT,
end_stage: PipelineStage = PipelineStage.TTS,
) -> None: ) -> None:
"""Create an audio pipeline from an audio stream. """Create an audio pipeline from an audio stream.
@@ -72,10 +77,11 @@ async def async_pipeline_from_audio_stream(
hass, hass,
context=context, context=context,
pipeline=async_get_pipeline(hass, pipeline_id=pipeline_id), pipeline=async_get_pipeline(hass, pipeline_id=pipeline_id),
start_stage=PipelineStage.STT, start_stage=start_stage,
end_stage=PipelineStage.TTS, end_stage=end_stage,
event_callback=event_callback, event_callback=event_callback,
tts_audio_output=tts_audio_output, tts_audio_output=tts_audio_output,
wake_word_settings=wake_word_settings,
), ),
) )
await pipeline_input.validate() await pipeline_input.validate()

View File

@@ -18,6 +18,14 @@ class PipelineNotFound(PipelineError):
"""Unspecified pipeline picked.""" """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): class SpeechToTextError(PipelineError):
"""Error in speech-to-text portion of pipeline.""" """Error in speech-to-text portion of pipeline."""

View File

@@ -2,7 +2,7 @@
"domain": "assist_pipeline", "domain": "assist_pipeline",
"name": "Assist pipeline", "name": "Assist pipeline",
"codeowners": ["@balloob", "@synesthesiam"], "codeowners": ["@balloob", "@synesthesiam"],
"dependencies": ["conversation", "stt", "tts"], "dependencies": ["conversation", "stt", "tts", "wake_word"],
"documentation": "https://www.home-assistant.io/integrations/assist_pipeline", "documentation": "https://www.home-assistant.io/integrations/assist_pipeline",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal", "quality_scale": "internal",

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import AsyncIterable, Callable, Iterable from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from enum import StrEnum from enum import StrEnum
import logging import logging
@@ -10,7 +10,14 @@ from typing import Any, cast
import voluptuous as vol 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 ( from homeassistant.components.tts.media_source import (
generate_media_source_id as tts_generate_media_source_id, generate_media_source_id as tts_generate_media_source_id,
) )
@@ -39,7 +46,10 @@ from .error import (
PipelineNotFound, PipelineNotFound,
SpeechToTextError, SpeechToTextError,
TextToSpeechError, TextToSpeechError,
WakeWordDetectionError,
WakeWordTimeoutError,
) )
from .vad import VoiceActivityTimeout, VoiceCommandSegmenter
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -241,6 +251,8 @@ class PipelineEventType(StrEnum):
RUN_START = "run-start" RUN_START = "run-start"
RUN_END = "run-end" RUN_END = "run-end"
WAKE_WORD_START = "wake_word-start"
WAKE_WORD_END = "wake_word-end"
STT_START = "stt-start" STT_START = "stt-start"
STT_END = "stt-end" STT_END = "stt-end"
INTENT_START = "intent-start" INTENT_START = "intent-start"
@@ -297,12 +309,14 @@ class Pipeline:
class PipelineStage(StrEnum): class PipelineStage(StrEnum):
"""Stages of a pipeline.""" """Stages of a pipeline."""
WAKE_WORD = "wake_word"
STT = "stt" STT = "stt"
INTENT = "intent" INTENT = "intent"
TTS = "tts" TTS = "tts"
PIPELINE_STAGE_ORDER = [ PIPELINE_STAGE_ORDER = [
PipelineStage.WAKE_WORD,
PipelineStage.STT, PipelineStage.STT,
PipelineStage.INTENT, PipelineStage.INTENT,
PipelineStage.TTS, 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 @dataclass
class PipelineRun: class PipelineRun:
"""Running context for a pipeline.""" """Running context for a pipeline."""
@@ -341,17 +366,20 @@ class PipelineRun:
runner_data: Any | None = None runner_data: Any | None = None
intent_agent: str | None = None intent_agent: str | None = None
tts_audio_output: str | None = None tts_audio_output: str | None = None
wake_word_settings: WakeWordSettings | None = None
id: str = field(default_factory=ulid_util.ulid) id: str = field(default_factory=ulid_util.ulid)
stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False) stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False)
tts_engine: str = field(init=False) tts_engine: str = field(init=False)
tts_options: dict | None = field(init=False, default=None) 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: def __post_init__(self) -> None:
"""Set language for pipeline.""" """Set language for pipeline."""
self.language = self.pipeline.language or self.hass.config.language 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( if PIPELINE_STAGE_ORDER.index(self.end_stage) < PIPELINE_STAGE_ORDER.index(
self.start_stage 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: async def prepare_speech_to_text(self, metadata: stt.SpeechMetadata) -> None:
"""Prepare speech-to-text.""" """Prepare speech-to-text."""
# pipeline.stt_engine can't be None or this function is not called # pipeline.stt_engine can't be None or this function is not called
@@ -443,9 +606,21 @@ class PipelineRun:
) )
try: 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 # Transcribe audio stream
result = await self.stt_provider.async_process_audio_stream( result = await self.stt_provider.async_process_audio_stream(
metadata, stream metadata, segment_stream(stream)
) )
except Exception as src_error: except Exception as src_error:
_LOGGER.exception("Unexpected error during speech-to-text") _LOGGER.exception("Unexpected error during speech-to-text")
@@ -663,17 +838,45 @@ class PipelineInput:
async def execute(self) -> None: async def execute(self) -> None:
"""Run pipeline.""" """Run pipeline."""
self.run.start() self.run.start()
current_stage = self.run.start_stage current_stage: PipelineStage | None = self.run.start_stage
audio_buffer: list[bytes] = []
try: 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 # speech-to-text
intent_input = self.intent_input intent_input = self.intent_input
if current_stage == PipelineStage.STT: if current_stage == PipelineStage.STT:
assert self.stt_metadata is not None assert self.stt_metadata is not None
assert self.stt_stream 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( intent_input = await self.run.speech_to_text(
self.stt_metadata, self.stt_metadata,
self.stt_stream, stt_stream,
) )
current_stage = PipelineStage.INTENT current_stage = PipelineStage.INTENT
@@ -707,7 +910,7 @@ class PipelineInput:
async def validate(self) -> None: async def validate(self) -> None:
"""Validate pipeline input against start stage.""" """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: if self.run.pipeline.stt_engine is None:
raise PipelineRunValidationError( raise PipelineRunValidationError(
"the pipeline does not support speech-to-text" "the pipeline does not support speech-to-text"
@@ -741,6 +944,13 @@ class PipelineInput:
prepare_tasks = [] 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 ( if (
start_stage_index start_stage_index
<= PIPELINE_STAGE_ORDER.index(PipelineStage.STT) <= PIPELINE_STAGE_ORDER.index(PipelineStage.STT)

View File

@@ -88,7 +88,7 @@ class VoiceCommandSegmenter:
self.in_command = False self.in_command = False
def process(self, samples: bytes) -> bool: 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. Returns False when command is done.
""" """
@@ -148,3 +148,94 @@ class VoiceCommandSegmenter:
self._silence_seconds_left = self.silence_seconds self._silence_seconds_left = self.silence_seconds
return True 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, PipelineInput,
PipelineRun, PipelineRun,
PipelineStage, PipelineStage,
WakeWordSettings,
async_get_pipeline, async_get_pipeline,
) )
from .vad import VoiceCommandSegmenter
DEFAULT_TIMEOUT = 30 DEFAULT_TIMEOUT = 30
DEFAULT_WAKE_WORD_TIMEOUT = 3
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -63,6 +64,18 @@ def async_register_websocket_api(hass: HomeAssistant) -> None:
cv.key_value_schemas( cv.key_value_schemas(
"start_stage", "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( PipelineStage.STT: vol.Schema(
{vol.Required("input"): {vol.Required("sample_rate"): int}}, {vol.Required("input"): {vol.Required("sample_rate"): int}},
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
@@ -102,6 +115,7 @@ async def websocket_run(
end_stage = PipelineStage(msg["end_stage"]) end_stage = PipelineStage(msg["end_stage"])
handler_id: int | None = None handler_id: int | None = None
unregister_handler: Callable[[], None] | None = None unregister_handler: Callable[[], None] | None = None
wake_word_settings: WakeWordSettings | None = None
# Arguments to PipelineInput # Arguments to PipelineInput
input_args: dict[str, Any] = { input_args: dict[str, Any] = {
@@ -109,24 +123,26 @@ async def websocket_run(
"device_id": msg.get("device_id"), "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 pipeline that will receive audio as binary websocket messages
audio_queue: asyncio.Queue[bytes] = asyncio.Queue() audio_queue: asyncio.Queue[bytes] = asyncio.Queue()
incoming_sample_rate = msg["input"]["sample_rate"] 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]: async def stt_stream() -> AsyncGenerator[bytes, None]:
state = None state = None
segmenter = VoiceCommandSegmenter()
# Yield until we receive an empty chunk # Yield until we receive an empty chunk
while chunk := await audio_queue.get(): while chunk := await audio_queue.get():
chunk, state = audioop.ratecv( if incoming_sample_rate != 16000:
chunk, 2, 1, incoming_sample_rate, 16000, state chunk, state = audioop.ratecv(
) chunk, 2, 1, incoming_sample_rate, 16000, state
if not segmenter.process(chunk): )
# Voice command is finished
break
yield chunk yield chunk
def handle_binary( def handle_binary(
@@ -169,6 +185,7 @@ async def websocket_run(
"stt_binary_handler_id": handler_id, "stt_binary_handler_id": handler_id,
"timeout": timeout, "timeout": timeout,
}, },
wake_word_settings=wake_word_settings,
) )
pipeline_input = PipelineInput(**input_args) 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.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er 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.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util, slugify 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.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,

View File

@@ -16,8 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

View File

@@ -3,8 +3,10 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import ValuesView from collections.abc import ValuesView
from datetime import datetime
from itertools import chain from itertools import chain
import logging import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError from aiohttp import ClientError, ClientResponseError
from yalexs.const import DEFAULT_BRAND from yalexs.const import DEFAULT_BRAND
@@ -238,14 +240,18 @@ class AugustData(AugustSubscriberMixin):
) )
@callback @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.""" """Process a pubnub message."""
device = self.get_device_detail(device_id) device = self.get_device_detail(device_id)
activities = activities_from_pubnub_message(device, date_time, message) activities = activities_from_pubnub_message(device, date_time, message)
activity_stream = self.activity_stream
assert activity_stream is not None
if activities: 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.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 @callback
def async_stop(self): def async_stop(self):

View File

@@ -1,16 +1,21 @@
"""Consume the august activity stream.""" """Consume the august activity stream."""
import asyncio import asyncio
from datetime import datetime
import logging import logging
from aiohttp import ClientError 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 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.debounce import Debouncer
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .const import ACTIVITY_UPDATE_INTERVAL from .const import ACTIVITY_UPDATE_INTERVAL
from .gateway import AugustGateway
from .subscriber import AugustSubscriberMixin from .subscriber import AugustSubscriberMixin
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -18,29 +23,50 @@ _LOGGER = logging.getLogger(__name__)
ACTIVITY_STREAM_FETCH_LIMIT = 10 ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 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): class ActivityStream(AugustSubscriberMixin):
"""August activity stream handler.""" """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.""" """Init August activity stream object."""
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
self._hass = hass self._hass = hass
self._schedule_updates = {} self._schedule_updates: dict[str, list[CALLBACK_TYPE]] = {}
self._august_gateway = august_gateway self._august_gateway = august_gateway
self._api = api self._api = api
self._house_ids = house_ids self._house_ids = house_ids
self._latest_activities = {} self._latest_activities: dict[str, dict[ActivityType, Activity]] = {}
self._last_update_time = None self._did_first_update = False
self.pubnub = pubnub self.pubnub = pubnub
self._update_debounce = {} self._update_debounce: dict[str, Debouncer] = {}
async def async_setup(self): async def async_setup(self):
"""Token refresh check and catch up the activity stream.""" """Token refresh check and catch up the activity stream."""
for house_id in self._house_ids: self._update_debounce = {
self._update_debounce[house_id] = self._async_create_debouncer(house_id) house_id: self._async_create_debouncer(house_id)
for house_id in self._house_ids
}
await self._async_refresh(utcnow()) await self._async_refresh(utcnow())
self._did_first_update = True
@callback @callback
def _async_create_debouncer(self, house_id): def _async_create_debouncer(self, house_id):
@@ -52,7 +78,7 @@ class ActivityStream(AugustSubscriberMixin):
return Debouncer( return Debouncer(
self._hass, self._hass,
_LOGGER, _LOGGER,
cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(), cooldown=ACTIVITY_DEBOUNCE_COOLDOWN,
immediate=True, immediate=True,
function=_async_update_house_id, function=_async_update_house_id,
) )
@@ -62,73 +88,73 @@ class ActivityStream(AugustSubscriberMixin):
"""Cleanup any debounces.""" """Cleanup any debounces."""
for debouncer in self._update_debounce.values(): for debouncer in self._update_debounce.values():
debouncer.async_cancel() debouncer.async_cancel()
for house_id, updater in self._schedule_updates.items(): for cancels in self._schedule_updates.values():
if updater is not None: _async_cancel_future_scheduled_updates(cancels)
updater()
self._schedule_updates[house_id] = None
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.""" """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 return None
latest_device_activities = self._latest_activities[device_id] latest_activity: Activity | None = None
latest_activity = None
for activity_type in activity_types: for activity_type in activity_types:
if activity_type in latest_device_activities: if activity := latest_device_activities.get(activity_type):
if ( if (
latest_activity is not None latest_activity
and latest_device_activities[activity_type].activity_start_time and activity.activity_start_time
<= latest_activity.activity_start_time <= latest_activity.activity_start_time
): ):
continue continue
latest_activity = latest_device_activities[activity_type] latest_activity = activity
return latest_activity return latest_activity
async def _async_refresh(self, time): async def _async_refresh(self, time: datetime) -> None:
"""Update the activity stream from August.""" """Update the activity stream from August."""
# This is the only place we refresh the api token # This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed() await self._august_gateway.async_refresh_access_token_if_needed()
if self.pubnub.connected: if self.pubnub.connected:
_LOGGER.debug("Skipping update because pubnub is connected") _LOGGER.debug("Skipping update because pubnub is connected")
return return
await self._async_update_device_activities(time)
async def _async_update_device_activities(self, time):
_LOGGER.debug("Start retrieving device activities") _LOGGER.debug("Start retrieving device activities")
await asyncio.gather( await asyncio.gather(
*( *(debouncer.async_call() for debouncer in self._update_debounce.values())
self._update_debounce[house_id].async_call()
for house_id in self._house_ids
)
) )
self._last_update_time = time
@callback @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.""" """Update for a house activities now and once in the future."""
if self._schedule_updates.get(house_id): if cancels := self._schedule_updates.get(house_id):
self._schedule_updates[house_id]() _async_cancel_future_scheduled_updates(cancels)
self._schedule_updates[house_id] = None
async def _update_house_activities(_): debouncer = self._update_debounce[house_id]
await self._update_debounce[house_id].async_call()
self._hass.async_create_task(self._update_debounce[house_id].async_call()) self._hass.async_create_task(debouncer.async_call())
# Schedule an update past the debounce to ensure # Schedule two updates past the debounce time
# we catch the case where the lock operator is # to ensure we catch the case where the activity
# not updated or the lock failed # api does not update right away and we need to poll
self._schedule_updates[house_id] = async_call_later( # it again. Sometimes the lock operator or a doorbell
self._hass, # will not show up in the activity stream right away.
ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1, future_updates = self._schedule_updates.setdefault(house_id, [])
_update_house_activities,
)
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.""" """Update device activities for a house."""
if self._last_update_time: if self._did_first_update:
limit = ACTIVITY_STREAM_FETCH_LIMIT limit = ACTIVITY_STREAM_FETCH_LIMIT
else: else:
limit = ACTIVITY_CATCH_UP_FETCH_LIMIT limit = ACTIVITY_CATCH_UP_FETCH_LIMIT
@@ -150,36 +176,34 @@ class ActivityStream(AugustSubscriberMixin):
_LOGGER.debug( _LOGGER.debug(
"Completed retrieving device activities for house id %s", house_id "Completed retrieving device activities for house id %s", house_id
) )
for device_id in self.async_process_newer_device_activities(activities):
updated_device_ids = self.async_process_newer_device_activities(activities)
if not updated_device_ids:
return
for device_id in updated_device_ids:
_LOGGER.debug( _LOGGER.debug(
"async_signal_device_id_update (from activity stream): %s", "async_signal_device_id_update (from activity stream): %s",
device_id, device_id,
) )
self.async_signal_device_id_update(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.""" """Process activities if they are newer than the last one."""
updated_device_ids = set() updated_device_ids = set()
latest_activities = self._latest_activities
for activity in activities: for activity in activities:
device_id = activity.device_id device_id = activity.device_id
activity_type = activity.activity_type 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 # Ignore activities that are older than the latest one unless it is a non
# locking or unlocking activity with the exact same start time. # locking or unlocking activity with the exact same start time.
if ( last_activity = device_activities.get(activity_type)
get_latest_activity(activity, device_activities.get(activity_type)) # The activity stream can have duplicate activities. So we need
!= activity # 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 continue
device_activities[activity_type] = activity device_activities[activity_type] = activity
updated_device_ids.add(device_id) updated_device_ids.add(device_id)
return updated_device_ids return updated_device_ids

View File

@@ -13,7 +13,7 @@ from yalexs.activity import (
ActivityType, ActivityType,
) )
from yalexs.doorbell import Doorbell, DoorbellDetail 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 yalexs.util import update_lock_detail_from_activity
from homeassistant.components.binary_sensor import ( 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.""" """Get the latest state of the sensor."""
# The doorbell will go into standby mode when there is no motion # 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 # 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 # to consider is available or we will not report motion or dings
if isinstance(detail, DoorbellDetail):
return detail.is_online or detail.is_standby return detail.is_online or detail.is_standby
return detail.bridge_is_online
def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: 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) 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 assert data.activity_stream is not None
latest = data.activity_stream.get_latest_device_activity( latest = data.activity_stream.get_latest_device_activity(
detail.device_id, {ActivityType.DOORBELL_DING} detail.device_id, {ActivityType.DOORBELL_DING}
@@ -135,15 +138,7 @@ SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription(
name="Open", name="Open",
) )
SENSOR_TYPES_VIDEO_DOORBELL = (
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
AugustDoorbellBinarySensorEntityDescription(
key="doorbell_ding",
name="Ding",
device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=_retrieve_ding_state,
is_time_based=True,
),
AugustDoorbellBinarySensorEntityDescription( AugustDoorbellBinarySensorEntityDescription(
key="doorbell_motion", key="doorbell_motion",
name="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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@@ -193,8 +199,17 @@ async def async_setup_entry(
_LOGGER.debug("Adding sensor class door for %s", door.device_name) _LOGGER.debug("Adding sensor class door for %s", door.device_name)
entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR)) 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 doorbell in data.doorbells:
for description in SENSOR_TYPES_DOORBELL: for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL:
_LOGGER.debug( _LOGGER.debug(
"Adding doorbell sensor class %s for %s", "Adding doorbell sensor class %s for %s",
description.device_class, description.device_class,
@@ -261,7 +276,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
def __init__( def __init__(
self, self,
data: AugustData, data: AugustData,
device: Doorbell, device: Doorbell | Lock,
description: AugustDoorbellBinarySensorEntityDescription, description: AugustDoorbellBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""

View File

@@ -6,7 +6,8 @@ from yalexs.lock import Lock
from yalexs.util import get_configuration_url from yalexs.util import get_configuration_url
from homeassistant.core import callback 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 . import DOMAIN, AugustData
from .const import MANUFACTURER from .const import MANUFACTURER

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "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.""" """Base class for August entity."""
from abc import abstractmethod
from datetime import datetime, timedelta
from homeassistant.const import EVENT_HOMEASSISTANT_STOP 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 from homeassistant.helpers.event import async_track_time_interval
class AugustSubscriberMixin: class AugustSubscriberMixin:
"""Base implementation for a subscriber.""" """Base implementation for a subscriber."""
def __init__(self, hass, update_interval): def __init__(self, hass: HomeAssistant, update_interval: timedelta) -> None:
"""Initialize an subscriber.""" """Initialize an subscriber."""
super().__init__() super().__init__()
self._hass = hass self._hass = hass
self._update_interval = update_interval self._update_interval = update_interval
self._subscriptions = {} self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {}
self._unsub_interval = None self._unsub_interval: CALLBACK_TYPE | None = None
self._stop_interval = None self._stop_interval: CALLBACK_TYPE | None = None
@callback @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. """Add an callback subscriber.
Returns a callable that can be used to unsubscribe. Returns a callable that can be used to unsubscribe.
@@ -34,8 +39,12 @@ class AugustSubscriberMixin:
return _unsubscribe return _unsubscribe
@abstractmethod
async def _async_refresh(self, time: datetime) -> None:
"""Refresh data."""
@callback @callback
def _async_setup_listeners(self): def _async_setup_listeners(self) -> None:
"""Create interval and stop listeners.""" """Create interval and stop listeners."""
self._unsub_interval = async_track_time_interval( self._unsub_interval = async_track_time_interval(
self._hass, self._hass,
@@ -54,7 +63,9 @@ class AugustSubscriberMixin:
) )
@callback @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.""" """Remove a callback subscriber."""
self._subscriptions[device_id].remove(update_callback) self._subscriptions[device_id].remove(update_callback)
if not self._subscriptions[device_id]: if not self._subscriptions[device_id]:
@@ -63,14 +74,15 @@ class AugustSubscriberMixin:
if self._subscriptions: if self._subscriptions:
return return
self._unsub_interval() if self._unsub_interval:
self._unsub_interval = None self._unsub_interval()
self._unsub_interval = None
if self._stop_interval: if self._stop_interval:
self._stop_interval() self._stop_interval()
self._stop_interval = None self._stop_interval = None
@callback @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.""" """Call the callbacks for a device_id."""
if not self._subscriptions.get(device_id): if not self._subscriptions.get(device_id):
return return

View File

@@ -7,10 +7,7 @@ from aiohttp import ClientError
from auroranoaa import AuroraForecast from auroranoaa import AuroraForecast
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
DataUpdateCoordinator,
UpdateFailed,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@@ -2,16 +2,10 @@
import logging import logging
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from .const import ( from .const import ATTRIBUTION, DOMAIN
ATTRIBUTION,
DOMAIN,
)
from .coordinator import AuroraDataUpdateCoordinator from .coordinator import AuroraDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@@ -7,7 +7,8 @@ from typing import Any
from aurorapy.client import AuroraSerialClient 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 ( from .const import (
ATTR_DEVICE_NAME, ATTR_DEVICE_NAME,

View File

@@ -15,8 +15,7 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfInformation, UnitOfTime from homeassistant.const import UnitOfInformation, UnitOfTime
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 python_awair.exceptions import AuthError, AwairError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform
CONF_ACCESS_TOKEN,
CONF_HOST,
Platform,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.core import HomeAssistant
from homeassistant.helpers import device_registry as dr 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 axis.models.event import Event, EventTopic
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN as AXIS_DOMAIN from .const import DOMAIN as AXIS_DOMAIN
from .device import AxisNetworkDevice from .device import AxisNetworkDevice

View File

@@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,

View File

@@ -5,8 +5,8 @@ from aiobafi6 import Device
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import DeviceInfo, format_mac
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
class BAFEntity(Entity): class BAFEntity(Entity):

View File

@@ -3,8 +3,8 @@ from __future__ import annotations
from pybalboa import EVENT_UPDATE, SpaClient from pybalboa import EVENT_UPDATE, SpaClient
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN 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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT

View File

@@ -14,7 +14,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
) )
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN 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.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import ( from .const import (

View File

@@ -9,7 +9,7 @@ from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform 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 homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH

View File

@@ -637,7 +637,7 @@ class BluetoothManager:
else: else:
# We could write out every item in the typed dict here # We could write out every item in the typed dict here
# but that would be a bit inefficient and verbose. # 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) callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
connectable = callback_matcher[CONNECTABLE] connectable = callback_matcher[CONNECTABLE]

View File

@@ -18,7 +18,7 @@
"bleak-retry-connector==3.1.1", "bleak-retry-connector==3.1.1",
"bluetooth-adapters==0.16.0", "bluetooth-adapters==0.16.0",
"bluetooth-auto-recovery==1.2.1", "bluetooth-auto-recovery==1.2.1",
"bluetooth-data-tools==1.7.0", "bluetooth-data-tools==1.8.0",
"dbus-fast==1.90.1" "dbus-fast==1.91.2"
] ]
} }

View File

@@ -16,14 +16,9 @@ from homeassistant.const import (
EntityCategory, EntityCategory,
) )
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.entity import ( from homeassistant.helpers.device_registry import DeviceInfo
DeviceInfo, from homeassistant.helpers.entity import Entity, EntityDescription
Entity, from homeassistant.helpers.entity_platform import async_get_current_platform
EntityDescription,
)
from homeassistant.helpers.entity_platform import (
async_get_current_platform,
)
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum

View File

@@ -12,14 +12,8 @@ from .api import (
async_register_callback, async_register_callback,
async_track_unavailable, async_track_unavailable,
) )
from .match import ( from .match import BluetoothCallbackMatcher
BluetoothCallbackMatcher, from .models import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
)
from .models import (
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfoBleak,
)
class BasePassiveBluetoothCoordinator(ABC): 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.core import HomeAssistant, callback
from homeassistant.helpers import discovery, entity_registry as er from homeassistant.helpers import discovery, entity_registry as er
import homeassistant.helpers.config_validation as cv 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.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity

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