Compare commits

..

7 Commits

Author SHA1 Message Date
Erik 0e00e15fa5 Update snapshots 2025-05-23 11:14:53 +02:00
Erik 3e19b5f6d5 Handle current entity id matches the automatic entity id 2025-05-23 11:14:22 +02:00
Erik 3f1d3bfc4a Update folder_watcher test 2025-05-23 11:14:20 +02:00
Erik 08a8bfaf8f Adjust test 2025-05-23 11:13:27 +02:00
Erik 31f8ea523f Fix logic and add additional tests 2025-05-23 11:13:26 +02:00
Erik 0924740cb4 Calculate suggested object id from entity properties 2025-05-23 11:13:26 +02:00
Erik 9f039002ff Add WS command to help reset custom entity_id 2025-05-12 08:10:43 +02:00
711 changed files with 16365 additions and 3792 deletions
Generated
+2 -2
View File
@@ -1498,8 +1498,8 @@ build.json @home-assistant/supervisor
/tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbee/ @jafar-atili
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
@@ -39,20 +39,11 @@ async def async_setup_entry(
session = async_create_clientsession(
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
)
def create_container_client() -> ContainerClient:
"""Create a ContainerClient."""
return ContainerClient(
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
container_name=entry.data[CONF_CONTAINER_NAME],
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=session),
)
# has a blocking call to open in cpython
container_client: ContainerClient = await hass.async_add_executor_job(
create_container_client
container_client = ContainerClient(
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
container_name=entry.data[CONF_CONTAINER_NAME],
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=session),
)
try:
@@ -27,25 +27,9 @@ _LOGGER = logging.getLogger(__name__)
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for azure storage."""
async def get_container_client(
self, account_name: str, container_name: str, storage_account_key: str
) -> ContainerClient:
"""Get the container client.
ContainerClient has a blocking call to open in cpython
"""
session = async_get_clientsession(self.hass)
def create_container_client() -> ContainerClient:
return ContainerClient(
account_url=f"https://{account_name}.blob.core.windows.net/",
container_name=container_name,
credential=storage_account_key,
transport=AioHttpTransport(session=session),
)
return await self.hass.async_add_executor_job(create_container_client)
def get_account_url(self, account_name: str) -> str:
"""Get the account URL."""
return f"https://{account_name}.blob.core.windows.net/"
async def validate_config(
self, container_client: ContainerClient
@@ -74,10 +58,11 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match(
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
)
container_client = await self.get_container_client(
account_name=user_input[CONF_ACCOUNT_NAME],
container_client = ContainerClient(
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
container_name=user_input[CONF_CONTAINER_NAME],
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
)
errors = await self.validate_config(container_client)
@@ -114,12 +99,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry = self._get_reauth_entry()
if user_input is not None:
container_client = await self.get_container_client(
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
container_client = ContainerClient(
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
)
errors = await self.validate_config(container_client)
if not errors:
return self.async_update_reload_and_abort(
@@ -144,10 +129,13 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
container_client = await self.get_container_client(
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
container_client = ContainerClient(
account_url=self.get_account_url(
reconfigure_entry.data[CONF_ACCOUNT_NAME]
),
container_name=user_input[CONF_CONTAINER_NAME],
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
)
errors = await self.validate_config(container_client)
if not errors:
+66 -10
View File
@@ -55,6 +55,7 @@ from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
deprecated_function,
dir_with_deprecated_constants,
)
from homeassistant.helpers.entity import Entity, EntityDescription
@@ -85,10 +86,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCAnswer,
WebRTCCandidate, # noqa: F401
WebRTCClientConfiguration,
WebRTCError, # noqa: F401
WebRTCError,
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
@@ -472,6 +473,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
self._supports_native_async_webrtc = (
type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer
@@ -575,6 +579,15 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
return None
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Handle the WebRTC offer and return an answer.
This is used by cameras with CameraEntityFeature.STREAM
and StreamType.WEB_RTC.
Integrations can override with a native WebRTC implementation.
"""
async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
@@ -587,6 +600,42 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation.
"""
if self._supports_native_sync_webrtc:
try:
answer = await deprecated_function(
"async_handle_async_webrtc_offer",
breaks_in_ha_version="2025.6",
)(self.async_handle_web_rtc_offer)(offer_sdp)
except ValueError as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
send_message(
WebRTCError(
"webrtc_offer_failed",
str(ex),
)
)
except TimeoutError:
# This catch was already here and should stay through the deprecation
_LOGGER.error("Timeout handling WebRTC offer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"Timeout handling WebRTC offer",
)
)
else:
if answer:
send_message(WebRTCAnswer(answer))
else:
_LOGGER.error("Error handling WebRTC offer: No answer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"No answer on WebRTC offer",
)
)
return
if self._webrtc_provider:
await self._webrtc_provider.async_handle_async_webrtc_offer(
self, offer_sdp, session_id, send_message
@@ -715,7 +764,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
new_provider = None
# Skip all providers if the camera has a native WebRTC implementation
if not self._supports_native_async_webrtc:
if not (
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
):
# Camera doesn't have a native WebRTC implementation
new_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_provider
@@ -747,12 +798,17 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration()
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
if not self._supports_native_sync_webrtc:
# Until 2024.11, the frontend was not resolving any ice servers
# The async approach was added 2024.11 and new integrations need to use it
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = self._supports_native_sync_webrtc
return config
@@ -782,7 +838,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_async_webrtc:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
else:
@@ -111,11 +111,13 @@ class WebRTCClientConfiguration:
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
data_channel: str | None = None
get_candidates_upfront: bool = False
def to_frontend_dict(self) -> dict[str, Any]:
"""Return a dict that can be used by the frontend."""
data: dict[str, Any] = {
"configuration": self.configuration.to_dict(),
"getCandidatesUpfront": self.get_candidates_upfront,
}
if self.data_channel is not None:
data["dataChannel"] = self.data_channel
+1 -7
View File
@@ -43,7 +43,6 @@ VALID_REPAIR_TRANSLATION_KEYS = {
"no_subscription",
"warn_bad_custom_domain_configuration",
"reset_bad_custom_domain_configuration",
"subscription_expired",
}
@@ -405,12 +404,7 @@ class CloudClient(Interface):
) -> None:
"""Create a repair issue."""
if translation_key not in VALID_REPAIR_TRANSLATION_KEYS:
_LOGGER.error(
"Invalid translation key %s for repair issue %s",
translation_key,
identifier,
)
return
raise ValueError(f"Invalid translation key {translation_key}")
async_create_issue(
hass=self._hass,
domain=DOMAIN,
@@ -73,10 +73,6 @@
"reset_bad_custom_domain_configuration": {
"title": "Custom domain ignored",
"description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. This domain has now been ignored and will not be used for Home Assistant Cloud. If you want to use this domain, please fix the DNS configuration and restart Home Assistant. If you do not need this anymore, you can remove it from the account page."
},
"subscription_expired": {
"title": "Subscription has expired",
"description": "Your Home Assistant Cloud subscription has expired. Resubscribe at {account_url}."
}
},
"services": {
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "bronze",
"requirements": ["aiocomelit==0.12.1"]
"requirements": ["aiocomelit==0.12.0"]
}
@@ -44,7 +44,6 @@ def websocket_list_areas(
vol.Optional("humidity_entity_id"): vol.Any(str, None),
vol.Optional("icon"): str,
vol.Optional("labels"): [str],
vol.Optional("motion_entity_id"): vol.Any(str, None),
vol.Required("name"): str,
vol.Optional("picture"): vol.Any(str, None),
vol.Optional("temperature_entity_id"): vol.Any(str, None),
@@ -113,7 +112,6 @@ def websocket_delete_area(
vol.Optional("humidity_entity_id"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
vol.Optional("labels"): [str],
vol.Optional("motion_entity_id"): vol.Any(str, None),
vol.Optional("name"): str,
vol.Optional("picture"): vol.Any(str, None),
vol.Optional("temperature_entity_id"): vol.Any(str, None),
@@ -9,12 +9,13 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
from homeassistant.helpers.json import json_dumps
@@ -22,6 +23,7 @@ from homeassistant.helpers.json import json_dumps
def async_setup(hass: HomeAssistant) -> bool:
"""Enable the Entity Registry views."""
websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids)
websocket_api.async_register_command(hass, websocket_get_entities)
websocket_api.async_register_command(hass, websocket_get_entity)
websocket_api.async_register_command(hass, websocket_list_entities_for_display)
@@ -316,3 +318,43 @@ def websocket_remove_entity(
registry.async_remove(msg["entity_id"])
connection.send_message(websocket_api.result_message(msg["id"]))
@websocket_api.websocket_command(
{
vol.Required("type"): "config/entity_registry/get_automatic_entity_ids",
vol.Required("entity_ids"): cv.entity_ids,
}
)
@callback
def websocket_get_automatic_entity_ids(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return the automatic entity IDs for the given entity IDs.
This is used to help user reset entity IDs which have been customized by the user.
"""
registry = er.async_get(hass)
entity_ids = msg["entity_ids"]
automatic_entity_ids: dict[str, str | None] = {}
for entity_id in entity_ids:
if not (entry := registry.entities.get(entity_id)):
automatic_entity_ids[entity_id] = None
continue
if (
suggested := async_get_entity_suggested_object_id(hass, entity_id)
) == split_entity_id(entry.entity_id)[1]:
# No need to generate a new entity ID
automatic_entity_ids[entity_id] = None
continue
automatic_entity_ids[entity_id] = registry.async_generate_entity_id(
entry.domain,
suggested or f"{entry.platform}_{entry.unique_id}",
)
connection.send_message(
websocket_api.result_message(msg["id"], automatic_entity_ids)
)
@@ -6,5 +6,5 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["debugpy==1.8.14"]
"requirements": ["debugpy==1.8.13"]
}
+1 -1
View File
@@ -14,7 +14,7 @@
],
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.0",
"aiodhcpwatcher==1.1.1",
"aiodiscover==2.7.0",
"cached-ipaddress==0.10.0"
]
@@ -107,7 +107,7 @@
"ac_module_temperature_sensor_faulty_l2": "AC module temperature sensor faulty (L2)",
"dc_component_measured_in_grid_too_high": "DC component measured in the grid too high",
"fixed_voltage_mode_out_of_range": "Fixed voltage mode has been selected instead of MPP voltage mode and the fixed voltage has been set to too low or too high a value",
"safety_cut_out_triggered": "Safety cut-out via option card or RECERBO has triggered",
"safety_cut_out_triggered": "Safety cut out via option card or RECERBO has triggered",
"no_communication_between_power_stage_and_control_system": "No communication possible between power stage set and control system",
"hardware_id_problem": "Hardware ID problem",
"unique_id_conflict": "Unique ID conflict",
@@ -148,7 +148,7 @@
"update_file_does_not_match_device": "Update file does not match the device, update file too old",
"write_or_read_error_occurred": "Write or read error occurred",
"file_could_not_be_opened": "File could not be opened",
"log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write-protected or full)",
"log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write protected or full)",
"initialisation_error_file_system_error_on_usb": "Initialization error in file system on USB flash drive",
"error_during_logging_data_recording": "Error during recording of logging data",
"error_during_update_process": "Error occurred during update process",
@@ -166,7 +166,7 @@
"invalid_device_type": "Invalid device type",
"insulation_measurement_triggered": "Insulation measurement triggered",
"inverter_settings_changed_restart_required": "Inverter settings have been changed, inverter restart required",
"wired_shut_down_triggered": "Wired shutdown triggered",
"wired_shut_down_triggered": "Wired shut down triggered",
"grid_frequency_exceeded_limit_reconnecting": "The grid frequency has exceeded a limit value when reconnecting",
"mains_voltage_dependent_power_reduction": "Mains voltage-dependent power reduction",
"too_little_dc_power_for_feed_in_operation": "Too little DC power for feed-in operation",
+49 -94
View File
@@ -14,78 +14,49 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store
from homeassistant.util.hass_dict import HassKey
DATA_STORAGE: HassKey[dict[str, UserStore]] = HassKey("frontend_storage")
DATA_STORAGE: HassKey[tuple[dict[str, Store], dict[str, dict]]] = HassKey(
"frontend_storage"
)
STORAGE_VERSION_USER_DATA = 1
@callback
def _initialize_frontend_storage(hass: HomeAssistant) -> None:
"""Set up frontend storage."""
if DATA_STORAGE in hass.data:
return
hass.data[DATA_STORAGE] = ({}, {})
async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
"""Set up frontend storage."""
_initialize_frontend_storage(hass)
websocket_api.async_register_command(hass, websocket_set_user_data)
websocket_api.async_register_command(hass, websocket_get_user_data)
websocket_api.async_register_command(hass, websocket_subscribe_user_data)
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
async def async_user_store(
hass: HomeAssistant, user_id: str
) -> tuple[Store, dict[str, Any]]:
"""Access a user store."""
stores = hass.data.setdefault(DATA_STORAGE, {})
_initialize_frontend_storage(hass)
stores, data = hass.data[DATA_STORAGE]
if (store := stores.get(user_id)) is None:
store = stores[user_id] = UserStore(hass, user_id)
await store.async_load()
return store
class UserStore:
"""User store for frontend data."""
def __init__(self, hass: HomeAssistant, user_id: str) -> None:
"""Initialize the user store."""
self._store = _UserStore(hass, user_id)
self.data: dict[str, Any] = {}
self.subscriptions: dict[str | None, list[Callable[[], None]]] = {}
async def async_load(self) -> None:
"""Load the data from the store."""
self.data = await self._store.async_load() or {}
async def async_set_item(self, key: str, value: Any) -> None:
"""Set an item item and save the store."""
self.data[key] = value
await self._store.async_save(self.data)
for cb in self.subscriptions.get(None, []):
cb()
for cb in self.subscriptions.get(key, []):
cb()
@callback
def async_subscribe(
self, key: str | None, on_update_callback: Callable[[], None]
) -> Callable[[], None]:
"""Save the data to the store."""
self.subscriptions.setdefault(key, []).append(on_update_callback)
def unsubscribe() -> None:
"""Unsubscribe from the store."""
self.subscriptions[key].remove(on_update_callback)
return unsubscribe
class _UserStore(Store[dict[str, Any]]):
"""User store for frontend data."""
def __init__(self, hass: HomeAssistant, user_id: str) -> None:
"""Initialize the user store."""
super().__init__(
store = stores[user_id] = Store(
hass,
STORAGE_VERSION_USER_DATA,
f"frontend.user_data_{user_id}",
)
if user_id not in data:
data[user_id] = await store.async_load() or {}
def with_user_store(
return store, data[user_id]
def with_store(
orig_func: Callable[
[HomeAssistant, ActiveConnection, dict[str, Any], UserStore],
[HomeAssistant, ActiveConnection, dict[str, Any], Store, dict[str, Any]],
Coroutine[Any, Any, None],
],
) -> Callable[
@@ -94,17 +65,17 @@ def with_user_store(
"""Decorate function to provide data."""
@wraps(orig_func)
async def with_user_store_func(
async def with_store_func(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Provide user specific data and store to function."""
user_id = connection.user.id
store = await async_user_store(hass, user_id)
store, user_data = await async_user_store(hass, user_id)
await orig_func(hass, connection, msg, store)
await orig_func(hass, connection, msg, store, user_data)
return with_user_store_func
return with_store_func
@websocket_api.websocket_command(
@@ -115,57 +86,41 @@ def with_user_store(
}
)
@websocket_api.async_response
@with_user_store
@with_store
async def websocket_set_user_data(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict[str, Any],
store: UserStore,
store: Store,
data: dict[str, Any],
) -> None:
"""Handle set user data command."""
await store.async_set_item(msg["key"], msg["value"])
connection.send_result(msg["id"])
"""Handle set global data command.
Async friendly.
"""
data[msg["key"]] = msg["value"]
await store.async_save(data)
connection.send_message(websocket_api.result_message(msg["id"]))
@websocket_api.websocket_command(
{vol.Required("type"): "frontend/get_user_data", vol.Optional("key"): str}
)
@websocket_api.async_response
@with_user_store
@with_store
async def websocket_get_user_data(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict[str, Any],
store: UserStore,
store: Store,
data: dict[str, Any],
) -> None:
"""Handle get user data command."""
data = store.data
connection.send_result(
msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data}
)
"""Handle get global data command.
@websocket_api.websocket_command(
{vol.Required("type"): "frontend/subscribe_user_data", vol.Optional("key"): str}
)
@websocket_api.async_response
@with_user_store
async def websocket_subscribe_user_data(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict[str, Any],
store: UserStore,
) -> None:
"""Handle subscribe to user data command."""
key: str | None = msg.get("key")
def on_data_update() -> None:
"""Handle user data update."""
data = store.data
connection.send_event(
msg["id"], {"value": data.get(key) if key is not None else data}
Async friendly.
"""
connection.send_message(
websocket_api.result_message(
msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data}
)
connection.subscriptions[msg["id"]] = store.async_subscribe(key, on_data_update)
on_data_update()
connection.send_result(msg["id"])
)
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.1"]
}
@@ -8,8 +8,7 @@ import jwt
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
@@ -59,22 +58,3 @@ class OAuth2FlowHandler(
)
self._abort_if_unique_id_configured()
return await super().async_oauth_create_entry(data)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a DHCP discovery."""
device_registry = dr.async_get(self.hass)
if device_entry := device_registry.async_get_device(
identifiers={
(DOMAIN, discovery_info.hostname),
(DOMAIN, discovery_info.hostname.split("-")[-1]),
}
):
device_registry.async_update_device(
device_entry.id,
new_connections={
(dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress)
},
)
return await super().async_step_dhcp(discovery_info)
@@ -110,14 +110,14 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity):
mower_attributes = self.mower_attributes
if mower_attributes.mower.state in PAUSED_STATES:
return LawnMowerActivity.PAUSED
if (mower_attributes.mower.state == "RESTRICTED") or (
mower_attributes.mower.activity in DOCKED_ACTIVITIES
):
return LawnMowerActivity.DOCKED
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
return LawnMowerActivity.RETURNING
return LawnMowerActivity.MOWING
if (mower_attributes.mower.state == "RESTRICTED") or (
mower_attributes.mower.activity in DOCKED_ACTIVITIES
):
return LawnMowerActivity.DOCKED
return LawnMowerActivity.ERROR
@property
@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"iot_class": "local_polling",
"requirements": ["automower-ble==0.2.1"]
"requirements": ["automower-ble==0.2.0"]
}
+1 -1
View File
@@ -10,7 +10,7 @@
"iot_class": "local_push",
"loggers": ["xknx", "xknxproject"],
"requirements": [
"xknx==3.8.0",
"xknx==3.6.0",
"xknxproject==3.8.2",
"knx-frontend==2025.4.1.91934"
],
+26 -17
View File
@@ -19,6 +19,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -43,6 +44,21 @@ CONFIG_SCHEMA = vol.Schema(CONFIG_DATA)
USER_SCHEMA = vol.Schema(USER_DATA)
def get_config_entry(
hass: HomeAssistant, data: ConfigType
) -> config_entries.ConfigEntry | None:
"""Check config entries for already configured entries based on the ip address/port."""
return next(
(
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.data[CONF_IP_ADDRESS] == data[CONF_IP_ADDRESS]
and entry.data[CONF_PORT] == data[CONF_PORT]
),
None,
)
async def validate_connection(data: ConfigType) -> str | None:
"""Validate if a connection to LCN can be established."""
error = None
@@ -104,20 +120,19 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None:
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)
self._async_abort_entries_match(
{
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
}
)
errors = None
if get_config_entry(self.hass, user_input):
errors = {CONF_BASE: "already_configured"}
elif (error := await validate_connection(user_input)) is not None:
errors = {CONF_BASE: error}
if (error := await validate_connection(user_input)) is not None:
if errors is not None:
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
USER_SCHEMA, user_input
),
errors={CONF_BASE: error},
errors=errors,
)
data: dict = {
@@ -137,21 +152,15 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
user_input[CONF_HOST] = reconfigure_entry.data[CONF_HOST]
self._async_abort_entries_match(
{
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
}
)
await self.hass.config_entries.async_unload(reconfigure_entry.entry_id)
if (error := await validate_connection(user_input)) is not None:
errors = {CONF_BASE: error}
if (error := await validate_connection(user_input)) is None:
if errors is None:
return self.async_update_reload_and_abort(
reconfigure_entry, data_updates=user_input
)
errors = {CONF_BASE: error}
await self.hass.config_entries.async_setup(reconfigure_entry.entry_id)
return self.async_show_form(
+3 -3
View File
@@ -66,11 +66,11 @@
"error": {
"authentication_error": "Authentication failed. Wrong username or password.",
"license_error": "Maximum number of connections was reached. An additional licence key is required.",
"connection_refused": "Unable to connect to PCHK. Check IP and port."
"connection_refused": "Unable to connect to PCHK. Check IP and port.",
"already_configured": "PCHK connection using the same ip address/port is already configured."
},
"abort": {
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"already_configured": "PCHK connection using the same ip address/port is already configured."
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"issues": {
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==9.2.2"]
"requirements": ["ical==9.2.1"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==9.2.2"]
"requirements": ["ical==9.2.1"]
}
@@ -23,8 +23,6 @@ from .const import MieleAppliance
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
-2
View File
@@ -17,8 +17,6 @@ from .const import DOMAIN, PROCESS_ACTION, MieleActions, MieleAppliance
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
@@ -26,8 +26,6 @@ from .const import DEVICE_TYPE_TAGS, DISABLED_TEMP_ENTITIES, DOMAIN, MieleApplia
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
-2
View File
@@ -27,8 +27,6 @@ from .const import DOMAIN, POWER_OFF, POWER_ON, VENTILATION_STEP, MieleAppliance
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
SPEED_RANGE = (1, 4)
@@ -32,9 +32,6 @@
"core_target_temperature": {
"default": "mdi:thermometer-probe"
},
"target_temperature": {
"default": "mdi:thermometer-check"
},
"drying_step": {
"default": "mdi:water-outline"
},
-2
View File
@@ -23,8 +23,6 @@ from .const import AMBIENT_LIGHT, DOMAIN, LIGHT, LIGHT_OFF, LIGHT_ON, MieleAppli
from .coordinator import MieleConfigEntry
from .entity import MieleDevice, MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "cloud_push",
"loggers": ["pymiele"],
"quality_scale": "bronze",
"requirements": ["pymiele==0.5.2"],
"requirements": ["pymiele==0.5.1"],
"single_config_entry": true,
"zeroconf": ["_mieleathome._tcp.local."]
}
@@ -32,23 +32,18 @@ rules:
Handled by a setting in manifest.json as there is no account information in API
# Silver
action-exceptions:
status: done
comment: No custom actions are defined
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No configuration parameters
docs-installation-parameters:
status: exempt
comment: |
Integration uses account linking via Nabu casa so no installation parameters are needed.
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable:
status: done
comment: Handled by DataUpdateCoordinator
parallel-updates: done
log-when-unavailable: todo
parallel-updates:
status: exempt
comment: Handled by coordinator
reauthentication-flow: done
test-coverage: todo
-26
View File
@@ -39,8 +39,6 @@ from .const import (
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
DISABLED_TEMPERATURE = -32768
@@ -384,7 +382,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_core_target_temperature",
@@ -401,29 +398,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHER_DRYER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_target_temperature",
translation_key="target_temperature",
zone=1,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=(
lambda value: cast(int, value.state_target_temperature[0].temperature)
/ 100.0
),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.OVEN,
@@ -876,9 +876,6 @@
"core_temperature": {
"name": "Core temperature"
},
"target_temperature": {
"name": "Target temperature"
},
"core_target_temperature": {
"name": "Core target temperature"
}
-2
View File
@@ -28,8 +28,6 @@ from .const import (
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
-2
View File
@@ -24,8 +24,6 @@ from .const import DOMAIN, PROCESS_ACTION, PROGRAM_ID, MieleActions, MieleApplia
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
# The following const classes define program speeds and programs for the vacuum cleaner.
+1 -1
View File
@@ -18,7 +18,7 @@
"sections": {
"auth": {
"name": "Authentication",
"description": "Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can provide a username and password. Home Assistant will automatically generate an access token to authenticate with ntfy.",
"description": "Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can provide a username and password.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
@@ -32,8 +32,6 @@ from .const import (
PLACEHOLDER_WEBHOOK_URL,
)
AUTH_TOKEN_URL = "https://intercom.help/plaato/en/articles/5004720-auth_token"
class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handles a Plaato config flow."""
@@ -155,10 +153,7 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="api_method",
data_schema=data_schema,
errors=errors,
description_placeholders={
PLACEHOLDER_DEVICE_TYPE: device_type.name,
"auth_token_url": AUTH_TOKEN_URL,
},
description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name},
)
async def _get_webhook_id(self):
+1 -1
View File
@@ -11,7 +11,7 @@
},
"api_method": {
"title": "Select API method",
"description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions]({auth_token_url})\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank",
"description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Selected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank",
"data": {
"use_webhook": "Use webhook",
"token": "Auth token"
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==9.2.2"]
"requirements": ["ical==9.2.1"]
}
+30 -3
View File
@@ -21,19 +21,26 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from .bridge import SamsungTVBridge, mac_from_device_info, model_requires_encryption
from .bridge import (
SamsungTVBridge,
async_get_device_info,
mac_from_device_info,
model_requires_encryption,
)
from .const import (
CONF_SESSION_ID,
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
CONF_SSDP_RENDERING_CONTROL_LOCATION,
DOMAIN,
ENTRY_RELOAD_COOLDOWN,
LEGACY_PORT,
LOGGER,
METHOD_ENCRYPTED_WEBSOCKET,
METHOD_LEGACY,
UPNP_SVC_MAIN_TV_AGENT,
UPNP_SVC_RENDERING_CONTROL,
)
@@ -173,10 +180,30 @@ async def _async_create_bridge_with_updated_data(
"""Create a bridge object and update any missing data in the config entry."""
updated_data: dict[str, str | int] = {}
host: str = entry.data[CONF_HOST]
method: str = entry.data[CONF_METHOD]
port: int | None = entry.data.get(CONF_PORT)
method: str | None = entry.data.get(CONF_METHOD)
load_info_attempted = False
info: dict[str, Any] | None = None
if not port or not method:
LOGGER.debug("Attempting to get port or method for %s", host)
if method == METHOD_LEGACY:
port = LEGACY_PORT
else:
# When we imported from yaml we didn't setup the method
# because we didn't know it
_result, port, method, info = await async_get_device_info(hass, host)
load_info_attempted = True
if not port or not method:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_determine_connection_method",
)
LOGGER.debug("Updated port to %s and method to %s for %s", port, method, host)
updated_data[CONF_PORT] = port
updated_data[CONF_METHOD] = method
bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data})
mac: str | None = entry.data.get(CONF_MAC)
@@ -56,6 +56,7 @@ from .const import (
RESULT_INVALID_PIN,
RESULT_NOT_SUPPORTED,
RESULT_SUCCESS,
RESULT_UNKNOWN_HOST,
SUCCESSFUL_RESULTS,
UPNP_SVC_MAIN_TV_AGENT,
UPNP_SVC_RENDERING_CONTROL,
@@ -109,11 +110,9 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 2
MINOR_VERSION = 2
_host: str
_bridge: SamsungTVBridge
def __init__(self) -> None:
"""Initialize flow."""
self._host: str = ""
self._mac: str | None = None
self._udn: str | None = None
self._upnp_udn: str | None = None
@@ -126,11 +125,13 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
self._name: str | None = None
self._title: str = ""
self._id: int | None = None
self._bridge: SamsungTVBridge | None = None
self._device_info: dict[str, Any] | None = None
self._authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = None
def _base_config_entry(self) -> dict[str, Any]:
"""Generate the base config entry without the method."""
assert self._bridge is not None
return {
CONF_HOST: self._host,
CONF_MAC: self._mac,
@@ -144,6 +145,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
def _get_entry_from_bridge(self) -> ConfigFlowResult:
"""Get device entry."""
assert self._bridge
data = self._base_config_entry()
if self._bridge.token:
data[CONF_TOKEN] = self._bridge.token
@@ -163,6 +165,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
self, raise_on_progress: bool = True
) -> None:
"""Set the unique id from the udn."""
assert self._host is not None
# Set the unique id without raising on progress in case
# there are two SSDP flows with for each ST
await self.async_set_unique_id(self._udn, raise_on_progress=False)
@@ -249,44 +252,38 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
self._mac = mac
return True
async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> bool:
async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> None:
try:
self._host = await self.hass.async_add_executor_job(
socket.gethostbyname, user_input[CONF_HOST]
)
except socket.gaierror as err:
LOGGER.debug("Failed to get IP for %s: %s", user_input[CONF_HOST], err)
return False
raise AbortFlow(RESULT_UNKNOWN_HOST) from err
self._title = self._host
return True
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] | None = None
if user_input is not None:
if await self._async_set_name_host_from_input(user_input):
await self._async_create_bridge()
self._async_abort_entries_match({CONF_HOST: self._host})
if self._bridge.method != METHOD_LEGACY:
# Legacy bridge does not provide device info
await self._async_set_device_unique_id(raise_on_progress=False)
if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET:
return await self.async_step_encrypted_pairing()
return await self.async_step_pairing({})
errors = {"base": "invalid_host"}
await self._async_set_name_host_from_input(user_input)
await self._async_create_bridge()
assert self._bridge
self._async_abort_entries_match({CONF_HOST: self._host})
if self._bridge.method != METHOD_LEGACY:
# Legacy bridge does not provide device info
await self._async_set_device_unique_id(raise_on_progress=False)
if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET:
return await self.async_step_encrypted_pairing()
return await self.async_step_pairing({})
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input),
errors=errors,
)
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
async def async_step_pairing(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a pairing by accepting the message on the TV."""
assert self._bridge is not None
errors: dict[str, str] = {}
if user_input is not None:
result = await self._bridge.async_try_connect()
@@ -308,6 +305,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a encrypted pairing."""
assert self._host is not None
await self._async_start_encrypted_pairing(self._host)
assert self._authenticator is not None
errors: dict[str, str] = {}
@@ -422,6 +420,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def _async_start_discovery_with_mac_address(self) -> None:
"""Start discovery."""
assert self._host is not None
if (entry := self._async_update_existing_matching_entry()) and entry.unique_id:
# If we have the unique id and the mac we abort
# as we do not need anything else
@@ -519,6 +518,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle user-confirmation of discovered node."""
if user_input is not None:
await self._async_create_bridge()
assert self._bridge
if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET:
return await self.async_step_encrypted_pairing()
return await self.async_step_pairing({})
@@ -43,7 +43,6 @@
},
"error": {
"auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]",
"invalid_host": "Host is invalid, please try again.",
"invalid_pin": "PIN is invalid, please try again."
},
"abort": {
@@ -53,6 +52,7 @@
"id_missing": "This Samsung device doesn't have a SerialNumber.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"not_supported": "This Samsung device is currently not supported.",
"unknown": "[%key:common::config_flow::error::unknown%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
+2 -19
View File
@@ -33,11 +33,7 @@ from homeassistant.const import (
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
CONNECTION_NETWORK_MAC,
format_mac,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .bluetooth import async_connect_scanner
@@ -164,11 +160,6 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
"""Sleep period of the device."""
return self.config_entry.data.get(CONF_SLEEP_PERIOD, 0)
@property
def connections(self) -> set[tuple[str, str]]:
"""Connections of the device."""
return {(CONNECTION_NETWORK_MAC, self.mac)}
def async_setup(self, pending_platforms: list[Platform] | None = None) -> None:
"""Set up the coordinator."""
self._pending_platforms = pending_platforms
@@ -176,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
device_entry = dev_reg.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
name=self.name,
connections=self.connections,
connections={(CONNECTION_NETWORK_MAC, self.mac)},
identifiers={(DOMAIN, self.mac)},
manufacturer="Shelly",
model=get_shelly_model_name(self.model, self.sleep_period, self.device),
@@ -532,14 +523,6 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
"""
return format_mac(bluetooth_mac_from_primary_mac(self.mac)).upper()
@property
def connections(self) -> set[tuple[str, str]]:
"""Connections of the device."""
connections = super().connections
if not self.sleep_period:
connections.add((CONNECTION_BLUETOOTH, self.bluetooth_source))
return connections
async def async_device_online(self, source: str) -> None:
"""Handle device going online."""
if not self.sleep_period:
+1 -1
View File
@@ -34,7 +34,7 @@
}
},
"confirm_discovery": {
"description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password-protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password-protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
"description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
},
"reconfigure": {
"description": "Update configuration for {device_name}.\n\nBefore setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.",
-136
View File
@@ -1,136 +0,0 @@
"""Offer sun based automation rules."""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import cast
import voluptuous as vol
from homeassistant.const import CONF_CONDITION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import (
ConditionCheckerType,
condition_trace_set_result,
condition_trace_update_result,
trace_condition_function,
)
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from homeassistant.util import dt as dt_util
CONDITION_SCHEMA = vol.All(
vol.Schema(
{
**cv.CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "sun",
vol.Optional("before"): cv.sun_event,
vol.Optional("before_offset"): cv.time_period,
vol.Optional("after"): vol.All(
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
),
vol.Optional("after_offset"): cv.time_period,
}
),
cv.has_at_least_one_key("before", "after"),
)
def sun(
hass: HomeAssistant,
before: str | None = None,
after: str | None = None,
before_offset: timedelta | None = None,
after_offset: timedelta | None = None,
) -> bool:
"""Test if current time matches sun requirements."""
utcnow = dt_util.utcnow()
today = dt_util.as_local(utcnow).date()
before_offset = before_offset or timedelta(0)
after_offset = after_offset or timedelta(0)
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
if after_sunrise and has_sunrise_condition:
tomorrow = today + timedelta(days=1)
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
if after_sunset and has_sunset_condition:
tomorrow = today + timedelta(days=1)
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
# Special case: before sunrise OR after sunset
# This will handle the very rare case in the polar region when the sun rises/sets
# but does not set/rise.
# However this entire condition does not handle those full days of darkness
# or light, the following should be used instead:
#
# condition:
# condition: state
# entity_id: sun.sun
# state: 'above_horizon' (or 'below_horizon')
#
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
wanted_time_after = cast(datetime, sunset) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
return utcnow < wanted_time_before or utcnow > wanted_time_after
if sunrise is None and has_sunrise_condition:
# There is no sunrise today
condition_trace_set_result(False, message="no sunrise today")
return False
if sunset is None and has_sunset_condition:
# There is no sunset today
condition_trace_set_result(False, message="no sunset today")
return False
if before == SUN_EVENT_SUNRISE:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
if utcnow > wanted_time_before:
return False
if before == SUN_EVENT_SUNSET:
wanted_time_before = cast(datetime, sunset) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
if utcnow > wanted_time_before:
return False
if after == SUN_EVENT_SUNRISE:
wanted_time_after = cast(datetime, sunrise) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
if utcnow < wanted_time_after:
return False
if after == SUN_EVENT_SUNSET:
wanted_time_after = cast(datetime, sunset) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
if utcnow < wanted_time_after:
return False
return True
def async_condition_from_config(config: ConfigType) -> ConditionCheckerType:
"""Wrap action method with sun based condition."""
before = config.get("before")
after = config.get("after")
before_offset = config.get("before_offset")
after_offset = config.get("after_offset")
@trace_condition_function
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate time based if-condition."""
return sun(hass, before, after, before_offset, after_offset)
return sun_if
@@ -32,8 +32,7 @@
"@RenierM26",
"@murtas",
"@Eloston",
"@dsypniewski",
"@zerzhang"
"@dsypniewski"
],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
@@ -95,7 +95,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
energysites: list[TeslemetryEnergyData] = []
# Create the stream
stream: TeslemetryStream | None = None
stream = TeslemetryStream(
session,
access_token,
server=f"{region.lower()}.teslemetry.com",
parse_timestamp=True,
manual=True,
)
for product in products:
if (
@@ -117,16 +123,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
serial_number=vin,
)
# Create stream if required
if not stream:
stream = TeslemetryStream(
session,
access_token,
server=f"{region.lower()}.teslemetry.com",
parse_timestamp=True,
manual=True,
)
remove_listener = stream.async_add_listener(
create_handle_vehicle_stream(vin, coordinator),
{"vin": vin},
@@ -244,8 +240,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
entry.runtime_data = TeslemetryData(vehicles, energysites, scopes)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
if stream:
entry.async_create_background_task(hass, stream.listen(), "Teslemetry Stream")
entry.async_create_background_task(hass, stream.listen(), "Teslemetry Stream")
return True
+2 -14
View File
@@ -469,7 +469,6 @@ class ResultStream:
use_file_cache: bool
language: str
options: dict
supports_streaming_input: bool
_manager: SpeechManager
@@ -485,10 +484,7 @@ class ResultStream:
@callback
def async_set_message(self, message: str) -> None:
"""Set message to be generated.
This method will leverage a disk cache to speed up generation.
"""
"""Set message to be generated."""
self._result_cache.set_result(
self._manager.async_cache_message_in_memory(
engine=self.engine,
@@ -501,10 +497,7 @@ class ResultStream:
@callback
def async_set_message_stream(self, message_stream: AsyncGenerator[str]) -> None:
"""Set a stream that will generate the message.
This method can result in faster first byte when generating long responses.
"""
"""Set a stream that will generate the message."""
self._result_cache.set_result(
self._manager.async_cache_message_stream_in_memory(
engine=self.engine,
@@ -733,10 +726,6 @@ class SpeechManager:
if (engine_instance := get_engine_instance(self.hass, engine)) is None:
raise HomeAssistantError(f"Provider {engine} not found")
supports_streaming_input = (
isinstance(engine_instance, TextToSpeechEntity)
and engine_instance.async_supports_streaming_input()
)
language, options = self.process_options(engine_instance, language, options)
if use_file_cache is None:
use_file_cache = self.use_file_cache
@@ -752,7 +741,6 @@ class SpeechManager:
engine=engine,
language=language,
options=options,
supports_streaming_input=supports_streaming_input,
_manager=self,
)
self.token_to_stream[token] = result_stream
-7
View File
@@ -89,13 +89,6 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH
"""Return a mapping with the default options."""
return self._attr_default_options
@classmethod
def async_supports_streaming_input(cls) -> bool:
"""Return if the TTS engine supports streaming input."""
return (
cls.async_stream_tts_audio is not TextToSpeechEntity.async_stream_tts_audio
)
@callback
def async_get_supported_voices(self, language: str) -> list[Voice] | None:
"""Return a list of supported voices for a language."""
+3 -3
View File
@@ -21,7 +21,7 @@
"tls": "Enable this if you use a secure connection to your Velbus interface, like a Signum.",
"host": "The IP address or hostname of the Velbus interface.",
"port": "The port number of the Velbus interface.",
"password": "The password of the Velbus interface, this is only needed if the interface is password-protected."
"password": "The password of the Velbus interface, this is only needed if the interface is password protected."
},
"description": "TCP/IP configuration, in case you use a Signum, VelServ, velbus-tcp or any other Velbus to TCP/IP interface."
},
@@ -58,7 +58,7 @@
"services": {
"sync_clock": {
"name": "Sync clock",
"description": "Syncs the clock of the Velbus modules to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.",
"description": "Syncs the Velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.",
"fields": {
"interface": {
"name": "Interface",
@@ -104,7 +104,7 @@
},
"set_memo_text": {
"name": "Set memo text",
"description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD. Be sure the pages of the modules are configured to display the memo text.",
"description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text.",
"fields": {
"interface": {
"name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]",
+12 -19
View File
@@ -65,7 +65,7 @@ def setup_platform(
name = travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)
sensors.append(
WashingtonStateTravelTimeSensor(
name, config[CONF_API_KEY], travel_time.get(CONF_ID)
name, config.get(CONF_API_KEY), travel_time.get(CONF_ID)
)
)
@@ -82,20 +82,20 @@ class WashingtonStateTransportSensor(SensorEntity):
_attr_icon = ICON
def __init__(self, name: str, access_code: str) -> None:
def __init__(self, name, access_code):
"""Initialize the sensor."""
self._data: dict[str, str | int | None] = {}
self._data = {}
self._access_code = access_code
self._name = name
self._state: int | None = None
self._state = None
@property
def name(self) -> str:
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def native_value(self) -> int | None:
def native_value(self):
"""Return the state of the sensor."""
return self._state
@@ -106,7 +106,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
_attr_attribution = ATTRIBUTION
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
def __init__(self, name: str, access_code: str, travel_time_id: str) -> None:
def __init__(self, name, access_code, travel_time_id):
"""Construct a travel time sensor."""
self._travel_time_id = travel_time_id
WashingtonStateTransportSensor.__init__(self, name, access_code)
@@ -123,17 +123,13 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
_LOGGER.warning("Invalid response from WSDOT API")
else:
self._data = response.json()
_state = self._data.get(ATTR_CURRENT_TIME)
if not isinstance(_state, int):
self._state = None
else:
self._state = _state
self._state = self._data.get(ATTR_CURRENT_TIME)
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return other details about the sensor state."""
if self._data is not None:
attrs: dict[str, str | int | None | datetime] = {}
attrs = {}
for key in (
ATTR_AVG_TIME,
ATTR_NAME,
@@ -148,15 +144,12 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
return None
def _parse_wsdot_timestamp(timestamp: Any) -> datetime | None:
def _parse_wsdot_timestamp(timestamp):
"""Convert WSDOT timestamp to datetime."""
if not isinstance(timestamp, str):
if not timestamp:
return None
# ex: Date(1485040200000-0800)
timestamp_parts = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp)
if timestamp_parts is None:
return None
milliseconds, tzone = timestamp_parts.groups()
milliseconds, tzone = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp).groups()
return datetime.fromtimestamp(
int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone)))
)
@@ -278,39 +278,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# and we'll handle the clean up below.
await driver_events.setup(driver)
if (old_unique_id := entry.unique_id) is not None and old_unique_id != (
new_unique_id := str(driver.controller.home_id)
):
device_registry = dr.async_get(hass)
controller_model = "Unknown model"
if (
(own_node := driver.controller.own_node)
and (
controller_device_entry := device_registry.async_get_device(
identifiers={get_device_id(driver, own_node)}
)
)
and (model := controller_device_entry.model)
):
controller_model = model
async_create_issue(
hass,
DOMAIN,
f"migrate_unique_id.{entry.entry_id}",
data={
"config_entry_id": entry.entry_id,
"config_entry_title": entry.title,
"controller_model": controller_model,
"new_unique_id": new_unique_id,
"old_unique_id": old_unique_id,
},
is_fixable=True,
severity=IssueSeverity.ERROR,
translation_key="migrate_unique_id",
)
else:
async_delete_issue(hass, DOMAIN, f"migrate_unique_id.{entry.entry_id}")
# If the listen task is already failed, we need to raise ConfigEntryNotReady
if listen_task.done():
listen_error, error_message = _get_listen_task_error(listen_task)
-23
View File
@@ -71,7 +71,6 @@ from homeassistant.components.websocket_api import (
ActiveConnection,
)
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -89,16 +88,13 @@ from .const import (
DATA_CLIENT,
DOMAIN,
EVENT_DEVICE_ADDED_TO_REGISTRY,
LOGGER,
RESTORE_NVM_DRIVER_READY_TIMEOUT,
USER_AGENT,
)
from .helpers import (
CannotConnect,
async_enable_statistics,
async_get_node_from_device_id,
async_get_provisioning_entry_from_device_id,
async_get_version_info,
get_device_id,
)
@@ -2869,25 +2865,6 @@ async def websocket_hard_reset_controller(
async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
# When resetting the controller, the controller home id is also changed.
# The controller state in the client is stale after resetting the controller,
# so get the new home id with a new client using the helper function.
# The client state will be refreshed by reloading the config entry,
# after the unique id of the config entry has been updated.
try:
version_info = await async_get_version_info(hass, entry.data[CONF_URL])
except CannotConnect:
# Just log this error, as there's nothing to do about it here.
# The stale unique id needs to be handled by a repair flow,
# after the config entry has been reloaded.
LOGGER.error(
"Failed to get server version, cannot update config entry"
"unique id with new home id, after controller reset"
)
else:
hass.config_entries.async_update_entry(
entry, unique_id=str(version_info.home_id)
)
await hass.config_entries.async_reload(entry.entry_id)
@@ -9,13 +9,14 @@ import logging
from pathlib import Path
from typing import Any
import aiohttp
from awesomeversion import AwesomeVersion
from serial.tools import list_ports
import voluptuous as vol
from zwave_js_server.client import Client
from zwave_js_server.exceptions import FailedCommand
from zwave_js_server.model.driver import Driver
from zwave_js_server.version import VersionInfo
from zwave_js_server.version import VersionInfo, get_server_version
from homeassistant.components import usb
from homeassistant.components.hassio import (
@@ -35,6 +36,7 @@ from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.usb import UsbServiceInfo
@@ -67,7 +69,6 @@ from .const import (
DOMAIN,
RESTORE_NVM_DRIVER_READY_TIMEOUT,
)
from .helpers import CannotConnect, async_get_version_info
_LOGGER = logging.getLogger(__name__)
@@ -78,6 +79,7 @@ ADDON_SETUP_TIMEOUT = 5
ADDON_SETUP_TIMEOUT_ROUNDS = 40
CONF_EMULATE_HARDWARE = "emulate_hardware"
CONF_LOG_LEVEL = "log_level"
SERVER_VERSION_TIMEOUT = 10
ADDON_LOG_LEVELS = {
"error": "Error",
@@ -128,6 +130,22 @@ async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo:
raise InvalidInput("cannot_connect") from err
async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo:
"""Return Z-Wave JS version info."""
try:
async with asyncio.timeout(SERVER_VERSION_TIMEOUT):
version_info: VersionInfo = await get_server_version(
ws_address, async_get_clientsession(hass)
)
except (TimeoutError, aiohttp.ClientError) as err:
# We don't want to spam the log if the add-on isn't started
# or takes a long time to start.
_LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err)
raise CannotConnect from err
return version_info
def get_usb_ports() -> dict[str, str]:
"""Return a dict of USB ports and their friendly names."""
ports = list_ports.comports()
@@ -1339,6 +1357,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
return client.driver
class CannotConnect(HomeAssistantError):
"""Indicate connection error."""
class InvalidInput(HomeAssistantError):
"""Error to indicate input data is invalid."""
@@ -2,13 +2,11 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from dataclasses import astuple, dataclass
import logging
from typing import Any, cast
import aiohttp
import voluptuous as vol
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import (
@@ -27,7 +25,6 @@ from zwave_js_server.model.value import (
ValueDataType,
get_value_id_str,
)
from zwave_js_server.version import VersionInfo, get_server_version
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -41,7 +38,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.group import expand_entity_ids
from homeassistant.helpers.typing import ConfigType, VolSchemaType
@@ -58,8 +54,6 @@ from .const import (
LOGGER,
)
SERVER_VERSION_TIMEOUT = 10
@dataclass
class ZwaveValueID:
@@ -574,23 +568,3 @@ def get_network_identifier_for_notification(
return f"`{config_entry.title}`, with the home ID `{home_id}`,"
return f"with the home ID `{home_id}`"
return ""
async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo:
"""Return Z-Wave JS version info."""
try:
async with asyncio.timeout(SERVER_VERSION_TIMEOUT):
version_info: VersionInfo = await get_server_version(
ws_address, async_get_clientsession(hass)
)
except (TimeoutError, aiohttp.ClientError) as err:
# We don't want to spam the log if the add-on isn't started
# or takes a long time to start.
LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err)
raise CannotConnect from err
return version_info
class CannotConnect(HomeAssistantError):
"""Indicate connection error."""
@@ -57,47 +57,6 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
)
class MigrateUniqueIDFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
def __init__(self, data: dict[str, str]) -> None:
"""Initialize."""
self.description_placeholders: dict[str, str] = {
"config_entry_title": data["config_entry_title"],
"controller_model": data["controller_model"],
"new_unique_id": data["new_unique_id"],
"old_unique_id": data["old_unique_id"],
}
self._config_entry_id: str = data["config_entry_id"]
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
config_entry = self.hass.config_entries.async_get_entry(
self._config_entry_id
)
# If config entry was removed, we can ignore the issue.
if config_entry is not None:
self.hass.config_entries.async_update_entry(
config_entry,
unique_id=self.description_placeholders["new_unique_id"],
)
return self.async_create_entry(data={})
return self.async_show_form(
step_id="confirm",
description_placeholders=self.description_placeholders,
)
async def async_create_fix_flow(
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
) -> RepairsFlow:
@@ -106,7 +65,4 @@ async def async_create_fix_flow(
if issue_id.split(".")[0] == "device_config_file_changed":
assert data
return DeviceConfigFileChangedFlow(data)
if issue_id.split(".")[0] == "migrate_unique_id":
assert data
return MigrateUniqueIDFlow(data)
return ConfirmRepairFlow()
@@ -273,17 +273,6 @@
"invalid_server_version": {
"description": "The version of Z-Wave Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave Server to the latest version to fix this issue.",
"title": "Newer version of Z-Wave Server needed"
},
"migrate_unique_id": {
"fix_flow": {
"step": {
"confirm": {
"description": "A Z-Wave controller of model {controller_model} with a different ID ({new_unique_id}) than the previously connected controller ({old_unique_id}) was connected to the {config_entry_title} configuration entry.\n\nReasons for a different controller ID could be:\n\n1. The controller was factory reset, with a 3rd party application.\n2. A controller Non Volatile Memory (NVM) backup was restored to the controller, with a 3rd party application.\n3. A different controller was connected to this configuration entry.\n\nIf a different controller was connected, you should instead set up a new configuration entry for the new controller.\n\nIf you are sure that the current controller is the correct controller you can confirm this by pressing Submit, and the configuration entry will remember the new controller ID.",
"title": "An unknown controller was detected"
}
}
},
"title": "An unknown controller was detected"
}
},
"services": {
+4 -4
View File
@@ -866,17 +866,17 @@ class Config:
# pylint: disable-next=import-outside-toplevel
from .components.frontend import storage as frontend_store
owner_store = await frontend_store.async_user_store(
_, owner_data = await frontend_store.async_user_store(
self.hass, owner.id
)
if (
"language" in owner_store.data
and "language" in owner_store.data["language"]
"language" in owner_data
and "language" in owner_data["language"]
):
with suppress(vol.InInvalid):
data["language"] = cv.language(
owner_store.data["language"]["language"]
owner_data["language"]["language"]
)
# pylint: disable-next=broad-except
except Exception:
+1 -38
View File
@@ -40,7 +40,7 @@ EVENT_AREA_REGISTRY_UPDATED: EventType[EventAreaRegistryUpdatedData] = EventType
)
STORAGE_KEY = "core.area_registry"
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 9
STORAGE_VERSION_MINOR = 8
class _AreaStoreData(TypedDict):
@@ -52,7 +52,6 @@ class _AreaStoreData(TypedDict):
icon: str | None
id: str
labels: list[str]
motion_entity_id: str | None
name: str
picture: str | None
temperature_entity_id: str | None
@@ -83,7 +82,6 @@ class AreaEntry(NormalizedNameBaseRegistryEntry):
icon: str | None
id: str
labels: set[str] = field(default_factory=set)
motion_entity_id: str | None
picture: str | None
temperature_entity_id: str | None
_cache: dict[str, Any] = field(default_factory=dict, compare=False, init=False)
@@ -100,7 +98,6 @@ class AreaEntry(NormalizedNameBaseRegistryEntry):
"humidity_entity_id": self.humidity_entity_id,
"icon": self.icon,
"labels": list(self.labels),
"motion_entity_id": self.motion_entity_id,
"name": self.name,
"picture": self.picture,
"temperature_entity_id": self.temperature_entity_id,
@@ -160,11 +157,6 @@ class AreaRegistryStore(Store[AreasRegistryStoreData]):
area["humidity_entity_id"] = None
area["temperature_entity_id"] = None
if old_minor_version < 9:
# Version 1.9 adds motion_entity_id
for area_data in old_data["areas"]:
area_data["motion_entity_id"] = None
if old_major_version > 1:
raise NotImplementedError
return old_data # type: ignore[return-value]
@@ -286,7 +278,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
humidity_entity_id: str | None = None,
icon: str | None = None,
labels: set[str] | None = None,
motion_entity_id: str | None = None,
picture: str | None = None,
temperature_entity_id: str | None = None,
) -> AreaEntry:
@@ -302,9 +293,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
if humidity_entity_id is not None:
_validate_humidity_entity(self.hass, humidity_entity_id)
if motion_entity_id is not None:
_validate_motion_entity(self.hass, motion_entity_id)
if temperature_entity_id is not None:
_validate_temperature_entity(self.hass, temperature_entity_id)
@@ -315,7 +303,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
icon=icon,
id=self._generate_id(name),
labels=labels or set(),
motion_entity_id=motion_entity_id,
name=name,
picture=picture,
temperature_entity_id=temperature_entity_id,
@@ -358,7 +345,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
humidity_entity_id: str | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
motion_entity_id: str | None | UndefinedType = UNDEFINED,
name: str | UndefinedType = UNDEFINED,
picture: str | None | UndefinedType = UNDEFINED,
temperature_entity_id: str | None | UndefinedType = UNDEFINED,
@@ -371,7 +357,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
humidity_entity_id=humidity_entity_id,
icon=icon,
labels=labels,
motion_entity_id=motion_entity_id,
name=name,
picture=picture,
temperature_entity_id=temperature_entity_id,
@@ -396,7 +381,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
humidity_entity_id: str | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
motion_entity_id: str | None | UndefinedType = UNDEFINED,
name: str | UndefinedType = UNDEFINED,
picture: str | None | UndefinedType = UNDEFINED,
temperature_entity_id: str | None | UndefinedType = UNDEFINED,
@@ -412,7 +396,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
("humidity_entity_id", humidity_entity_id),
("icon", icon),
("labels", labels),
("motion_entity_id", motion_entity_id),
("picture", picture),
("temperature_entity_id", temperature_entity_id),
)
@@ -422,9 +405,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
if "humidity_entity_id" in new_values and humidity_entity_id is not None:
_validate_humidity_entity(self.hass, new_values["humidity_entity_id"])
if "motion_entity_id" in new_values and motion_entity_id is not None:
_validate_motion_entity(self.hass, new_values["motion_entity_id"])
if "temperature_entity_id" in new_values and temperature_entity_id is not None:
_validate_temperature_entity(self.hass, new_values["temperature_entity_id"])
@@ -460,7 +440,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
icon=area["icon"],
id=area["id"],
labels=set(area["labels"]),
motion_entity_id=area["motion_entity_id"],
name=area["name"],
picture=area["picture"],
temperature_entity_id=area["temperature_entity_id"],
@@ -483,7 +462,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
"icon": entry.icon,
"id": entry.id,
"labels": list(entry.labels),
"motion_entity_id": entry.motion_entity_id,
"name": entry.name,
"picture": entry.picture,
"temperature_entity_id": entry.temperature_entity_id,
@@ -591,18 +569,3 @@ def _validate_humidity_entity(hass: HomeAssistant, entity_id: str) -> None:
or state.attributes.get(ATTR_DEVICE_CLASS) != SensorDeviceClass.HUMIDITY
):
raise ValueError(f"Entity {entity_id} is not a humidity sensor")
def _validate_motion_entity(hass: HomeAssistant, entity_id: str) -> None:
"""Validate motion entity."""
# pylint: disable=import-outside-toplevel
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
if not (state := hass.states.get(entity_id)):
raise ValueError(f"Entity {entity_id} does not exist")
if (
state.domain != "binary_sensor"
or state.attributes.get(ATTR_DEVICE_CLASS) != BinarySensorDeviceClass.MOTION
):
raise ValueError(f"Entity {entity_id} is not a motion binary_sensor")
+105 -4
View File
@@ -42,6 +42,8 @@ from homeassistant.const import (
ENTITY_MATCH_ANY,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
WEEKDAYS,
)
from homeassistant.core import HomeAssistant, State, callback
@@ -58,6 +60,7 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
from . import config_validation as cv, entity_registry as er
from .sun import get_astral_event_date
from .template import Template, render_complex
from .trace import (
TraceElement,
@@ -82,6 +85,7 @@ _PLATFORM_ALIASES = {
"numeric_state": None,
"or": None,
"state": None,
"sun": None,
"template": None,
"time": None,
"trigger": None,
@@ -651,6 +655,105 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType:
return if_state
def sun(
hass: HomeAssistant,
before: str | None = None,
after: str | None = None,
before_offset: timedelta | None = None,
after_offset: timedelta | None = None,
) -> bool:
"""Test if current time matches sun requirements."""
utcnow = dt_util.utcnow()
today = dt_util.as_local(utcnow).date()
before_offset = before_offset or timedelta(0)
after_offset = after_offset or timedelta(0)
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
if after_sunrise and has_sunrise_condition:
tomorrow = today + timedelta(days=1)
sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
if after_sunset and has_sunset_condition:
tomorrow = today + timedelta(days=1)
sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
# Special case: before sunrise OR after sunset
# This will handle the very rare case in the polar region when the sun rises/sets
# but does not set/rise.
# However this entire condition does not handle those full days of darkness
# or light, the following should be used instead:
#
# condition:
# condition: state
# entity_id: sun.sun
# state: 'above_horizon' (or 'below_horizon')
#
if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
wanted_time_after = cast(datetime, sunset) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
return utcnow < wanted_time_before or utcnow > wanted_time_after
if sunrise is None and has_sunrise_condition:
# There is no sunrise today
condition_trace_set_result(False, message="no sunrise today")
return False
if sunset is None and has_sunset_condition:
# There is no sunset today
condition_trace_set_result(False, message="no sunset today")
return False
if before == SUN_EVENT_SUNRISE:
wanted_time_before = cast(datetime, sunrise) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
if utcnow > wanted_time_before:
return False
if before == SUN_EVENT_SUNSET:
wanted_time_before = cast(datetime, sunset) + before_offset
condition_trace_update_result(wanted_time_before=wanted_time_before)
if utcnow > wanted_time_before:
return False
if after == SUN_EVENT_SUNRISE:
wanted_time_after = cast(datetime, sunrise) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
if utcnow < wanted_time_after:
return False
if after == SUN_EVENT_SUNSET:
wanted_time_after = cast(datetime, sunset) + after_offset
condition_trace_update_result(wanted_time_after=wanted_time_after)
if utcnow < wanted_time_after:
return False
return True
def sun_from_config(config: ConfigType) -> ConditionCheckerType:
"""Wrap action method with sun based condition."""
before = config.get("before")
after = config.get("after")
before_offset = config.get("before_offset")
after_offset = config.get("after_offset")
@trace_condition_function
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate time based if-condition."""
return sun(hass, before, after, before_offset, after_offset)
return sun_if
def template(
hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None
) -> bool:
@@ -951,10 +1054,8 @@ async def async_validate_condition_config(
return config
platform = await _async_get_condition_platform(hass, config)
if platform is not None:
if hasattr(platform, "async_validate_condition_config"):
return await platform.async_validate_condition_config(hass, config)
return cast(ConfigType, platform.CONDITION_SCHEMA(config))
if platform is not None and hasattr(platform, "async_validate_condition_config"):
return await platform.async_validate_condition_config(hass, config)
if platform is None and condition in ("numeric_state", "state"):
validator = cast(
Callable[[HomeAssistant, ConfigType], ConfigType],
+30 -40
View File
@@ -1084,13 +1084,10 @@ def renamed(
return validator
type ValueSchemas = dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]]
def key_value_schemas(
key: str,
value_schemas: ValueSchemas,
default_schema: VolSchemaType | Callable[[Any], dict[str, Any]] | None = None,
value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]],
default_schema: VolSchemaType | None = None,
default_description: str | None = None,
) -> Callable[[Any], dict[Hashable, Any]]:
"""Create a validator that validates based on a value for specific key.
@@ -1738,41 +1735,25 @@ CONDITION_SHORTHAND_SCHEMA = vol.Schema(
}
)
BUILT_IN_CONDITIONS: ValueSchemas = {
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
}
# This is first round of validation, we don't want to mutate the config here already,
# just ensure basics as condition type and alias are there.
def _base_condition_validator(value: Any) -> Any:
vol.Schema(
{
**CONDITION_BASE_SCHEMA,
CONF_CONDITION: vol.NotIn(BUILT_IN_CONDITIONS),
},
extra=vol.ALLOW_EXTRA,
)(value)
return value
CONDITION_SCHEMA: vol.Schema = vol.Schema(
vol.Any(
vol.All(
expand_condition_shorthand,
key_value_schemas(
CONF_CONDITION,
BUILT_IN_CONDITIONS,
_base_condition_validator,
{
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
},
),
),
dynamic_template_condition,
@@ -1799,11 +1780,20 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
expand_condition_shorthand,
key_value_schemas(
CONF_CONDITION,
BUILT_IN_CONDITIONS,
vol.Any(
dynamic_template_condition_action,
_base_condition_validator,
),
{
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
},
dynamic_template_condition_action,
"a list of conditions or a valid template",
),
)
@@ -1862,7 +1852,7 @@ def _base_trigger_list_flatten(triggers: list[Any]) -> list[Any]:
return flatlist
# This is first round of validation, we don't want to mutate the config here already,
# This is first round of validation, we don't want to process the config here already,
# just ensure basics as platform and ID are there.
def _base_trigger_validator(value: Any) -> Any:
_base_trigger_validator_schema(value)
+43 -5
View File
@@ -29,20 +29,27 @@ from homeassistant.core import (
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.util.hass_dict import HassKey
from . import config_validation as cv, discovery, entity, service
from .entity_platform import EntityPlatform
from . import (
config_validation as cv,
device_registry as dr,
discovery,
entity,
entity_registry as er,
service,
)
from .entity_platform import EntityPlatform, async_calculate_suggested_object_id
from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
DATA_INSTANCES = "entity_components"
DATA_INSTANCES: HassKey[dict[str, EntityComponent]] = HassKey("entity_components")
@bind_hass
async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
"""Trigger an update for an entity."""
domain = entity_id.partition(".")[0]
entity_comp: EntityComponent[entity.Entity] | None
entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain)
if entity_comp is None:
@@ -60,6 +67,37 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
await entity_obj.async_update_ha_state(True)
@callback
def async_get_entity_suggested_object_id(
hass: HomeAssistant, entity_id: str
) -> str | None:
"""Get the suggested object id for an entity.
Raises HomeAssistantError if the entity is not in the registry.
"""
entity_registry = er.async_get(hass)
if not (entity_entry := entity_registry.async_get(entity_id)):
raise HomeAssistantError(f"Entity {entity_id} is not in the registry.")
domain = entity_id.partition(".")[0]
if entity_entry.suggested_object_id:
return entity_entry.suggested_object_id
if entity_entry.name:
return entity_entry.name
entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain)
entity_obj = entity_comp.get_entity(entity_id) if entity_comp else None
if entity_obj:
device: dr.DeviceEntry | None = None
if device_id := entity_entry.device_id:
device = dr.async_get(hass).async_get(device_id)
return async_calculate_suggested_object_id(entity_obj, device)
return entity_entry.calculated_object_id
class EntityComponent[_EntityT: entity.Entity = entity.Entity]:
"""The EntityComponent manages platforms that manage entities.
@@ -95,7 +133,7 @@ class EntityComponent[_EntityT: entity.Entity = entity.Entity]:
self.async_add_entities = domain_platform.async_add_entities
self.add_entities = domain_platform.add_entities
self._entities: dict[str, entity.Entity] = domain_platform.domain_entities
hass.data.setdefault(DATA_INSTANCES, {})[domain] = self
hass.data.setdefault(DATA_INSTANCES, {})[domain] = self # type: ignore[assignment]
@property
def entities(self) -> Iterable[_EntityT]:
+34 -25
View File
@@ -764,7 +764,7 @@ class EntityPlatform:
already_exists = True
return (already_exists, restored)
async def _async_add_entity( # noqa: C901
async def _async_add_entity(
self,
entity: Entity,
update_before_add: bool,
@@ -843,31 +843,18 @@ class EntityPlatform:
else:
device = None
if not registered_entity_id:
# Do not bother working out a suggested_object_id
# if the entity is already registered as it will
# be ignored.
#
# An entity may suggest the entity_id by setting entity_id itself
suggested_entity_id: str | None = entity.entity_id
if suggested_entity_id is not None:
suggested_object_id = split_entity_id(entity.entity_id)[1]
else:
if device and entity.has_entity_name:
device_name = device.name_by_user or device.name
if entity.use_device_name:
suggested_object_id = device_name
else:
suggested_object_id = (
f"{device_name} {entity.suggested_object_id}"
)
if not suggested_object_id:
suggested_object_id = entity.suggested_object_id
# An entity may suggest the entity_id by setting entity_id itself
calculated_object_id: str | None = None
suggested_entity_id: str | None = entity.entity_id
if suggested_entity_id is not None:
suggested_object_id = split_entity_id(entity.entity_id)[1]
else:
calculated_object_id = async_calculate_suggested_object_id(
entity, device
)
if self.entity_namespace is not None:
suggested_object_id = (
f"{self.entity_namespace} {suggested_object_id}"
)
if self.entity_namespace is not None and suggested_object_id is not None:
suggested_object_id = f"{self.entity_namespace} {suggested_object_id}"
disabled_by: RegistryEntryDisabler | None = None
if not entity.entity_registry_enabled_default:
@@ -881,6 +868,7 @@ class EntityPlatform:
self.domain,
self.platform_name,
entity.unique_id,
calculated_object_id=calculated_object_id,
capabilities=entity.capability_attributes,
config_entry=self.config_entry,
config_subentry_id=config_subentry_id,
@@ -1124,6 +1112,27 @@ class EntityPlatform:
await asyncio.gather(*tasks)
@callback
def async_calculate_suggested_object_id(
entity: Entity, device: dev_reg.DeviceEntry | None
) -> str | None:
"""Calculate the suggested object ID for an entity."""
calculated_object_id: str | None = None
if device and entity.has_entity_name:
device_name = device.name_by_user or device.name
if entity.use_device_name:
calculated_object_id = device_name
else:
calculated_object_id = f"{device_name} {entity.suggested_object_id}"
if not calculated_object_id:
calculated_object_id = entity.suggested_object_id
if (platform := entity.platform) and platform.entity_namespace is not None:
calculated_object_id = f"{platform.entity_namespace} {calculated_object_id}"
return calculated_object_id
current_platform: ContextVar[EntityPlatform | None] = ContextVar(
"current_platform", default=None
)
+17 -2
View File
@@ -79,7 +79,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 16
STORAGE_VERSION_MINOR = 17
STORAGE_KEY = "core.entity_registry"
CLEANUP_INTERVAL = 3600 * 24
@@ -195,9 +195,11 @@ class RegistryEntry:
name: str | None = attr.ib(default=None)
options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options)
# As set by integration
calculated_object_id: str | None = attr.ib()
original_device_class: str | None = attr.ib()
original_icon: str | None = attr.ib()
original_name: str | None = attr.ib()
suggested_object_id: str | None = attr.ib()
supported_features: int = attr.ib()
translation_key: str | None = attr.ib()
unit_of_measurement: str | None = attr.ib()
@@ -337,6 +339,7 @@ class RegistryEntry:
{
"aliases": list(self.aliases),
"area_id": self.area_id,
"calculated_object_id": self.calculated_object_id,
"categories": self.categories,
"capabilities": self.capabilities,
"config_entry_id": self.config_entry_id,
@@ -359,6 +362,7 @@ class RegistryEntry:
"original_icon": self.original_icon,
"original_name": self.original_name,
"platform": self.platform,
"suggested_object_id": self.suggested_object_id,
"supported_features": self.supported_features,
"translation_key": self.translation_key,
"unique_id": self.unique_id,
@@ -548,6 +552,12 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entity in data["deleted_entities"]:
entity["config_subentry_id"] = None
if old_minor_version < 17:
# Version 1.17 adds calculated_object_id and suggested_object_id
for entity in data["entities"]:
entity["calculated_object_id"] = None
entity["suggested_object_id"] = None
if old_major_version > 1:
raise NotImplementedError
return data
@@ -836,6 +846,7 @@ class EntityRegistry(BaseRegistry):
unique_id: str,
*,
# To influence entity ID generation
calculated_object_id: str | None = None,
suggested_object_id: str | None = None,
# To disable or hide an entity if it gets created
disabled_by: RegistryEntryDisabler | None = None,
@@ -908,7 +919,7 @@ class EntityRegistry(BaseRegistry):
entity_id = self.async_generate_entity_id(
domain,
suggested_object_id or f"{platform}_{unique_id}",
suggested_object_id or calculated_object_id or f"{platform}_{unique_id}",
)
if (
@@ -942,6 +953,8 @@ class EntityRegistry(BaseRegistry):
original_icon=none_if_undefined(original_icon),
original_name=none_if_undefined(original_name),
platform=platform,
calculated_object_id=calculated_object_id,
suggested_object_id=suggested_object_id,
supported_features=none_if_undefined(supported_features) or 0,
translation_key=none_if_undefined(translation_key),
unique_id=unique_id,
@@ -1350,6 +1363,7 @@ class EntityRegistry(BaseRegistry):
entities[entity["entity_id"]] = RegistryEntry(
aliases=set(entity["aliases"]),
area_id=entity["area_id"],
calculated_object_id=entity["calculated_object_id"],
categories=entity["categories"],
capabilities=entity["capabilities"],
config_entry_id=entity["config_entry_id"],
@@ -1378,6 +1392,7 @@ class EntityRegistry(BaseRegistry):
original_icon=entity["original_icon"],
original_name=entity["original_name"],
platform=entity["platform"],
suggested_object_id=entity["suggested_object_id"],
supported_features=entity["supported_features"],
translation_key=entity["translation_key"],
unique_id=entity["unique_id"],
+1 -1
View File
@@ -1,6 +1,6 @@
# Automatically generated by gen_requirements_all.py, do not edit
aiodhcpwatcher==1.2.0
aiodhcpwatcher==1.1.1
aiodiscover==2.7.0
aiodns==3.4.0
aiohasupervisor==0.3.1
+8 -8
View File
@@ -211,10 +211,10 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==0.12.1
aiocomelit==0.12.0
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.0
aiodhcpwatcher==1.1.1
# homeassistant.components.dhcp
aiodiscover==2.7.0
@@ -542,7 +542,7 @@ aurorapy==0.2.7
autarco==3.1.0
# homeassistant.components.husqvarna_automower_ble
automower-ble==0.2.1
automower-ble==0.2.0
# homeassistant.components.generic
# homeassistant.components.stream
@@ -750,7 +750,7 @@ datapoint==0.9.9
dbus-fast==2.43.0
# homeassistant.components.debugpy
debugpy==1.8.14
debugpy==1.8.13
# homeassistant.components.decora_wifi
# decora-wifi==1.4
@@ -983,7 +983,7 @@ gardena-bluetooth==1.6.0
gassist-text==0.0.12
# homeassistant.components.google
gcal-sync==7.0.1
gcal-sync==7.0.0
# homeassistant.components.geniushub
geniushub-client==0.7.1
@@ -1197,7 +1197,7 @@ ibmiotf==0.3.4
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==9.2.2
ical==9.2.1
# homeassistant.components.caldav
icalendar==6.1.0
@@ -2132,7 +2132,7 @@ pymeteoclimatic==0.1.0
pymicro-vad==1.0.1
# homeassistant.components.miele
pymiele==0.5.2
pymiele==0.5.1
# homeassistant.components.xiaomi_tv
pymitv==1.4.3
@@ -3101,7 +3101,7 @@ xbox-webapi==2.1.0
xiaomi-ble==0.38.0
# homeassistant.components.knx
xknx==3.8.0
xknx==3.6.0
# homeassistant.components.knx
xknxproject==3.8.2
+1 -1
View File
@@ -18,7 +18,7 @@ pre-commit==4.0.0
pydantic==2.11.3
pylint==3.3.7
pylint-per-file-ignores==1.4.0
pipdeptree==2.26.1
pipdeptree==2.25.1
pytest-asyncio==0.26.0
pytest-aiohttp==1.1.0
pytest-cov==6.0.0
+8 -8
View File
@@ -199,10 +199,10 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==0.12.1
aiocomelit==0.12.0
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.0
aiodhcpwatcher==1.1.1
# homeassistant.components.dhcp
aiodiscover==2.7.0
@@ -497,7 +497,7 @@ aurorapy==0.2.7
autarco==3.1.0
# homeassistant.components.husqvarna_automower_ble
automower-ble==0.2.1
automower-ble==0.2.0
# homeassistant.components.generic
# homeassistant.components.stream
@@ -647,7 +647,7 @@ datapoint==0.9.9
dbus-fast==2.43.0
# homeassistant.components.debugpy
debugpy==1.8.14
debugpy==1.8.13
# homeassistant.components.ecovacs
deebot-client==13.1.0
@@ -837,7 +837,7 @@ gardena-bluetooth==1.6.0
gassist-text==0.0.12
# homeassistant.components.google
gcal-sync==7.0.1
gcal-sync==7.0.0
# homeassistant.components.geniushub
geniushub-client==0.7.1
@@ -1018,7 +1018,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==9.2.2
ical==9.2.1
# homeassistant.components.caldav
icalendar==6.1.0
@@ -1744,7 +1744,7 @@ pymeteoclimatic==0.1.0
pymicro-vad==1.0.1
# homeassistant.components.miele
pymiele==0.5.2
pymiele==0.5.1
# homeassistant.components.mochad
pymochad==0.2.0
@@ -2509,7 +2509,7 @@ xbox-webapi==2.1.0
xiaomi-ble==0.38.0
# homeassistant.components.knx
xknx==3.8.0
xknx==3.6.0
# homeassistant.components.knx
xknxproject==3.8.2
+1 -1
View File
@@ -24,7 +24,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \
--no-cache \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \
stdlib-list==0.10.0 pipdeptree==2.25.1 tqdm==4.67.1 ruff==0.11.0 \
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
+2
View File
@@ -651,6 +651,7 @@ class RegistryEntryWithDefaults(er.RegistryEntry):
"""Helper to create a registry entry with defaults."""
capabilities: Mapping[str, Any] | None = attr.ib(default=None)
calculated_object_id: str | None = attr.ib(default=None)
config_entry_id: str | None = attr.ib(default=None)
config_subentry_id: str | None = attr.ib(default=None)
created_at: datetime = attr.ib(factory=dt_util.utcnow)
@@ -669,6 +670,7 @@ class RegistryEntryWithDefaults(er.RegistryEntry):
original_device_class: str | None = attr.ib(default=None)
original_icon: str | None = attr.ib(default=None)
original_name: str | None = attr.ib(default=None)
suggested_object_id: str | None = attr.ib(default=None)
supported_features: int = attr.ib(default=0)
translation_key: str | None = attr.ib(default=None)
unit_of_measurement: str | None = attr.ib(default=None)
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Timer running',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Timer running',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'timer_running',
'unique_id': 'aa:bb:cc:dd:ee:ff_timer_running',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Reset timer',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Reset timer',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_timer',
'unique_id': 'aa:bb:cc:dd:ee:ff_reset_timer',
@@ -51,6 +53,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Start/stop timer',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -74,6 +77,7 @@
'original_name': 'Start/stop timer',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'start_stop',
'unique_id': 'aa:bb:cc:dd:ee:ff_start_stop',
@@ -98,6 +102,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Tare',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -121,6 +126,7 @@
'original_name': 'Tare',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'tare',
'unique_id': 'aa:bb:cc:dd:ee:ff_tare',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -29,6 +30,7 @@
'original_name': 'Battery',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_battery',
@@ -56,6 +58,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Volume flow rate',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -84,6 +87,7 @@
'original_name': 'Volume flow rate',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_flow_rate',
@@ -111,6 +115,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Weight',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -136,6 +141,7 @@
'original_name': 'Weight',
'platform': 'acaia',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff_weight',
File diff suppressed because it is too large Load Diff
@@ -245,6 +245,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -268,6 +269,7 @@
'original_name': None,
'platform': 'accuweather',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <WeatherEntityFeature: 1>,
'translation_key': None,
'unique_id': '0123456',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Calibrate CO2 sensor',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Calibrate CO2 sensor',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co2_calibration',
'unique_id': '84fce612f5b8-co2_calibration',
@@ -51,6 +53,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Test LED bar',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -74,6 +77,7 @@
'original_name': 'Test LED bar',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_bar_test',
'unique_id': '84fce612f5b8-led_bar_test',
@@ -98,6 +102,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Calibrate CO2 sensor',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -121,6 +126,7 @@
'original_name': 'Calibrate CO2 sensor',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co2_calibration',
'unique_id': '84fce612f5b8-co2_calibration',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display brightness',
'capabilities': dict({
'max': 100,
'min': 0,
@@ -32,6 +33,7 @@
'original_name': 'Display brightness',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_brightness',
'unique_id': '84fce612f5b8-display_brightness',
@@ -61,6 +63,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar brightness',
'capabilities': dict({
'max': 100,
'min': 0,
@@ -89,6 +92,7 @@
'original_name': 'LED bar brightness',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_bar_brightness',
'unique_id': '84fce612f5b8-led_bar_brightness',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient CO2 automatic baseline duration',
'capabilities': dict({
'options': list([
'1',
@@ -36,6 +37,7 @@
'original_name': 'CO2 automatic baseline duration',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co2_automatic_baseline_calibration',
'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration',
@@ -68,6 +70,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Configuration source',
'capabilities': dict({
'options': list([
'cloud',
@@ -96,6 +99,7 @@
'original_name': 'Configuration source',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'configuration_control',
'unique_id': '84fce612f5b8-configuration_control',
@@ -124,6 +128,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display PM standard',
'capabilities': dict({
'options': list([
'ugm3',
@@ -152,6 +157,7 @@
'original_name': 'Display PM standard',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_pm_standard',
'unique_id': '84fce612f5b8-display_pm_standard',
@@ -180,6 +186,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display temperature unit',
'capabilities': dict({
'options': list([
'c',
@@ -208,6 +215,7 @@
'original_name': 'Display temperature unit',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_temperature_unit',
'unique_id': '84fce612f5b8-display_temperature_unit',
@@ -236,6 +244,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar mode',
'capabilities': dict({
'options': list([
'off',
@@ -265,6 +274,7 @@
'original_name': 'LED bar mode',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_bar_mode',
'unique_id': '84fce612f5b8-led_bar_mode',
@@ -294,6 +304,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -325,6 +336,7 @@
'original_name': 'NOx index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'nox_index_learning_time_offset',
'unique_id': '84fce612f5b8-nox_index_learning_time_offset',
@@ -356,6 +368,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -387,6 +400,7 @@
'original_name': 'VOC index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'voc_index_learning_time_offset',
'unique_id': '84fce612f5b8-voc_index_learning_time_offset',
@@ -418,6 +432,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient CO2 automatic baseline duration',
'capabilities': dict({
'options': list([
'1',
@@ -450,6 +465,7 @@
'original_name': 'CO2 automatic baseline duration',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co2_automatic_baseline_calibration',
'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration',
@@ -482,6 +498,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Configuration source',
'capabilities': dict({
'options': list([
'cloud',
@@ -510,6 +527,7 @@
'original_name': 'Configuration source',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'configuration_control',
'unique_id': '84fce612f5b8-configuration_control',
@@ -538,6 +556,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -569,6 +588,7 @@
'original_name': 'NOx index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'nox_index_learning_time_offset',
'unique_id': '84fce612f5b8-nox_index_learning_time_offset',
@@ -600,6 +620,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -631,6 +652,7 @@
'original_name': 'VOC index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'voc_index_learning_time_offset',
'unique_id': '84fce612f5b8-voc_index_learning_time_offset',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Carbon dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -29,6 +30,7 @@
'original_name': 'Carbon dioxide',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-co2',
@@ -56,6 +58,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Carbon dioxide automatic baseline calibration',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -79,6 +82,7 @@
'original_name': 'Carbon dioxide automatic baseline calibration',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co2_automatic_baseline_calibration_days',
'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days',
@@ -105,6 +109,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display brightness',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -128,6 +133,7 @@
'original_name': 'Display brightness',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_brightness',
'unique_id': '84fce612f5b8-display_brightness',
@@ -153,6 +159,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display PM standard',
'capabilities': dict({
'options': list([
'ugm3',
@@ -181,6 +188,7 @@
'original_name': 'Display PM standard',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_pm_standard',
'unique_id': '84fce612f5b8-display_pm_standard',
@@ -210,6 +218,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display temperature unit',
'capabilities': dict({
'options': list([
'c',
@@ -238,6 +247,7 @@
'original_name': 'Display temperature unit',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_temperature_unit',
'unique_id': '84fce612f5b8-display_temperature_unit',
@@ -267,6 +277,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -292,6 +303,7 @@
'original_name': 'Humidity',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-humidity',
@@ -319,6 +331,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar brightness',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -342,6 +355,7 @@
'original_name': 'LED bar brightness',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_bar_brightness',
'unique_id': '84fce612f5b8-led_bar_brightness',
@@ -367,6 +381,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar mode',
'capabilities': dict({
'options': list([
'off',
@@ -396,6 +411,7 @@
'original_name': 'LED bar mode',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_bar_mode',
'unique_id': '84fce612f5b8-led_bar_mode',
@@ -426,6 +442,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -451,6 +468,7 @@
'original_name': 'NOx index',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'nitrogen_index',
'unique_id': '84fce612f5b8-nitrogen_index',
@@ -476,6 +494,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -499,6 +518,7 @@
'original_name': 'NOx index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'nox_learning_offset',
'unique_id': '84fce612f5b8-nox_learning_offset',
@@ -525,6 +545,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM0.3',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -550,6 +571,7 @@
'original_name': 'PM0.3',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pm003_count',
'unique_id': '84fce612f5b8-pm003',
@@ -576,6 +598,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM1',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -601,6 +624,7 @@
'original_name': 'PM1',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-pm01',
@@ -628,6 +652,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM10',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -653,6 +678,7 @@
'original_name': 'PM10',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-pm10',
@@ -680,6 +706,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -705,6 +732,7 @@
'original_name': 'PM2.5',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-pm02',
@@ -732,6 +760,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw NOx',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -757,6 +786,7 @@
'original_name': 'Raw NOx',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'raw_nitrogen',
'unique_id': '84fce612f5b8-nox_raw',
@@ -783,6 +813,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -808,6 +839,7 @@
'original_name': 'Raw PM2.5',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'raw_pm02',
'unique_id': '84fce612f5b8-pm02_raw',
@@ -835,6 +867,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw VOC',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -860,6 +893,7 @@
'original_name': 'Raw VOC',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'raw_total_volatile_organic_component',
'unique_id': '84fce612f5b8-tvoc_raw',
@@ -886,6 +920,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -911,6 +946,7 @@
'original_name': 'Signal strength',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-signal_strength',
@@ -938,6 +974,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -963,6 +1000,7 @@
'original_name': 'Temperature',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-temperature',
@@ -990,6 +1028,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1015,6 +1054,7 @@
'original_name': 'VOC index',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_volatile_organic_component_index',
'unique_id': '84fce612f5b8-tvoc',
@@ -1040,6 +1080,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1063,6 +1104,7 @@
'original_name': 'VOC index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'tvoc_learning_offset',
'unique_id': '84fce612f5b8-tvoc_learning_offset',
@@ -1089,6 +1131,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Carbon dioxide automatic baseline calibration',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1112,6 +1155,7 @@
'original_name': 'Carbon dioxide automatic baseline calibration',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co2_automatic_baseline_calibration_days',
'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days',
@@ -1138,6 +1182,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1163,6 +1208,7 @@
'original_name': 'NOx index',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'nitrogen_index',
'unique_id': '84fce612f5b8-nitrogen_index',
@@ -1188,6 +1234,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1211,6 +1258,7 @@
'original_name': 'NOx index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'nox_learning_offset',
'unique_id': '84fce612f5b8-nox_learning_offset',
@@ -1237,6 +1285,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw NOx',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1262,6 +1311,7 @@
'original_name': 'Raw NOx',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'raw_nitrogen',
'unique_id': '84fce612f5b8-nox_raw',
@@ -1288,6 +1338,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw VOC',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1313,6 +1364,7 @@
'original_name': 'Raw VOC',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'raw_total_volatile_organic_component',
'unique_id': '84fce612f5b8-tvoc_raw',
@@ -1339,6 +1391,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1364,6 +1417,7 @@
'original_name': 'Signal strength',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-signal_strength',
@@ -1391,6 +1445,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1416,6 +1471,7 @@
'original_name': 'VOC index',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_volatile_organic_component_index',
'unique_id': '84fce612f5b8-tvoc',
@@ -1441,6 +1497,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1464,6 +1521,7 @@
'original_name': 'VOC index learning offset',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'tvoc_learning_offset',
'unique_id': '84fce612f5b8-tvoc_learning_offset',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Post data to Airgradient',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Post data to Airgradient',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'post_data_to_airgradient',
'unique_id': '84fce612f5b8-post_data_to_airgradient',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Firmware',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Firmware',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-update',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Carbon monoxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -32,6 +33,7 @@
'original_name': 'Carbon monoxide',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'co',
'unique_id': '123-456-co',
@@ -61,6 +63,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Common air quality index',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -87,6 +90,7 @@
'original_name': 'Common air quality index',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'caqi',
'unique_id': '123-456-caqi',
@@ -116,6 +120,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -144,6 +149,7 @@
'original_name': 'Humidity',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-humidity',
@@ -172,6 +178,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Nitrogen dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -200,6 +207,7 @@
'original_name': 'Nitrogen dioxide',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-no2',
@@ -230,6 +238,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Ozone',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -258,6 +267,7 @@
'original_name': 'Ozone',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-o3',
@@ -288,6 +298,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home PM1',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -316,6 +327,7 @@
'original_name': 'PM1',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm1',
@@ -344,6 +356,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home PM10',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -372,6 +385,7 @@
'original_name': 'PM10',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm10',
@@ -402,6 +416,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -430,6 +445,7 @@
'original_name': 'PM2.5',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm25',
@@ -460,6 +476,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -488,6 +505,7 @@
'original_name': 'Pressure',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pressure',
@@ -516,6 +534,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Sulphur dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -544,6 +563,7 @@
'original_name': 'Sulphur dioxide',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-so2',
@@ -574,6 +594,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -602,6 +623,7 @@
'original_name': 'Temperature',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-temperature',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Zone 1 Damper',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Damper',
'platform': 'airtouch5',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 7>,
'translation_key': 'damper',
'unique_id': 'zone_1_open_percentage',
@@ -54,6 +56,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Zone 2 Damper',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -77,6 +80,7 @@
'original_name': 'Damper',
'platform': 'airtouch5',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 7>,
'translation_key': 'damper',
'unique_id': 'zone_2_open_percentage',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone 2:1 Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -29,6 +30,7 @@
'original_name': 'Humidity',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_2:1_humidity',
@@ -56,6 +58,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone 2:1 Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -81,6 +84,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_2:1_temp',
@@ -108,6 +112,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone DHW Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -133,6 +138,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_dhw_temp',
@@ -160,6 +166,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone WebServer RSSI',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -185,6 +192,7 @@
'original_name': 'RSSI',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'rssi',
'unique_id': 'airzone_unique_id_ws_wifi-rssi',
@@ -212,6 +220,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Aux Heat Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -237,6 +246,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_4:1_temp',
@@ -264,6 +274,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -289,6 +300,7 @@
'original_name': 'Battery',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:4_thermostat-battery',
@@ -316,6 +328,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -341,6 +354,7 @@
'original_name': 'Humidity',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:4_humidity',
@@ -368,6 +382,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -393,6 +408,7 @@
'original_name': 'Signal strength',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_signal',
'unique_id': 'airzone_unique_id_1:4_thermostat-signal',
@@ -419,6 +435,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -444,6 +461,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:4_temp',
@@ -471,6 +489,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'DKN Plus Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -496,6 +515,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_3:1_temp',
@@ -523,6 +543,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -548,6 +569,7 @@
'original_name': 'Battery',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:3_thermostat-battery',
@@ -575,6 +597,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -600,6 +623,7 @@
'original_name': 'Humidity',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:3_humidity',
@@ -627,6 +651,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -652,6 +677,7 @@
'original_name': 'Signal strength',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_signal',
'unique_id': 'airzone_unique_id_1:3_thermostat-signal',
@@ -678,6 +704,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -703,6 +730,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:3_temp',
@@ -730,6 +758,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -755,6 +784,7 @@
'original_name': 'Battery',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:5_thermostat-battery',
@@ -782,6 +812,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -807,6 +838,7 @@
'original_name': 'Humidity',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:5_humidity',
@@ -834,6 +866,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -859,6 +892,7 @@
'original_name': 'Signal strength',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_signal',
'unique_id': 'airzone_unique_id_1:5_thermostat-signal',
@@ -885,6 +919,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -910,6 +945,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:5_temp',
@@ -937,6 +973,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -962,6 +999,7 @@
'original_name': 'Battery',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:2_thermostat-battery',
@@ -989,6 +1027,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1014,6 +1053,7 @@
'original_name': 'Humidity',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:2_humidity',
@@ -1041,6 +1081,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1066,6 +1107,7 @@
'original_name': 'Signal strength',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_signal',
'unique_id': 'airzone_unique_id_1:2_thermostat-signal',
@@ -1092,6 +1134,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1117,6 +1160,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:2_temp',
@@ -1144,6 +1188,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Salon Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1169,6 +1214,7 @@
'original_name': 'Humidity',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:1_humidity',
@@ -1196,6 +1242,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Salon Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1221,6 +1268,7 @@
'original_name': 'Temperature',
'platform': 'airzone',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'airzone_unique_id_1:1_temp',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Absolute pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -35,6 +36,7 @@
'original_name': 'Absolute pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'absolute_pressure',
'unique_id': 'AA:AA:AA:AA:AA:AA_baromabsin',
@@ -64,6 +66,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Daily rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -95,6 +98,7 @@
'original_name': 'Daily rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'daily_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_dailyrainin',
@@ -124,6 +128,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Dew point',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -152,6 +157,7 @@
'original_name': 'Dew point',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dew_point',
'unique_id': 'AA:AA:AA:AA:AA:AA_dewPoint',
@@ -181,6 +187,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Feels like',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -209,6 +216,7 @@
'original_name': 'Feels like',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'feels_like',
'unique_id': 'AA:AA:AA:AA:AA:AA_feelsLike',
@@ -238,6 +246,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Hourly rain',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -269,6 +278,7 @@
'original_name': 'Hourly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hourly_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_hourlyrainin',
@@ -298,6 +308,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -326,6 +337,7 @@
'original_name': 'Humidity',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_humidity',
@@ -355,6 +367,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Irradiance',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -383,6 +396,7 @@
'original_name': 'Irradiance',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_solarradiation',
@@ -412,6 +426,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Last rain',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -435,6 +450,7 @@
'original_name': 'Last rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'last_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_lastRain',
@@ -462,6 +478,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Max daily gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -493,6 +510,7 @@
'original_name': 'Max daily gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'max_daily_gust',
'unique_id': 'AA:AA:AA:AA:AA:AA_maxdailygust',
@@ -522,6 +540,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Monthly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -553,6 +572,7 @@
'original_name': 'Monthly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'monthly_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_monthlyrainin',
@@ -582,6 +602,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Relative pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -613,6 +634,7 @@
'original_name': 'Relative pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'relative_pressure',
'unique_id': 'AA:AA:AA:AA:AA:AA_baromrelin',
@@ -642,6 +664,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -670,6 +693,7 @@
'original_name': 'Temperature',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_tempf',
@@ -699,6 +723,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A UV index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -727,6 +752,7 @@
'original_name': 'UV index',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uv_index',
'unique_id': 'AA:AA:AA:AA:AA:AA_uv',
@@ -755,6 +781,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Weekly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -786,6 +813,7 @@
'original_name': 'Weekly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'weekly_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_weeklyrainin',
@@ -815,6 +843,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Wind direction',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
@@ -843,6 +872,7 @@
'original_name': 'Wind direction',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wind_direction',
'unique_id': 'AA:AA:AA:AA:AA:AA_winddir',
@@ -872,6 +902,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Wind gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -903,6 +934,7 @@
'original_name': 'Wind gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wind_gust',
'unique_id': 'AA:AA:AA:AA:AA:AA_windgustmph',
@@ -932,6 +964,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Wind speed',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -963,6 +996,7 @@
'original_name': 'Wind speed',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_windspeedmph',
@@ -992,6 +1026,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Absolute pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1023,6 +1058,7 @@
'original_name': 'Absolute pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'absolute_pressure',
'unique_id': 'CC:CC:CC:CC:CC:CC_baromabsin',
@@ -1052,6 +1088,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Daily rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -1083,6 +1120,7 @@
'original_name': 'Daily rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'daily_rain',
'unique_id': 'CC:CC:CC:CC:CC:CC_dailyrainin',
@@ -1112,6 +1150,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Dew point',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1140,6 +1179,7 @@
'original_name': 'Dew point',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dew_point',
'unique_id': 'CC:CC:CC:CC:CC:CC_dewPoint',
@@ -1169,6 +1209,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Feels like',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1197,6 +1238,7 @@
'original_name': 'Feels like',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'feels_like',
'unique_id': 'CC:CC:CC:CC:CC:CC_feelsLike',
@@ -1226,6 +1268,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Hourly rain',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1257,6 +1300,7 @@
'original_name': 'Hourly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hourly_rain',
'unique_id': 'CC:CC:CC:CC:CC:CC_hourlyrainin',
@@ -1286,6 +1330,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1314,6 +1359,7 @@
'original_name': 'Humidity',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'CC:CC:CC:CC:CC:CC_humidity',
@@ -1343,6 +1389,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Irradiance',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1371,6 +1418,7 @@
'original_name': 'Irradiance',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'CC:CC:CC:CC:CC:CC_solarradiation',
@@ -1400,6 +1448,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Last rain',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1423,6 +1472,7 @@
'original_name': 'Last rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'last_rain',
'unique_id': 'CC:CC:CC:CC:CC:CC_lastRain',
@@ -1450,6 +1500,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Max daily gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1481,6 +1532,7 @@
'original_name': 'Max daily gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'max_daily_gust',
'unique_id': 'CC:CC:CC:CC:CC:CC_maxdailygust',
@@ -1510,6 +1562,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Monthly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -1541,6 +1594,7 @@
'original_name': 'Monthly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'monthly_rain',
'unique_id': 'CC:CC:CC:CC:CC:CC_monthlyrainin',
@@ -1570,6 +1624,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Relative pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1601,6 +1656,7 @@
'original_name': 'Relative pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'relative_pressure',
'unique_id': 'CC:CC:CC:CC:CC:CC_baromrelin',
@@ -1630,6 +1686,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1658,6 +1715,7 @@
'original_name': 'Temperature',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'CC:CC:CC:CC:CC:CC_tempf',
@@ -1687,6 +1745,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C UV index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1715,6 +1774,7 @@
'original_name': 'UV index',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uv_index',
'unique_id': 'CC:CC:CC:CC:CC:CC_uv',
@@ -1743,6 +1803,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Weekly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -1774,6 +1835,7 @@
'original_name': 'Weekly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'weekly_rain',
'unique_id': 'CC:CC:CC:CC:CC:CC_weeklyrainin',
@@ -1803,6 +1865,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Wind direction',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
@@ -1831,6 +1894,7 @@
'original_name': 'Wind direction',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wind_direction',
'unique_id': 'CC:CC:CC:CC:CC:CC_winddir',
@@ -1860,6 +1924,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Wind gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1891,6 +1956,7 @@
'original_name': 'Wind gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wind_gust',
'unique_id': 'CC:CC:CC:CC:CC:CC_windgustmph',
@@ -1920,6 +1986,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Wind speed',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1951,6 +2018,7 @@
'original_name': 'Wind speed',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'CC:CC:CC:CC:CC:CC_windspeedmph',
@@ -1980,6 +2048,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Absolute pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2011,6 +2080,7 @@
'original_name': 'Absolute pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'absolute_pressure',
'unique_id': 'DD:DD:DD:DD:DD:DD_baromabsin',
@@ -2039,6 +2109,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Daily rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -2070,6 +2141,7 @@
'original_name': 'Daily rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'daily_rain',
'unique_id': 'DD:DD:DD:DD:DD:DD_dailyrainin',
@@ -2098,6 +2170,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Dew point',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2126,6 +2199,7 @@
'original_name': 'Dew point',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dew_point',
'unique_id': 'DD:DD:DD:DD:DD:DD_dewPoint',
@@ -2154,6 +2228,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Feels like',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2182,6 +2257,7 @@
'original_name': 'Feels like',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'feels_like',
'unique_id': 'DD:DD:DD:DD:DD:DD_feelsLike',
@@ -2210,6 +2286,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Hourly rain',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2241,6 +2318,7 @@
'original_name': 'Hourly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hourly_rain',
'unique_id': 'DD:DD:DD:DD:DD:DD_hourlyrainin',
@@ -2269,6 +2347,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2297,6 +2376,7 @@
'original_name': 'Humidity',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'DD:DD:DD:DD:DD:DD_humidity',
@@ -2325,6 +2405,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Irradiance',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2353,6 +2434,7 @@
'original_name': 'Irradiance',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'DD:DD:DD:DD:DD:DD_solarradiation',
@@ -2381,6 +2463,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Max daily gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2412,6 +2495,7 @@
'original_name': 'Max daily gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'max_daily_gust',
'unique_id': 'DD:DD:DD:DD:DD:DD_maxdailygust',
@@ -2440,6 +2524,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Monthly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -2471,6 +2556,7 @@
'original_name': 'Monthly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'monthly_rain',
'unique_id': 'DD:DD:DD:DD:DD:DD_monthlyrainin',
@@ -2499,6 +2585,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Relative pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2530,6 +2617,7 @@
'original_name': 'Relative pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'relative_pressure',
'unique_id': 'DD:DD:DD:DD:DD:DD_baromrelin',
@@ -2558,6 +2646,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2586,6 +2675,7 @@
'original_name': 'Temperature',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'DD:DD:DD:DD:DD:DD_tempf',
@@ -2614,6 +2704,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D UV index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2642,6 +2733,7 @@
'original_name': 'UV index',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uv_index',
'unique_id': 'DD:DD:DD:DD:DD:DD_uv',
@@ -2669,6 +2761,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Weekly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -2700,6 +2793,7 @@
'original_name': 'Weekly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'weekly_rain',
'unique_id': 'DD:DD:DD:DD:DD:DD_weeklyrainin',
@@ -2728,6 +2822,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Wind direction',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
@@ -2756,6 +2851,7 @@
'original_name': 'Wind direction',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wind_direction',
'unique_id': 'DD:DD:DD:DD:DD:DD_winddir',
@@ -2784,6 +2880,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Wind gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2815,6 +2912,7 @@
'original_name': 'Wind gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wind_gust',
'unique_id': 'DD:DD:DD:DD:DD:DD_windgustmph',
@@ -2843,6 +2941,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Wind speed',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2874,6 +2973,7 @@
'original_name': 'Wind speed',
'platform': 'ambient_network',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'DD:DD:DD:DD:DD:DD_windspeedmph',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics core_samba',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -29,6 +30,7 @@
'original_name': 'core_samba',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'addons',
'unique_id': 'addon_core_samba_active_installations',
@@ -55,6 +57,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics hacs (custom)',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -80,6 +83,7 @@
'original_name': 'hacs (custom)',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'custom_integrations',
'unique_id': 'custom_hacs_active_installations',
@@ -106,6 +110,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics myq',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -131,6 +136,7 @@
'original_name': 'myq',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'core_integrations',
'unique_id': 'core_myq_active_installations',
@@ -157,6 +163,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics spotify',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -182,6 +189,7 @@
'original_name': 'spotify',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'core_integrations',
'unique_id': 'core_spotify_active_installations',
@@ -208,6 +216,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics Total active installations',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -233,6 +242,7 @@
'original_name': 'Total active installations',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_active_installations',
'unique_id': 'total_active_installations',
@@ -259,6 +269,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics Total reported integrations',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -284,6 +295,7 @@
'original_name': 'Total reported integrations',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_reports_integrations',
'unique_id': 'total_reports_integrations',
@@ -310,6 +322,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics YouTube',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -335,6 +348,7 @@
'original_name': 'YouTube',
'platform': 'analytics_insights',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'core_integrations',
'unique_id': 'core_youtube_active_installations',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater Energy usage',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -32,6 +33,7 @@
'original_name': 'Energy usage',
'platform': 'aosmith',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy_usage',
'unique_id': 'energy_usage_junctionId',
@@ -59,6 +61,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater Hot water availability',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -82,6 +85,7 @@
'original_name': 'Hot water availability',
'platform': 'aosmith',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hot_water_availability',
'unique_id': 'hot_water_availability_junctionId',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater',
'capabilities': dict({
'max_temp': 130,
'min_temp': 95,
@@ -30,6 +31,7 @@
'original_name': None,
'platform': 'aosmith',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <WaterHeaterEntityFeature: 5>,
'translation_key': None,
'unique_id': 'junctionId',
@@ -62,6 +64,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater',
'capabilities': dict({
'max_temp': 130,
'min_temp': 95,
@@ -93,6 +96,7 @@
'original_name': None,
'platform': 'aosmith',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <WaterHeaterEntityFeature: 7>,
'translation_key': None,
'unique_id': 'junctionId',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title DC 1 short circuit error status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'DC 1 short circuit error status',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dc_1_short_circuit_error_status',
'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status',
@@ -52,6 +54,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title DC 2 short circuit error status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -75,6 +78,7 @@
'original_name': 'DC 2 short circuit error status',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'dc_2_short_circuit_error_status',
'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status',
@@ -100,6 +104,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Off-grid status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -123,6 +128,7 @@
'original_name': 'Off-grid status',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'off_grid_status',
'unique_id': 'MY_SERIAL_NUMBER_off_grid_status',
@@ -148,6 +154,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Output fault status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -171,6 +178,7 @@
'original_name': 'Output fault status',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'output_fault_status',
'unique_id': 'MY_SERIAL_NUMBER_output_fault_status',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Max output',
'capabilities': dict({
'max': 1000,
'min': 0,
@@ -32,6 +33,7 @@
'original_name': 'Max output',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'max_output',
'unique_id': 'MY_SERIAL_NUMBER_output_limit',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Lifetime production of P1',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -29,6 +30,7 @@
'original_name': 'Lifetime production of P1',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'lifetime_production_p1',
'unique_id': 'MY_SERIAL_NUMBER_lifetime_production_p1',
@@ -56,6 +58,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Lifetime production of P2',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -81,6 +84,7 @@
'original_name': 'Lifetime production of P2',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'lifetime_production_p2',
'unique_id': 'MY_SERIAL_NUMBER_lifetime_production_p2',
@@ -108,6 +112,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Power of P1',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -133,6 +138,7 @@
'original_name': 'Power of P1',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_power_p1',
'unique_id': 'MY_SERIAL_NUMBER_total_power_p1',
@@ -160,6 +166,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Power of P2',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -185,6 +192,7 @@
'original_name': 'Power of P2',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_power_p2',
'unique_id': 'MY_SERIAL_NUMBER_total_power_p2',
@@ -212,6 +220,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Production of today',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -237,6 +246,7 @@
'original_name': 'Production of today',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'today_production',
'unique_id': 'MY_SERIAL_NUMBER_today_production',
@@ -264,6 +274,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Production of today from P1',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -289,6 +300,7 @@
'original_name': 'Production of today from P1',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'today_production_p1',
'unique_id': 'MY_SERIAL_NUMBER_today_production_p1',
@@ -316,6 +328,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Production of today from P2',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -341,6 +354,7 @@
'original_name': 'Production of today from P2',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'today_production_p2',
'unique_id': 'MY_SERIAL_NUMBER_today_production_p2',
@@ -368,6 +382,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Total lifetime production',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -393,6 +408,7 @@
'original_name': 'Total lifetime production',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'lifetime_production',
'unique_id': 'MY_SERIAL_NUMBER_lifetime_production',
@@ -420,6 +436,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Total power',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -445,6 +462,7 @@
'original_name': 'Total power',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_power',
'unique_id': 'MY_SERIAL_NUMBER_total_power',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Inverter status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Inverter status',
'platform': 'apsystems',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'inverter_status',
'unique_id': 'MY_SERIAL_NUMBER_inverter_status',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Battery',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -27,6 +28,7 @@
'original_name': 'Battery',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'DSN-battery',
@@ -53,6 +55,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Salt left side percentage',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -78,6 +81,7 @@
'original_name': 'Salt left side percentage',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'salt_left_side_percentage',
'unique_id': 'DSN-salt_left_side_percentage',
@@ -104,6 +108,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Salt left side time remaining',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -127,6 +132,7 @@
'original_name': 'Salt left side time remaining',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'salt_left_side_time_remaining',
'unique_id': 'DSN-salt_left_side_time_remaining',
@@ -153,6 +159,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Salt right side percentage',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -178,6 +185,7 @@
'original_name': 'Salt right side percentage',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'salt_right_side_percentage',
'unique_id': 'DSN-salt_right_side_percentage',
@@ -204,6 +212,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Salt right side time remaining',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -227,6 +236,7 @@
'original_name': 'Salt right side time remaining',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'salt_right_side_time_remaining',
'unique_id': 'DSN-salt_right_side_time_remaining',
@@ -253,6 +263,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Wi-Fi strength',
'capabilities': dict({
'options': list([
'high',
@@ -282,6 +293,7 @@
'original_name': 'Wi-Fi strength',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wi_fi_strength',
'unique_id': 'DSN-wi_fi_strength',
@@ -4,6 +4,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Air quality index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -29,6 +30,7 @@
'original_name': 'Air quality index',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test-serial-number_AQI',
@@ -40,6 +42,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Carbon dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -65,6 +68,7 @@
'original_name': 'Carbon dioxide',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test-serial-number_CO2',
@@ -76,6 +80,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -101,6 +106,7 @@
'original_name': 'Humidity',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test-serial-number_Humidity',
@@ -112,6 +118,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor PM10',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -137,6 +144,7 @@
'original_name': 'PM10',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test-serial-number_PM10',
@@ -148,6 +156,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -173,6 +182,7 @@
'original_name': 'PM2.5',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test-serial-number_PM25',
@@ -184,6 +194,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -209,6 +220,7 @@
'original_name': 'Temperature',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test-serial-number_Temperature',
@@ -220,6 +232,7 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Total volatile organic compounds',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -245,6 +258,7 @@
'original_name': 'Total volatile organic compounds',
'platform': 'arve',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'tvoc',
'unique_id': 'test-serial-number_TVOC',
+2 -21
View File
@@ -37,7 +37,7 @@ from tests.common import (
mock_platform,
)
from tests.components.stt.common import MockSTTProvider, MockSTTProviderEntity
from tests.components.tts.common import MockTTSEntity, MockTTSProvider
from tests.components.tts.common import MockTTSProvider
_TRANSCRIPT = "test transcript"
@@ -68,15 +68,6 @@ async def mock_tts_provider() -> MockTTSProvider:
return provider
@pytest.fixture
def mock_tts_entity() -> MockTTSEntity:
"""Test TTS entity."""
entity = MockTTSEntity("en")
entity._attr_unique_id = "test_tts"
entity._attr_supported_languages = ["en-US"]
return entity
@pytest.fixture
async def mock_stt_provider() -> MockSTTProvider:
"""Mock STT provider."""
@@ -207,7 +198,6 @@ async def init_supporting_components(
mock_stt_provider: MockSTTProvider,
mock_stt_provider_entity: MockSTTProviderEntity,
mock_tts_provider: MockTTSProvider,
mock_tts_entity: MockTTSEntity,
mock_wake_word_provider_entity: MockWakeWordEntity,
mock_wake_word_provider_entity2: MockWakeWordEntity2,
config_flow_fixture,
@@ -219,7 +209,7 @@ async def init_supporting_components(
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(
config_entry, [Platform.STT, Platform.TTS, Platform.WAKE_WORD]
config_entry, [Platform.STT, Platform.WAKE_WORD]
)
return True
@@ -240,14 +230,6 @@ async def init_supporting_components(
"""Set up test stt platform via config entry."""
async_add_entities([mock_stt_provider_entity])
async def async_setup_entry_tts_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up test tts platform via config entry."""
async_add_entities([mock_tts_entity])
async def async_setup_entry_wake_word_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -271,7 +253,6 @@ async def init_supporting_components(
"test.tts",
MockTTSPlatform(
async_get_engine=AsyncMock(return_value=mock_tts_provider),
async_setup_entry=async_setup_entry_tts_platform,
),
)
mock_platform(
@@ -74,17 +74,17 @@
}),
dict({
'data': dict({
'engine': 'tts.test',
'language': 'en_US',
'engine': 'test',
'language': 'en-US',
'tts_input': "Sorry, I couldn't understand that",
'voice': None,
'voice': 'james_earl_jones',
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
}),
dict({
'data': dict({
'tts_output': dict({
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -395,17 +395,17 @@
}),
dict({
'data': dict({
'engine': 'tts.test',
'language': 'en_US',
'engine': 'test',
'language': 'en-US',
'tts_input': "Sorry, I couldn't understand that",
'voice': None,
'voice': 'james_earl_jones',
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
}),
dict({
'data': dict({
'tts_output': dict({
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -1,158 +1,4 @@
# serializer version: 1
# name: test_chat_log_tts_streaming[to_stream_tts0]
list([
dict({
'data': dict({
'conversation_id': 'mock-ulid',
'language': 'en',
'pipeline': <ANY>,
'tts_output': dict({
'mime_type': 'audio/mpeg',
'token': 'mocked-token.mp3',
'url': '/api/tts_proxy/mocked-token.mp3',
}),
}),
'type': <PipelineEventType.RUN_START: 'run-start'>,
}),
dict({
'data': dict({
'conversation_id': 'mock-ulid',
'device_id': None,
'engine': 'test-agent',
'intent_input': 'Set a timer',
'language': 'en',
'prefer_local_intents': False,
}),
'type': <PipelineEventType.INTENT_START: 'intent-start'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'role': 'assistant',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': 'hello,',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': ' ',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': 'how',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': ' ',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': 'are',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': ' ',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': 'you',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'chat_log_delta': dict({
'content': '?',
}),
}),
'type': <PipelineEventType.INTENT_PROGRESS: 'intent-progress'>,
}),
dict({
'data': dict({
'intent_output': dict({
'continue_conversation': True,
'conversation_id': <ANY>,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'hello, how are you?',
}),
}),
}),
}),
'processed_locally': False,
}),
'type': <PipelineEventType.INTENT_END: 'intent-end'>,
}),
dict({
'data': dict({
'engine': 'tts.test',
'language': 'en_US',
'tts_input': 'hello, how are you?',
'voice': None,
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
}),
dict({
'data': dict({
'tts_output': dict({
'media_id': 'media-source://tts/tts.test?message=hello,+how+are+you?&language=en_US&tts_options=%7B%7D',
'mime_type': 'audio/mpeg',
'token': 'mocked-token.mp3',
'url': '/api/tts_proxy/mocked-token.mp3',
}),
}),
'type': <PipelineEventType.TTS_END: 'tts-end'>,
}),
dict({
'data': None,
'type': <PipelineEventType.RUN_END: 'run-end'>,
}),
])
# ---
# name: test_pipeline_language_used_instead_of_conversation_language
list([
dict({
@@ -71,16 +71,16 @@
# ---
# name: test_audio_pipeline.5
dict({
'engine': 'tts.test',
'language': 'en_US',
'engine': 'test',
'language': 'en-US',
'tts_input': "Sorry, I couldn't understand that",
'voice': None,
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline.6
dict({
'tts_output': dict({
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -162,16 +162,16 @@
# ---
# name: test_audio_pipeline_debug.5
dict({
'engine': 'tts.test',
'language': 'en_US',
'engine': 'test',
'language': 'en-US',
'tts_input': "Sorry, I couldn't understand that",
'voice': None,
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline_debug.6
dict({
'tts_output': dict({
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -265,16 +265,16 @@
# ---
# name: test_audio_pipeline_with_enhancements.5
dict({
'engine': 'tts.test',
'language': 'en_US',
'engine': 'test',
'language': 'en-US',
'tts_input': "Sorry, I couldn't understand that",
'voice': None,
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline_with_enhancements.6
dict({
'tts_output': dict({
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -378,16 +378,16 @@
# ---
# name: test_audio_pipeline_with_wake_word_no_timeout.7
dict({
'engine': 'tts.test',
'language': 'en_US',
'engine': 'test',
'language': 'en-US',
'tts_input': "Sorry, I couldn't understand that",
'voice': None,
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline_with_wake_word_no_timeout.8
dict({
'tts_output': dict({
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
+11 -162
View File
@@ -40,7 +40,6 @@ from . import MANY_LANGUAGES, process_events
from .conftest import (
MockSTTProvider,
MockSTTProviderEntity,
MockTTSEntity,
MockTTSProvider,
MockWakeWordEntity,
make_10ms_chunk,
@@ -63,12 +62,6 @@ async def load_homeassistant(hass: HomeAssistant) -> None:
assert await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
async def disable_tts_entity(mock_tts_entity: tts.TextToSpeechEntity) -> None:
"""Disable the TTS entity."""
mock_tts_entity._attr_entity_registry_enabled_default = False
@pytest.mark.usefixtures("init_components")
async def test_load_pipelines(hass: HomeAssistant) -> None:
"""Make sure that we can load/save data correctly."""
@@ -290,7 +283,6 @@ async def test_migrate_pipeline_store(
@pytest.mark.usefixtures("init_supporting_components")
@pytest.mark.usefixtures("disable_tts_entity")
async def test_create_default_pipeline(hass: HomeAssistant) -> None:
"""Test async_create_default_pipeline."""
assert await async_setup_component(hass, "assist_pipeline", {})
@@ -438,7 +430,6 @@ async def test_default_pipeline_no_stt_tts(
],
)
@pytest.mark.usefixtures("init_supporting_components")
@pytest.mark.usefixtures("disable_tts_entity")
async def test_default_pipeline(
hass: HomeAssistant,
mock_stt_provider_entity: MockSTTProviderEntity,
@@ -483,7 +474,6 @@ async def test_default_pipeline(
@pytest.mark.usefixtures("init_supporting_components")
@pytest.mark.usefixtures("disable_tts_entity")
async def test_default_pipeline_unsupported_stt_language(
hass: HomeAssistant, mock_stt_provider_entity: MockSTTProviderEntity
) -> None:
@@ -514,7 +504,6 @@ async def test_default_pipeline_unsupported_stt_language(
@pytest.mark.usefixtures("init_supporting_components")
@pytest.mark.usefixtures("disable_tts_entity")
async def test_default_pipeline_unsupported_tts_language(
hass: HomeAssistant, mock_tts_provider: MockTTSProvider
) -> None:
@@ -836,7 +825,7 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None:
async def test_tts_audio_output(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_tts_entity: MockTTSProvider,
mock_tts_provider: MockTTSProvider,
init_components,
pipeline_data: assist_pipeline.pipeline.PipelineData,
mock_chat_session: chat_session.ChatSession,
@@ -880,7 +869,7 @@ async def test_tts_audio_output(
== 1
)
with patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio:
with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio:
await pipeline_input.execute()
for event in events:
@@ -892,14 +881,14 @@ async def test_tts_audio_output(
# Ensure that no unsupported options were passed in
assert mock_get_tts_audio.called
options = mock_get_tts_audio.call_args_list[0].kwargs["options"]
extra_options = set(options).difference(mock_tts_entity.supported_options)
extra_options = set(options).difference(mock_tts_provider.supported_options)
assert len(extra_options) == 0, extra_options
async def test_tts_wav_preferred_format(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_tts_entity: MockTTSEntity,
mock_tts_provider: MockTTSProvider,
init_components,
mock_chat_session: chat_session.ChatSession,
pipeline_data: assist_pipeline.pipeline.PipelineData,
@@ -931,7 +920,7 @@ async def test_tts_wav_preferred_format(
await pipeline_input.validate()
# Make the TTS provider support preferred format options
supported_options = list(mock_tts_entity.supported_options or [])
supported_options = list(mock_tts_provider.supported_options or [])
supported_options.extend(
[
tts.ATTR_PREFERRED_FORMAT,
@@ -942,8 +931,8 @@ async def test_tts_wav_preferred_format(
)
with (
patch.object(mock_tts_entity, "_supported_options", supported_options),
patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio,
patch.object(mock_tts_provider, "_supported_options", supported_options),
patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio,
):
await pipeline_input.execute()
@@ -966,7 +955,7 @@ async def test_tts_wav_preferred_format(
async def test_tts_dict_preferred_format(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_tts_entity: MockTTSEntity,
mock_tts_provider: MockTTSProvider,
init_components,
mock_chat_session: chat_session.ChatSession,
pipeline_data: assist_pipeline.pipeline.PipelineData,
@@ -1003,7 +992,7 @@ async def test_tts_dict_preferred_format(
await pipeline_input.validate()
# Make the TTS provider support preferred format options
supported_options = list(mock_tts_entity.supported_options or [])
supported_options = list(mock_tts_provider.supported_options or [])
supported_options.extend(
[
tts.ATTR_PREFERRED_FORMAT,
@@ -1014,8 +1003,8 @@ async def test_tts_dict_preferred_format(
)
with (
patch.object(mock_tts_entity, "_supported_options", supported_options),
patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio,
patch.object(mock_tts_provider, "_supported_options", supported_options),
patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio,
):
await pipeline_input.execute()
@@ -1556,143 +1545,3 @@ async def test_pipeline_language_used_instead_of_conversation_language(
mock_async_converse.call_args_list[0].kwargs.get("language")
== pipeline.language
)
@pytest.mark.parametrize(
"to_stream_tts",
[
[
"hello,",
" ",
"how",
" ",
"are",
" ",
"you",
"?",
]
],
)
async def test_chat_log_tts_streaming(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
init_components,
mock_chat_session: chat_session.ChatSession,
snapshot: SnapshotAssertion,
mock_tts_entity: MockTTSEntity,
pipeline_data: assist_pipeline.pipeline.PipelineData,
to_stream_tts: list[str],
) -> None:
"""Test that chat log events are streamed to the TTS entity."""
events: list[assist_pipeline.PipelineEvent] = []
pipeline_store = pipeline_data.pipeline_store
pipeline_id = pipeline_store.async_get_preferred_item()
pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id)
await assist_pipeline.pipeline.async_update_pipeline(
hass, pipeline, conversation_engine="test-agent"
)
pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id)
pipeline_input = assist_pipeline.pipeline.PipelineInput(
intent_input="Set a timer",
session=mock_chat_session,
run=assist_pipeline.pipeline.PipelineRun(
hass,
context=Context(),
pipeline=pipeline,
start_stage=assist_pipeline.PipelineStage.INTENT,
end_stage=assist_pipeline.PipelineStage.TTS,
event_callback=events.append,
),
)
received_tts = []
async def async_stream_tts_audio(
request: tts.TTSAudioRequest,
) -> tts.TTSAudioResponse:
"""Mock stream TTS audio."""
async def gen_data():
async for msg in request.message_gen:
received_tts.append(msg)
yield msg.encode()
return tts.TTSAudioResponse(
extension="mp3",
data_gen=gen_data(),
)
mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio
with patch(
"homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info",
return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"),
):
await pipeline_input.validate()
async def mock_converse(
hass: HomeAssistant,
text: str,
conversation_id: str | None,
context: Context,
language: str | None = None,
agent_id: str | None = None,
device_id: str | None = None,
extra_system_prompt: str | None = None,
):
"""Mock converse."""
conversation_input = conversation.ConversationInput(
text=text,
context=context,
conversation_id=conversation_id,
device_id=device_id,
language=language,
agent_id=agent_id,
extra_system_prompt=extra_system_prompt,
)
async def stream_llm_response():
yield {"role": "assistant"}
for chunk in to_stream_tts:
yield {"content": chunk}
with (
chat_session.async_get_chat_session(hass, conversation_id) as session,
conversation.async_get_chat_log(
hass,
session,
conversation_input,
) as chat_log,
):
async for _content in chat_log.async_add_delta_content_stream(
agent_id, stream_llm_response()
):
pass
intent_response = intent.IntentResponse(language)
intent_response.async_set_speech("".join(to_stream_tts))
return conversation.ConversationResult(
response=intent_response,
conversation_id=chat_log.conversation_id,
continue_conversation=chat_log.continue_conversation,
)
with patch(
"homeassistant.components.assist_pipeline.pipeline.conversation.async_converse",
mock_converse,
):
await pipeline_input.execute()
stream = tts.async_get_stream(hass, events[0].data["tts_output"]["token"])
assert stream is not None
tts_result = "".join(
[chunk.decode() async for chunk in stream.async_stream_result()]
)
streamed_text = "".join(to_stream_tts)
assert tts_result == streamed_text
assert len(received_tts) == 1
assert "".join(received_tts) == streamed_text
assert process_events(events) == snapshot
@@ -1153,9 +1153,9 @@ async def test_get_pipeline(
"name": "Home Assistant",
"stt_engine": "stt.mock_stt",
"stt_language": "en-US",
"tts_engine": "tts.test",
"tts_language": "en_US",
"tts_voice": None,
"tts_engine": "test",
"tts_language": "en-US",
"tts_voice": "james_earl_jones",
"wake_word_entity": None,
"wake_word_id": None,
"prefer_local_intents": False,
@@ -1179,9 +1179,9 @@ async def test_get_pipeline(
# It found these defaults
"stt_engine": "stt.mock_stt",
"stt_language": "en-US",
"tts_engine": "tts.test",
"tts_language": "en_US",
"tts_voice": None,
"tts_engine": "test",
"tts_language": "en-US",
"tts_voice": "james_earl_jones",
"wake_word_entity": None,
"wake_word_id": None,
"prefer_local_intents": False,
@@ -1266,9 +1266,9 @@ async def test_list_pipelines(
"name": "Home Assistant",
"stt_engine": "stt.mock_stt",
"stt_language": "en-US",
"tts_engine": "tts.test",
"tts_language": "en_US",
"tts_voice": None,
"tts_engine": "test",
"tts_language": "en-US",
"tts_voice": "james_earl_jones",
"wake_word_entity": None,
"wake_word_id": None,
"prefer_local_intents": False,

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