mirror of
https://github.com/home-assistant/core.git
synced 2025-08-09 23:55:07 +02:00
Merge branch 'dev' into feature/starlink-device-tracker
This commit is contained in:
@@ -86,6 +86,7 @@ components: &components
|
|||||||
- homeassistant/components/lovelace/**
|
- homeassistant/components/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/**
|
||||||
|
@@ -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
|
||||||
|
5
.github/workflows/builder.yml
vendored
5
.github/workflows/builder.yml
vendored
@@ -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 \
|
||||||
|
@@ -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.*
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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(
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
@@ -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
|
||||||
|
@@ -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"),
|
||||||
|
@@ -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():
|
||||||
|
@@ -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 = {
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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}",
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -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(
|
||||||
|
@@ -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(
|
||||||
|
@@ -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
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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))
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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__)
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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."""
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
@@ -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."""
|
||||||
|
@@ -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
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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__)
|
||||||
|
|
||||||
|
@@ -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__)
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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]
|
||||||
|
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
Reference in New Issue
Block a user