Compare commits

..

53 Commits

Author SHA1 Message Date
Paulus Schoutsen 554c6e0cca Fix service helper tests 2025-05-13 20:17:07 +00:00
Paulus Schoutsen 7bac640267 Add area motion entity ID 2025-05-13 17:29:25 +00:00
epenet 26796f87cd Add device registry snapshots to samsungtv tests (#144804)
* Add device registry snapshots to samsungtv tests

* Simplify

* Adjust

* Reduce
2025-05-13 18:20:43 +02:00
Åke Strandberg e2dd897ac7 Bump dependency pymiele -> 0.5.2 (#144758) 2025-05-13 18:19:49 +02:00
Retha Runolfsson 3bbe4baaf7 Update codeowner for switchbot Integration (#144829)
update codeowners
2025-05-13 18:16:05 +02:00
Alistair Francis d409b86217 Bump automower-ble to 0.2.1 (#144817) 2025-05-13 14:21:56 +01:00
Josef Zweck 7928c15849 Fix blocking call in azure_storage config flow (#144818)
* Fix blocking call in azure_storage config flow

* Fix blocking call in azure_storage config_flow as well

* move session getting to event flow
2025-05-13 14:23:41 +02:00
epenet d197debbc0 Improve SamsungTV config flow type hints (#144820) 2025-05-13 14:02:07 +02:00
Martin Hjelmare 55b9dee448 Fix Z-Wave unique id after controller reset (#144813) 2025-05-13 14:12:00 +03:00
epenet 5c6984d326 Do not abort on invalid host in SamsungTV user flow (#144794) 2025-05-13 10:47:26 +02:00
Josef Zweck a7787d6080 Fix blocking call in azure storage (#144803) 2025-05-13 10:46:46 +02:00
Jeremiah Paige 2db60340c2 Add typing to wsdot (#143117)
* increase wsdot typing

* remove Final types

* help out mypy

* simplify wsdot types

* minor wsdot type changes

* type wsdot state
2025-05-13 10:43:03 +02:00
Mick Vleeshouwer c121631fef Refactor config flow tests to improve result variable usage in Overkiz (#143374)
* Refactor test setup for unique ID migration in Overkiz integration

* Refactor test cases to unify result variable usage in Overkiz config flow tests (resultn -> result)

* Revert change in test_init
2025-05-13 10:35:32 +02:00
epenet b0fb16d48d Remove obsolete compatibility code from SamsungTV (#144800) 2025-05-13 09:54:26 +02:00
Franck Nijhof 3e07f6543e Update debugpy to v1.8.14 (#144755) 2025-05-13 08:14:55 +02:00
Brett Adams d4c2356c70 Create stream on demand in Teslemetry (#144777)
Create stream on demand
2025-05-13 08:05:33 +02:00
epenet eec617b391 Add comments to samsungtv config flow tests (#144787) 2025-05-13 07:54:37 +02:00
Maciej Bieniek b15c9ad130 Link Shelly device entry with Shelly BT scanner entry (#144626)
* Add BT address to DeviceInfo.connections

* Cleaning

* Use bluetooth_source property

* Add test

* Add connections property
2025-05-13 07:19:07 +02:00
Erik Montnemery 0128d85999 Move sun conditions to the sun integration (#144742) 2025-05-12 23:03:37 +01:00
David Rapan e69ca0cf80 Bump aiodhcpwatcher to 1.2.0 (#144769) 2025-05-12 17:00:17 -05:00
Åke Strandberg 0719753be3 Set PARALLEL_UPDATES and update quality_scale for Miele integration (#144770)
Set PARALLEL_UPDATES and update quality_scale
2025-05-12 23:53:54 +02:00
Franck Nijhof ba3181d4e7 Update pipdeptree to 2.26.1 (#144775) 2025-05-12 23:52:27 +02:00
Guido Schmitz e58750555e Rework platform setup tests for devolo Home Network (#143114)
* Rework platform setup tests for devolo Home Network

* Fix sensor test

* Remove unload
2025-05-12 23:21:14 +02:00
Guido Schmitz 026687299d Assert resulting data in devolo Home Network test_form_reauth (#144760) 2025-05-12 21:28:40 +02:00
Martin Hjelmare 3eed552c56 Repair Z-Wave unknown controller (#144738)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-05-12 21:18:55 +02:00
J. Diego Rodríguez Royo 15a4514c7d Add MAC connection through DHCP discovery to Home Connect devices (#144611)
* Add MAC connection through DHCP discovery to Home Connect devices

* Update snapshots
2025-05-12 21:11:12 +02:00
Joakim Sørensen b5445c0061 Allow subscription_expired repair issue in cloud (#144316)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-05-12 19:48:20 +02:00
Øyvind Matheson Wergeland 1d0584a90d Bump gcal-sync to 7.0.1 (#144718)
Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-05-12 19:45:34 +02:00
Matthias Alphart 158b795c70 Update xknx to 3.8.0 (#144753) 2025-05-12 19:45:02 +02:00
Paulus Schoutsen 4994229215 Track if TTS entity supports streaming input (#144697)
* Track if entity supports streaming

* Make class method
2025-05-12 13:44:39 -04:00
Andre Lengwenus c022c32d2f Simplify unique config_entry check for LCN (#135756)
* Simplify check for unique config_entry

* Fix tests

* Fix reconfigure flow

* Add check for unchanging IP/port combination

* Remove explicit check for unchanged IP/port combination
2025-05-12 19:44:24 +02:00
Joost Lekkerkerker d2ef3ca100 Fill in Plaato URL via placeholders (#144754) 2025-05-12 19:37:45 +02:00
Manu 00faadcfea Improve config flow description in ntfy integration (#144581) 2025-05-12 19:36:53 +02:00
Norbert Rittel a6ff52b300 Fix outdated help center URL in plaato (#144748)
* Fix outdated help center URL in `plaato`

* Remove excessive space character
2025-05-12 19:12:49 +02:00
Joakim Sørensen da0d65ca5b Log instead of ValueError for missing cloud translation key (#144732)
* Log instead of ValueError for missing translation key

* Update homeassistant/components/cloud/client.py
2025-05-12 18:59:38 +02:00
Paulus Schoutsen 2266e97417 Add a test for Assist Pipeline streaming deltas to TTS (#144711)
* Add a test for Assist Pipeline streaming deltas to TTS

* Adjust tests to new TTS engine
2025-05-12 12:15:05 -04:00
Norbert Rittel d471de5645 Spelling fixes in user-facing strings of fronius (#144744) 2025-05-12 16:54:22 +02:00
Norbert Rittel 38674f0dc2 Add missing hyphen to "password-protected" in Shelly (#144746) 2025-05-12 17:47:14 +03:00
Erik Montnemery b192ca4bad Make it possible to subscribe to frontend user store (#144724) 2025-05-12 16:01:42 +02:00
epenet 73a59523f5 Merge websocket test constants in samsungtv tests (#144741) 2025-05-12 15:51:21 +02:00
Erik Montnemery 05324dedd0 Deduplicate condition schemas (#144739) 2025-05-12 15:38:31 +02:00
Norbert Rittel f1e5f73d7e Improve user-facing strings of velbus (#144716)
- add the missing hyphen to "password-protected"
- resolve missing genitive in `sync_clock` action description
- resolve singular/plural mismatch in `set_memo_text` action description
2025-05-12 16:35:06 +03:00
Robert Resch 7b23f21712 Remove deprecated camera async_handle_web_rtc_offer function (#144561) 2025-05-12 14:47:49 +02:00
epenet 4dde314338 Remove obsolete tests in SamsungTV (#144735) 2025-05-12 13:45:20 +02:00
Erik Montnemery cba12fb598 Refactor frontend user store (#144723)
* Refactor frontend user store

* Address review comments
2025-05-12 12:00:32 +02:00
epenet 63e38b4d8d Rename samsung encrypted websocket test fixtures and constants (#144726)
* Rename samsung encrypted websocket test fixtures and constants

* More

* More
2025-05-12 11:36:22 +02:00
Simone Chemelli 7eded95315 Bump aiocomelit to 0.12.1 (#144720) 2025-05-12 11:23:44 +02:00
epenet e493fe1105 Rename samsung websocket test fixtures and constants (#144719) 2025-05-12 10:27:29 +02:00
Åke Strandberg 646c230940 Add target temp sensor to Miele washing machines (#144507) 2025-05-12 09:42:27 +02:00
Thomas55555 5276a3688e Fix wrong state in Husqvarna Automower (#144684) 2025-05-12 09:39:30 +02:00
Allen Porter 0616bf16f4 Bump ical to 9.2.2 (#144713) 2025-05-12 09:37:57 +02:00
epenet fbe1811e2b Improve SamsungTV test coverage (#144717) 2025-05-12 09:23:55 +02:00
epenet 2333c10915 Rename samsung legacy test fixtures and constants (#144715)
* Rename samsung legacy test fixtures and constants

* More
2025-05-12 09:13:23 +02:00
711 changed files with 3794 additions and 16367 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
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
@@ -39,11 +39,20 @@ async def async_setup_entry(
session = async_create_clientsession(
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
)
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),
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
)
try:
@@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__)
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for azure storage."""
def get_account_url(self, account_name: str) -> str:
"""Get the account URL."""
return f"https://{account_name}.blob.core.windows.net/"
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)
async def validate_config(
self, container_client: ContainerClient
@@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match(
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
)
container_client = ContainerClient(
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
container_client = await self.get_container_client(
account_name=user_input[CONF_ACCOUNT_NAME],
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
@@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry = self._get_reauth_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
container_client = await self.get_container_client(
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
if not errors:
return self.async_update_reload_and_abort(
@@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(
reconfigure_entry.data[CONF_ACCOUNT_NAME]
),
container_client = await self.get_container_client(
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
if not errors:
+10 -66
View File
@@ -55,7 +55,6 @@ 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
@@ -86,10 +85,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
WebRTCClientConfiguration,
WebRTCError,
WebRTCError, # noqa: F401
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
@@ -473,9 +472,6 @@ 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
@@ -579,15 +575,6 @@ 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:
@@ -600,42 +587,6 @@ 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
@@ -764,9 +715,7 @@ 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_sync_webrtc or self._supports_native_async_webrtc
):
if not 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
@@ -798,17 +747,12 @@ 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()
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
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
return config
@@ -838,7 +782,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_sync_webrtc or self._supports_native_async_webrtc:
if self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
else:
@@ -111,13 +111,11 @@ 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
+7 -1
View File
@@ -43,6 +43,7 @@ VALID_REPAIR_TRANSLATION_KEYS = {
"no_subscription",
"warn_bad_custom_domain_configuration",
"reset_bad_custom_domain_configuration",
"subscription_expired",
}
@@ -404,7 +405,12 @@ class CloudClient(Interface):
) -> None:
"""Create a repair issue."""
if translation_key not in VALID_REPAIR_TRANSLATION_KEYS:
raise ValueError(f"Invalid translation key {translation_key}")
_LOGGER.error(
"Invalid translation key %s for repair issue %s",
translation_key,
identifier,
)
return
async_create_issue(
hass=self._hass,
domain=DOMAIN,
@@ -73,6 +73,10 @@
"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.0"]
"requirements": ["aiocomelit==0.12.1"]
}
@@ -44,6 +44,7 @@ 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),
@@ -112,6 +113,7 @@ 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,13 +9,12 @@ 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, split_entity_id
from homeassistant.core import HomeAssistant, callback
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
@@ -23,7 +22,6 @@ 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)
@@ -318,43 +316,3 @@ 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.13"]
"requirements": ["debugpy==1.8.14"]
}
+1 -1
View File
@@ -14,7 +14,7 @@
],
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.1.1",
"aiodhcpwatcher==1.2.0",
"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 shut down triggered",
"wired_shut_down_triggered": "Wired shutdown 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",
+95 -50
View File
@@ -14,49 +14,78 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store
from homeassistant.util.hass_dict import HassKey
DATA_STORAGE: HassKey[tuple[dict[str, Store], dict[str, dict]]] = HassKey(
"frontend_storage"
)
DATA_STORAGE: HassKey[dict[str, UserStore]] = 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
) -> tuple[Store, dict[str, Any]]:
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
"""Access a user store."""
_initialize_frontend_storage(hass)
stores, data = hass.data[DATA_STORAGE]
stores = hass.data.setdefault(DATA_STORAGE, {})
if (store := stores.get(user_id)) is None:
store = stores[user_id] = Store(
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__(
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 {}
return store, data[user_id]
def with_store(
def with_user_store(
orig_func: Callable[
[HomeAssistant, ActiveConnection, dict[str, Any], Store, dict[str, Any]],
[HomeAssistant, ActiveConnection, dict[str, Any], UserStore],
Coroutine[Any, Any, None],
],
) -> Callable[
@@ -65,17 +94,17 @@ def with_store(
"""Decorate function to provide data."""
@wraps(orig_func)
async def with_store_func(
async def with_user_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, user_data = await async_user_store(hass, user_id)
store = await async_user_store(hass, user_id)
await orig_func(hass, connection, msg, store, user_data)
await orig_func(hass, connection, msg, store)
return with_store_func
return with_user_store_func
@websocket_api.websocket_command(
@@ -86,41 +115,57 @@ def with_store(
}
)
@websocket_api.async_response
@with_store
@with_user_store
async def websocket_set_user_data(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict[str, Any],
store: Store,
data: dict[str, Any],
store: UserStore,
) -> None:
"""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"]))
"""Handle set user data command."""
await store.async_set_item(msg["key"], msg["value"])
connection.send_result(msg["id"])
@websocket_api.websocket_command(
{vol.Required("type"): "frontend/get_user_data", vol.Optional("key"): str}
)
@websocket_api.async_response
@with_store
@with_user_store
async def websocket_get_user_data(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict[str, Any],
store: Store,
data: dict[str, Any],
store: UserStore,
) -> None:
"""Handle get global data command.
Async friendly.
"""
connection.send_message(
websocket_api.result_message(
msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data}
)
"""Handle get user data command."""
data = store.data
connection.send_result(
msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data}
)
@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}
)
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.0", "oauth2client==4.1.3", "ical==9.2.1"]
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
}
@@ -8,7 +8,8 @@ import jwt
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import DOMAIN
@@ -58,3 +59,22 @@ 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 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
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
return LawnMowerActivity.RETURNING
return LawnMowerActivity.MOWING
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.0"]
"requirements": ["automower-ble==0.2.1"]
}
+1 -1
View File
@@ -10,7 +10,7 @@
"iot_class": "local_push",
"loggers": ["xknx", "xknxproject"],
"requirements": [
"xknx==3.6.0",
"xknx==3.8.0",
"xknxproject==3.8.2",
"knx-frontend==2025.4.1.91934"
],
+18 -27
View File
@@ -19,7 +19,6 @@ 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
@@ -44,21 +43,6 @@ 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
@@ -120,19 +104,20 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None:
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)
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}
self._async_abort_entries_match(
{
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
}
)
if errors is not None:
if (error := await validate_connection(user_input)) is not None:
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
USER_SCHEMA, user_input
),
errors=errors,
errors={CONF_BASE: error},
)
data: dict = {
@@ -152,15 +137,21 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
user_input[CONF_HOST] = reconfigure_entry.data[CONF_HOST]
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}
self._async_abort_entries_match(
{
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
}
)
if errors is None:
await self.hass.config_entries.async_unload(reconfigure_entry.entry_id)
if (error := await validate_connection(user_input)) 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.",
"already_configured": "PCHK connection using the same ip address/port is already configured."
"connection_refused": "Unable to connect to PCHK. Check IP and port."
},
"abort": {
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"already_configured": "PCHK connection using the same ip address/port is already configured."
}
},
"issues": {
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==9.2.1"]
"requirements": ["ical==9.2.2"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==9.2.1"]
"requirements": ["ical==9.2.2"]
}
@@ -23,6 +23,8 @@ from .const import MieleAppliance
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
+2
View File
@@ -17,6 +17,8 @@ from .const import DOMAIN, PROCESS_ACTION, MieleActions, MieleAppliance
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +26,8 @@ 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,6 +27,8 @@ 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,6 +32,9 @@
"core_target_temperature": {
"default": "mdi:thermometer-probe"
},
"target_temperature": {
"default": "mdi:thermometer-check"
},
"drying_step": {
"default": "mdi:water-outline"
},
+2
View File
@@ -23,6 +23,8 @@ 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.1"],
"requirements": ["pymiele==0.5.2"],
"single_config_entry": true,
"zeroconf": ["_mieleathome._tcp.local."]
}
@@ -32,18 +32,23 @@ rules:
Handled by a setting in manifest.json as there is no account information in API
# Silver
action-exceptions: todo
action-exceptions:
status: done
comment: No custom actions are defined
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No configuration parameters
docs-installation-parameters: todo
docs-installation-parameters:
status: exempt
comment: |
Integration uses account linking via Nabu casa so no installation parameters are needed.
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates:
status: exempt
comment: Handled by coordinator
log-when-unavailable:
status: done
comment: Handled by DataUpdateCoordinator
parallel-updates: done
reauthentication-flow: done
test-coverage: todo
+26
View File
@@ -39,6 +39,8 @@ from .const import (
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
DISABLED_TEMPERATURE = -32768
@@ -382,6 +384,7 @@ 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",
@@ -398,6 +401,29 @@ 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,6 +876,9 @@
"core_temperature": {
"name": "Core temperature"
},
"target_temperature": {
"name": "Target temperature"
},
"core_target_temperature": {
"name": "Core target temperature"
}
+2
View File
@@ -28,6 +28,8 @@ from .const import (
from .coordinator import MieleConfigEntry
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
+2
View File
@@ -24,6 +24,8 @@ 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.",
"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.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
@@ -32,6 +32,8 @@ 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."""
@@ -153,7 +155,10 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="api_method",
data_schema=data_schema,
errors=errors,
description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name},
description_placeholders={
PLACEHOLDER_DEVICE_TYPE: device_type.name,
"auth_token_url": AUTH_TOKEN_URL,
},
)
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](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",
"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",
"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.1"]
"requirements": ["ical==9.2.2"]
}
+3 -30
View File
@@ -21,26 +21,19 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from .bridge import (
SamsungTVBridge,
async_get_device_info,
mac_from_device_info,
model_requires_encryption,
)
from .bridge import SamsungTVBridge, 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,
)
@@ -180,30 +173,10 @@ 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]
port: int | None = entry.data.get(CONF_PORT)
method: str | None = entry.data.get(CONF_METHOD)
method: str = entry.data[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,7 +56,6 @@ 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,
@@ -110,9 +109,11 @@ 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
@@ -125,13 +126,11 @@ 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,
@@ -145,7 +144,6 @@ 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
@@ -165,7 +163,6 @@ 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)
@@ -252,38 +249,44 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
self._mac = mac
return True
async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> None:
async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> bool:
try:
self._host = await self.hass.async_add_executor_job(
socket.gethostbyname, user_input[CONF_HOST]
)
except socket.gaierror as err:
raise AbortFlow(RESULT_UNKNOWN_HOST) from err
LOGGER.debug("Failed to get IP for %s: %s", user_input[CONF_HOST], err)
return False
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:
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({})
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"}
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input),
errors=errors,
)
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()
@@ -305,7 +308,6 @@ 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] = {}
@@ -420,7 +422,6 @@ 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
@@ -518,7 +519,6 @@ 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,6 +43,7 @@
},
"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": {
@@ -52,7 +53,6 @@
"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%]"
}
},
+19 -2
View File
@@ -33,7 +33,11 @@ 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_NETWORK_MAC, format_mac
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
CONNECTION_NETWORK_MAC,
format_mac,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .bluetooth import async_connect_scanner
@@ -160,6 +164,11 @@ 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
@@ -167,7 +176,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={(CONNECTION_NETWORK_MAC, self.mac)},
connections=self.connections,
identifiers={(DOMAIN, self.mac)},
manufacturer="Shelly",
model=get_shelly_model_name(self.model, self.sleep_period, self.device),
@@ -523,6 +532,14 @@ 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
@@ -0,0 +1,136 @@
"""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,7 +32,8 @@
"@RenierM26",
"@murtas",
"@Eloston",
"@dsypniewski"
"@dsypniewski",
"@zerzhang"
],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
@@ -95,13 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
energysites: list[TeslemetryEnergyData] = []
# Create the stream
stream = TeslemetryStream(
session,
access_token,
server=f"{region.lower()}.teslemetry.com",
parse_timestamp=True,
manual=True,
)
stream: TeslemetryStream | None = None
for product in products:
if (
@@ -123,6 +117,16 @@ 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},
@@ -240,7 +244,8 @@ 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)
entry.async_create_background_task(hass, stream.listen(), "Teslemetry Stream")
if stream:
entry.async_create_background_task(hass, stream.listen(), "Teslemetry Stream")
return True
+14 -2
View File
@@ -469,6 +469,7 @@ class ResultStream:
use_file_cache: bool
language: str
options: dict
supports_streaming_input: bool
_manager: SpeechManager
@@ -484,7 +485,10 @@ class ResultStream:
@callback
def async_set_message(self, message: str) -> None:
"""Set message to be generated."""
"""Set message to be generated.
This method will leverage a disk cache to speed up generation.
"""
self._result_cache.set_result(
self._manager.async_cache_message_in_memory(
engine=self.engine,
@@ -497,7 +501,10 @@ class ResultStream:
@callback
def async_set_message_stream(self, message_stream: AsyncGenerator[str]) -> None:
"""Set a stream that will generate the message."""
"""Set a stream that will generate the message.
This method can result in faster first byte when generating long responses.
"""
self._result_cache.set_result(
self._manager.async_cache_message_stream_in_memory(
engine=self.engine,
@@ -726,6 +733,10 @@ 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
@@ -741,6 +752,7 @@ 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,6 +89,13 @@ 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 Velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.",
"description": "Syncs the clock of the Velbus modules 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 page(s) of the module is configured to display the 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.",
"fields": {
"interface": {
"name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]",
+19 -12
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.get(CONF_API_KEY), travel_time.get(CONF_ID)
name, config[CONF_API_KEY], travel_time.get(CONF_ID)
)
)
@@ -82,20 +82,20 @@ class WashingtonStateTransportSensor(SensorEntity):
_attr_icon = ICON
def __init__(self, name, access_code):
def __init__(self, name: str, access_code: str) -> None:
"""Initialize the sensor."""
self._data = {}
self._data: dict[str, str | int | None] = {}
self._access_code = access_code
self._name = name
self._state = None
self._state: int | None = None
@property
def name(self):
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def native_value(self):
def native_value(self) -> int | None:
"""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, access_code, travel_time_id):
def __init__(self, name: str, access_code: str, travel_time_id: str) -> None:
"""Construct a travel time sensor."""
self._travel_time_id = travel_time_id
WashingtonStateTransportSensor.__init__(self, name, access_code)
@@ -123,13 +123,17 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
_LOGGER.warning("Invalid response from WSDOT API")
else:
self._data = response.json()
self._state = self._data.get(ATTR_CURRENT_TIME)
_state = self._data.get(ATTR_CURRENT_TIME)
if not isinstance(_state, int):
self._state = None
else:
self._state = _state
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return other details about the sensor state."""
if self._data is not None:
attrs = {}
attrs: dict[str, str | int | None | datetime] = {}
for key in (
ATTR_AVG_TIME,
ATTR_NAME,
@@ -144,12 +148,15 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
return None
def _parse_wsdot_timestamp(timestamp):
def _parse_wsdot_timestamp(timestamp: Any) -> datetime | None:
"""Convert WSDOT timestamp to datetime."""
if not timestamp:
if not isinstance(timestamp, str):
return None
# ex: Date(1485040200000-0800)
milliseconds, tzone = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp).groups()
timestamp_parts = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp)
if timestamp_parts is None:
return None
milliseconds, tzone = timestamp_parts.groups()
return datetime.fromtimestamp(
int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone)))
)
@@ -278,6 +278,39 @@ 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,6 +71,7 @@ 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
@@ -88,13 +89,16 @@ 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,
)
@@ -2865,6 +2869,25 @@ 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,14 +9,13 @@ 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, get_server_version
from zwave_js_server.version import VersionInfo
from homeassistant.components import usb
from homeassistant.components.hassio import (
@@ -36,7 +35,6 @@ 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
@@ -69,6 +67,7 @@ from .const import (
DOMAIN,
RESTORE_NVM_DRIVER_READY_TIMEOUT,
)
from .helpers import CannotConnect, async_get_version_info
_LOGGER = logging.getLogger(__name__)
@@ -79,7 +78,6 @@ 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",
@@ -130,22 +128,6 @@ 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()
@@ -1357,10 +1339,6 @@ 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,11 +2,13 @@
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 (
@@ -25,6 +27,7 @@ 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
@@ -38,6 +41,7 @@ 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
@@ -54,6 +58,8 @@ from .const import (
LOGGER,
)
SERVER_VERSION_TIMEOUT = 10
@dataclass
class ZwaveValueID:
@@ -568,3 +574,23 @@ 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,6 +57,47 @@ 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:
@@ -65,4 +106,7 @@ 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,6 +273,17 @@
"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_data = await frontend_store.async_user_store(
owner_store = await frontend_store.async_user_store(
self.hass, owner.id
)
if (
"language" in owner_data
and "language" in owner_data["language"]
"language" in owner_store.data
and "language" in owner_store.data["language"]
):
with suppress(vol.InInvalid):
data["language"] = cv.language(
owner_data["language"]["language"]
owner_store.data["language"]["language"]
)
# pylint: disable-next=broad-except
except Exception:
+38 -1
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 = 8
STORAGE_VERSION_MINOR = 9
class _AreaStoreData(TypedDict):
@@ -52,6 +52,7 @@ 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
@@ -82,6 +83,7 @@ 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)
@@ -98,6 +100,7 @@ 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,
@@ -157,6 +160,11 @@ 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]
@@ -278,6 +286,7 @@ 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:
@@ -293,6 +302,9 @@ 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)
@@ -303,6 +315,7 @@ 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,
@@ -345,6 +358,7 @@ 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,
@@ -357,6 +371,7 @@ 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,
@@ -381,6 +396,7 @@ 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,
@@ -396,6 +412,7 @@ 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),
)
@@ -405,6 +422,9 @@ 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"])
@@ -440,6 +460,7 @@ 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"],
@@ -462,6 +483,7 @@ 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,
@@ -569,3 +591,18 @@ 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")
+4 -105
View File
@@ -42,8 +42,6 @@ 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
@@ -60,7 +58,6 @@ 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,
@@ -85,7 +82,6 @@ _PLATFORM_ALIASES = {
"numeric_state": None,
"or": None,
"state": None,
"sun": None,
"template": None,
"time": None,
"trigger": None,
@@ -655,105 +651,6 @@ 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:
@@ -1054,8 +951,10 @@ async def async_validate_condition_config(
return config
platform = await _async_get_condition_platform(hass, 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 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 None and condition in ("numeric_state", "state"):
validator = cast(
Callable[[HomeAssistant, ConfigType], ConfigType],
+40 -30
View File
@@ -1084,10 +1084,13 @@ def renamed(
return validator
type ValueSchemas = dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]]
def key_value_schemas(
key: str,
value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]],
default_schema: VolSchemaType | None = None,
value_schemas: ValueSchemas,
default_schema: VolSchemaType | Callable[[Any], dict[str, Any]] | None = None,
default_description: str | None = None,
) -> Callable[[Any], dict[Hashable, Any]]:
"""Create a validator that validates based on a value for specific key.
@@ -1735,25 +1738,41 @@ 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,
{
"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,
},
BUILT_IN_CONDITIONS,
_base_condition_validator,
),
),
dynamic_template_condition,
@@ -1780,20 +1799,11 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
expand_condition_shorthand,
key_value_schemas(
CONF_CONDITION,
{
"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,
BUILT_IN_CONDITIONS,
vol.Any(
dynamic_template_condition_action,
_base_condition_validator,
),
"a list of conditions or a valid template",
),
)
@@ -1852,7 +1862,7 @@ def _base_trigger_list_flatten(triggers: list[Any]) -> list[Any]:
return flatlist
# This is first round of validation, we don't want to process the config here already,
# This is first round of validation, we don't want to mutate 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)
+5 -43
View File
@@ -29,27 +29,20 @@ 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,
device_registry as dr,
discovery,
entity,
entity_registry as er,
service,
)
from .entity_platform import EntityPlatform, async_calculate_suggested_object_id
from . import config_validation as cv, discovery, entity, service
from .entity_platform import EntityPlatform
from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
DATA_INSTANCES: HassKey[dict[str, EntityComponent]] = HassKey("entity_components")
DATA_INSTANCES = "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:
@@ -67,37 +60,6 @@ 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.
@@ -133,7 +95,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 # type: ignore[assignment]
hass.data.setdefault(DATA_INSTANCES, {})[domain] = self
@property
def entities(self) -> Iterable[_EntityT]:
+25 -34
View File
@@ -764,7 +764,7 @@ class EntityPlatform:
already_exists = True
return (already_exists, restored)
async def _async_add_entity(
async def _async_add_entity( # noqa: C901
self,
entity: Entity,
update_before_add: bool,
@@ -843,18 +843,31 @@ class EntityPlatform:
else:
device = None
# 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 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
if self.entity_namespace is not None and suggested_object_id is not None:
suggested_object_id = f"{self.entity_namespace} {suggested_object_id}"
if self.entity_namespace 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:
@@ -868,7 +881,6 @@ 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,
@@ -1112,27 +1124,6 @@ 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
)
+2 -17
View File
@@ -79,7 +79,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 17
STORAGE_VERSION_MINOR = 16
STORAGE_KEY = "core.entity_registry"
CLEANUP_INTERVAL = 3600 * 24
@@ -195,11 +195,9 @@ 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()
@@ -339,7 +337,6 @@ 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,
@@ -362,7 +359,6 @@ 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,
@@ -552,12 +548,6 @@ 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
@@ -846,7 +836,6 @@ 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,
@@ -919,7 +908,7 @@ class EntityRegistry(BaseRegistry):
entity_id = self.async_generate_entity_id(
domain,
suggested_object_id or calculated_object_id or f"{platform}_{unique_id}",
suggested_object_id or f"{platform}_{unique_id}",
)
if (
@@ -953,8 +942,6 @@ 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,
@@ -1363,7 +1350,6 @@ 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"],
@@ -1392,7 +1378,6 @@ 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.1.1
aiodhcpwatcher==1.2.0
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.0
aiocomelit==0.12.1
# homeassistant.components.dhcp
aiodhcpwatcher==1.1.1
aiodhcpwatcher==1.2.0
# 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.0
automower-ble==0.2.1
# 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.13
debugpy==1.8.14
# 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.0
gcal-sync==7.0.1
# 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.1
ical==9.2.2
# 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.1
pymiele==0.5.2
# 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.6.0
xknx==3.8.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.25.1
pipdeptree==2.26.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.0
aiocomelit==0.12.1
# homeassistant.components.dhcp
aiodhcpwatcher==1.1.1
aiodhcpwatcher==1.2.0
# 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.0
automower-ble==0.2.1
# 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.13
debugpy==1.8.14
# 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.0
gcal-sync==7.0.1
# 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.1
ical==9.2.2
# 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.1
pymiele==0.5.2
# 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.6.0
xknx==3.8.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.25.1 tqdm==4.67.1 ruff==0.11.0 \
stdlib-list==0.10.0 pipdeptree==2.26.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,7 +651,6 @@ 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)
@@ -670,7 +669,6 @@ 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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Timer running',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Reset timer',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'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',
@@ -53,7 +51,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Start/stop timer',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -77,7 +74,6 @@
'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',
@@ -102,7 +98,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Tare',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -126,7 +121,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -30,7 +29,6 @@
'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',
@@ -58,7 +56,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Volume flow rate',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -87,7 +84,6 @@
'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',
@@ -115,7 +111,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'LUNAR-DDEEFF Weight',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -141,7 +136,6 @@
'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,7 +245,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -269,7 +268,6 @@
'original_name': None,
'platform': 'accuweather',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <WeatherEntityFeature: 1>,
'translation_key': None,
'unique_id': '0123456',
@@ -4,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Calibrate CO2 sensor',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'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',
@@ -53,7 +51,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Test LED bar',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -77,7 +74,6 @@
'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',
@@ -102,7 +98,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Calibrate CO2 sensor',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -126,7 +121,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display brightness',
'capabilities': dict({
'max': 100,
'min': 0,
@@ -33,7 +32,6 @@
'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',
@@ -63,7 +61,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar brightness',
'capabilities': dict({
'max': 100,
'min': 0,
@@ -92,7 +89,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient CO2 automatic baseline duration',
'capabilities': dict({
'options': list([
'1',
@@ -37,7 +36,6 @@
'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',
@@ -70,7 +68,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Configuration source',
'capabilities': dict({
'options': list([
'cloud',
@@ -99,7 +96,6 @@
'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',
@@ -128,7 +124,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display PM standard',
'capabilities': dict({
'options': list([
'ugm3',
@@ -157,7 +152,6 @@
'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',
@@ -186,7 +180,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display temperature unit',
'capabilities': dict({
'options': list([
'c',
@@ -215,7 +208,6 @@
'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',
@@ -244,7 +236,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar mode',
'capabilities': dict({
'options': list([
'off',
@@ -274,7 +265,6 @@
'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',
@@ -304,7 +294,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -336,7 +325,6 @@
'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',
@@ -368,7 +356,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -400,7 +387,6 @@
'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',
@@ -432,7 +418,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient CO2 automatic baseline duration',
'capabilities': dict({
'options': list([
'1',
@@ -465,7 +450,6 @@
'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',
@@ -498,7 +482,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Configuration source',
'capabilities': dict({
'options': list([
'cloud',
@@ -527,7 +510,6 @@
'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',
@@ -556,7 +538,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -588,7 +569,6 @@
'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',
@@ -620,7 +600,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': dict({
'options': list([
'12',
@@ -652,7 +631,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Carbon dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -30,7 +29,6 @@
'original_name': 'Carbon dioxide',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-co2',
@@ -58,7 +56,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Carbon dioxide automatic baseline calibration',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -82,7 +79,6 @@
'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',
@@ -109,7 +105,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display brightness',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -133,7 +128,6 @@
'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',
@@ -159,7 +153,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display PM standard',
'capabilities': dict({
'options': list([
'ugm3',
@@ -188,7 +181,6 @@
'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',
@@ -218,7 +210,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Display temperature unit',
'capabilities': dict({
'options': list([
'c',
@@ -247,7 +238,6 @@
'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',
@@ -277,7 +267,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -303,7 +292,6 @@
'original_name': 'Humidity',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-humidity',
@@ -331,7 +319,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar brightness',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -355,7 +342,6 @@
'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',
@@ -381,7 +367,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient LED bar mode',
'capabilities': dict({
'options': list([
'off',
@@ -411,7 +396,6 @@
'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',
@@ -442,7 +426,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -468,7 +451,6 @@
'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',
@@ -494,7 +476,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -518,7 +499,6 @@
'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',
@@ -545,7 +525,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM0.3',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -571,7 +550,6 @@
'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',
@@ -598,7 +576,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM1',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -624,7 +601,6 @@
'original_name': 'PM1',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-pm01',
@@ -652,7 +628,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM10',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -678,7 +653,6 @@
'original_name': 'PM10',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-pm10',
@@ -706,7 +680,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -732,7 +705,6 @@
'original_name': 'PM2.5',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-pm02',
@@ -760,7 +732,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw NOx',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -786,7 +757,6 @@
'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',
@@ -813,7 +783,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -839,7 +808,6 @@
'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',
@@ -867,7 +835,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw VOC',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -893,7 +860,6 @@
'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',
@@ -920,7 +886,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -946,7 +911,6 @@
'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',
@@ -974,7 +938,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1000,7 +963,6 @@
'original_name': 'Temperature',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-temperature',
@@ -1028,7 +990,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1054,7 +1015,6 @@
'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',
@@ -1080,7 +1040,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1104,7 +1063,6 @@
'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',
@@ -1131,7 +1089,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Carbon dioxide automatic baseline calibration',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1155,7 +1112,6 @@
'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',
@@ -1182,7 +1138,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1208,7 +1163,6 @@
'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',
@@ -1234,7 +1188,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient NOx index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1258,7 +1211,6 @@
'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',
@@ -1285,7 +1237,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw NOx',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1311,7 +1262,6 @@
'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',
@@ -1338,7 +1288,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Raw VOC',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1364,7 +1313,6 @@
'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',
@@ -1391,7 +1339,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1417,7 +1364,6 @@
'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',
@@ -1445,7 +1391,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1471,7 +1416,6 @@
'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',
@@ -1497,7 +1441,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient VOC index learning offset',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1521,7 +1464,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Post data to Airgradient',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airgradient Firmware',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'original_name': 'Firmware',
'platform': 'airgradient',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '84fce612f5b8-update',
@@ -4,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Carbon monoxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -33,7 +32,6 @@
'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',
@@ -63,7 +61,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Common air quality index',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -90,7 +87,6 @@
'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',
@@ -120,7 +116,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -149,7 +144,6 @@
'original_name': 'Humidity',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-humidity',
@@ -178,7 +172,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Nitrogen dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -207,7 +200,6 @@
'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',
@@ -238,7 +230,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Ozone',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -267,7 +258,6 @@
'original_name': 'Ozone',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-o3',
@@ -298,7 +288,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home PM1',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -327,7 +316,6 @@
'original_name': 'PM1',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm1',
@@ -356,7 +344,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home PM10',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -385,7 +372,6 @@
'original_name': 'PM10',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pm10',
@@ -416,7 +402,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -445,7 +430,6 @@
'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',
@@ -476,7 +460,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -505,7 +488,6 @@
'original_name': 'Pressure',
'platform': 'airly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123-456-pressure',
@@ -534,7 +516,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Sulphur dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -563,7 +544,6 @@
'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',
@@ -594,7 +574,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Home Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -623,7 +602,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Zone 1 Damper',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'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',
@@ -56,7 +54,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Zone 2 Damper',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -80,7 +77,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone 2:1 Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -30,7 +29,6 @@
'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',
@@ -58,7 +56,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone 2:1 Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -84,7 +81,6 @@
'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',
@@ -112,7 +108,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone DHW Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -138,7 +133,6 @@
'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',
@@ -166,7 +160,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Airzone WebServer RSSI',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -192,7 +185,6 @@
'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',
@@ -220,7 +212,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Aux Heat Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -246,7 +237,6 @@
'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',
@@ -274,7 +264,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -300,7 +289,6 @@
'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',
@@ -328,7 +316,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -354,7 +341,6 @@
'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',
@@ -382,7 +368,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -408,7 +393,6 @@
'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',
@@ -435,7 +419,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Despacho Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -461,7 +444,6 @@
'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',
@@ -489,7 +471,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'DKN Plus Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -515,7 +496,6 @@
'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',
@@ -543,7 +523,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -569,7 +548,6 @@
'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',
@@ -597,7 +575,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -623,7 +600,6 @@
'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',
@@ -651,7 +627,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -677,7 +652,6 @@
'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',
@@ -704,7 +678,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #1 Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -730,7 +703,6 @@
'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',
@@ -758,7 +730,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -784,7 +755,6 @@
'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',
@@ -812,7 +782,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -838,7 +807,6 @@
'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',
@@ -866,7 +834,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -892,7 +859,6 @@
'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',
@@ -919,7 +885,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm #2 Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -945,7 +910,6 @@
'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',
@@ -973,7 +937,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Battery',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -999,7 +962,6 @@
'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',
@@ -1027,7 +989,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1053,7 +1014,6 @@
'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',
@@ -1081,7 +1041,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Signal strength',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1107,7 +1066,6 @@
'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',
@@ -1134,7 +1092,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Dorm Ppal Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1160,7 +1117,6 @@
'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',
@@ -1188,7 +1144,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Salon Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1214,7 +1169,6 @@
'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',
@@ -1242,7 +1196,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Salon Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1268,7 +1221,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Absolute pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -36,7 +35,6 @@
'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',
@@ -66,7 +64,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Daily rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -98,7 +95,6 @@
'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',
@@ -128,7 +124,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Dew point',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -157,7 +152,6 @@
'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',
@@ -187,7 +181,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Feels like',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -216,7 +209,6 @@
'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',
@@ -246,7 +238,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Hourly rain',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -278,7 +269,6 @@
'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',
@@ -308,7 +298,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -337,7 +326,6 @@
'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',
@@ -367,7 +355,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Irradiance',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -396,7 +383,6 @@
'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',
@@ -426,7 +412,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Last rain',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -450,7 +435,6 @@
'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',
@@ -478,7 +462,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Max daily gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -510,7 +493,6 @@
'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',
@@ -540,7 +522,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Monthly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -572,7 +553,6 @@
'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',
@@ -602,7 +582,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Relative pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -634,7 +613,6 @@
'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',
@@ -664,7 +642,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -693,7 +670,6 @@
'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',
@@ -723,7 +699,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A UV index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -752,7 +727,6 @@
'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',
@@ -781,7 +755,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Weekly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -813,7 +786,6 @@
'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',
@@ -843,7 +815,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Wind direction',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
@@ -872,7 +843,6 @@
'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',
@@ -902,7 +872,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Wind gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -934,7 +903,6 @@
'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',
@@ -964,7 +932,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station A Wind speed',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -996,7 +963,6 @@
'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',
@@ -1026,7 +992,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Absolute pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1058,7 +1023,6 @@
'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',
@@ -1088,7 +1052,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Daily rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -1120,7 +1083,6 @@
'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',
@@ -1150,7 +1112,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Dew point',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1179,7 +1140,6 @@
'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',
@@ -1209,7 +1169,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Feels like',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1238,7 +1197,6 @@
'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',
@@ -1268,7 +1226,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Hourly rain',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1300,7 +1257,6 @@
'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',
@@ -1330,7 +1286,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1359,7 +1314,6 @@
'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',
@@ -1389,7 +1343,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Irradiance',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1418,7 +1371,6 @@
'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',
@@ -1448,7 +1400,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Last rain',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1472,7 +1423,6 @@
'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',
@@ -1500,7 +1450,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Max daily gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1532,7 +1481,6 @@
'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',
@@ -1562,7 +1510,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Monthly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -1594,7 +1541,6 @@
'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',
@@ -1624,7 +1570,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Relative pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1656,7 +1601,6 @@
'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',
@@ -1686,7 +1630,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1715,7 +1658,6 @@
'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',
@@ -1745,7 +1687,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C UV index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1774,7 +1715,6 @@
'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',
@@ -1803,7 +1743,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Weekly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -1835,7 +1774,6 @@
'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',
@@ -1865,7 +1803,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Wind direction',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
@@ -1894,7 +1831,6 @@
'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',
@@ -1924,7 +1860,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Wind gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -1956,7 +1891,6 @@
'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',
@@ -1986,7 +1920,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station C Wind speed',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2018,7 +1951,6 @@
'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',
@@ -2048,7 +1980,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Absolute pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2080,7 +2011,6 @@
'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',
@@ -2109,7 +2039,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Daily rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -2141,7 +2070,6 @@
'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',
@@ -2170,7 +2098,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Dew point',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2199,7 +2126,6 @@
'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',
@@ -2228,7 +2154,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Feels like',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2257,7 +2182,6 @@
'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',
@@ -2286,7 +2210,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Hourly rain',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2318,7 +2241,6 @@
'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',
@@ -2347,7 +2269,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2376,7 +2297,6 @@
'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',
@@ -2405,7 +2325,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Irradiance',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2434,7 +2353,6 @@
'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',
@@ -2463,7 +2381,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Max daily gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2495,7 +2412,6 @@
'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',
@@ -2524,7 +2440,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Monthly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -2556,7 +2471,6 @@
'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',
@@ -2585,7 +2499,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Relative pressure',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2617,7 +2530,6 @@
'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',
@@ -2646,7 +2558,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2675,7 +2586,6 @@
'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',
@@ -2704,7 +2614,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D UV index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2733,7 +2642,6 @@
'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',
@@ -2761,7 +2669,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Weekly rain',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -2793,7 +2700,6 @@
'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',
@@ -2822,7 +2728,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Wind direction',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
}),
@@ -2851,7 +2756,6 @@
'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',
@@ -2880,7 +2784,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Wind gust',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2912,7 +2815,6 @@
'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',
@@ -2941,7 +2843,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Station D Wind speed',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -2973,7 +2874,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics core_samba',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -30,7 +29,6 @@
'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',
@@ -57,7 +55,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics hacs (custom)',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -83,7 +80,6 @@
'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',
@@ -110,7 +106,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics myq',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -136,7 +131,6 @@
'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',
@@ -163,7 +157,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics spotify',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -189,7 +182,6 @@
'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',
@@ -216,7 +208,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics Total active installations',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -242,7 +233,6 @@
'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',
@@ -269,7 +259,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics Total reported integrations',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -295,7 +284,6 @@
'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',
@@ -322,7 +310,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Homeassistant Analytics YouTube',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
@@ -348,7 +335,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater Energy usage',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -33,7 +32,6 @@
'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',
@@ -61,7 +59,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater Hot water availability',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -85,7 +82,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater',
'capabilities': dict({
'max_temp': 130,
'min_temp': 95,
@@ -31,7 +30,6 @@
'original_name': None,
'platform': 'aosmith',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <WaterHeaterEntityFeature: 5>,
'translation_key': None,
'unique_id': 'junctionId',
@@ -64,7 +62,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'My water heater',
'capabilities': dict({
'max_temp': 130,
'min_temp': 95,
@@ -96,7 +93,6 @@
'original_name': None,
'platform': 'aosmith',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <WaterHeaterEntityFeature: 7>,
'translation_key': None,
'unique_id': 'junctionId',
@@ -4,7 +4,6 @@
'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>,
@@ -28,7 +27,6 @@
'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',
@@ -54,7 +52,6 @@
'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>,
@@ -78,7 +75,6 @@
'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',
@@ -104,7 +100,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Off-grid status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -128,7 +123,6 @@
'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',
@@ -154,7 +148,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Output fault status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -178,7 +171,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Max output',
'capabilities': dict({
'max': 1000,
'min': 0,
@@ -33,7 +32,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Lifetime production of P1',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -30,7 +29,6 @@
'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',
@@ -58,7 +56,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Lifetime production of P2',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -84,7 +81,6 @@
'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',
@@ -112,7 +108,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Power of P1',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -138,7 +133,6 @@
'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',
@@ -166,7 +160,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Power of P2',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -192,7 +185,6 @@
'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',
@@ -220,7 +212,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Production of today',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -246,7 +237,6 @@
'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',
@@ -274,7 +264,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Production of today from P1',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -300,7 +289,6 @@
'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',
@@ -328,7 +316,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Production of today from P2',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -354,7 +341,6 @@
'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',
@@ -382,7 +368,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Total lifetime production',
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
@@ -408,7 +393,6 @@
'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',
@@ -436,7 +420,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Total power',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -462,7 +445,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Mock Title Inverter status',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Battery',
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -28,7 +27,6 @@
'original_name': 'Battery',
'platform': 'aquacell',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'DSN-battery',
@@ -55,7 +53,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Salt left side percentage',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -81,7 +78,6 @@
'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',
@@ -108,7 +104,6 @@
'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>,
@@ -132,7 +127,6 @@
'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',
@@ -159,7 +153,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Salt right side percentage',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -185,7 +178,6 @@
'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',
@@ -212,7 +204,6 @@
'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>,
@@ -236,7 +227,6 @@
'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',
@@ -263,7 +253,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'AquaCell name Wi-Fi strength',
'capabilities': dict({
'options': list([
'high',
@@ -293,7 +282,6 @@
'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,7 +4,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Air quality index',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -30,7 +29,6 @@
'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',
@@ -42,7 +40,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Carbon dioxide',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -68,7 +65,6 @@
'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',
@@ -80,7 +76,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Humidity',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -106,7 +101,6 @@
'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',
@@ -118,7 +112,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor PM10',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -144,7 +137,6 @@
'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',
@@ -156,7 +148,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor PM2.5',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -182,7 +173,6 @@
'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',
@@ -194,7 +184,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Temperature',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -220,7 +209,6 @@
'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',
@@ -232,7 +220,6 @@
'aliases': set({
}),
'area_id': None,
'calculated_object_id': 'Test Sensor Total volatile organic compounds',
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
@@ -258,7 +245,6 @@
'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',
+21 -2
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 MockTTSProvider
from tests.components.tts.common import MockTTSEntity, MockTTSProvider
_TRANSCRIPT = "test transcript"
@@ -68,6 +68,15 @@ 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."""
@@ -198,6 +207,7 @@ 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,
@@ -209,7 +219,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.WAKE_WORD]
config_entry, [Platform.STT, Platform.TTS, Platform.WAKE_WORD]
)
return True
@@ -230,6 +240,14 @@ 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,
@@ -253,6 +271,7 @@ 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': 'test',
'language': 'en-US',
'engine': 'tts.test',
'language': 'en_US',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
'voice': None,
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
}),
dict({
'data': dict({
'tts_output': dict({
'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",
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -395,17 +395,17 @@
}),
dict({
'data': dict({
'engine': 'test',
'language': 'en-US',
'engine': 'tts.test',
'language': 'en_US',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
'voice': None,
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
}),
dict({
'data': dict({
'tts_output': dict({
'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",
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
@@ -1,4 +1,158 @@
# 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': 'test',
'language': 'en-US',
'engine': 'tts.test',
'language': 'en_US',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
'voice': None,
})
# ---
# name: test_audio_pipeline.6
dict({
'tts_output': dict({
'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",
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%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': 'test',
'language': 'en-US',
'engine': 'tts.test',
'language': 'en_US',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
'voice': None,
})
# ---
# name: test_audio_pipeline_debug.6
dict({
'tts_output': dict({
'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",
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%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': 'test',
'language': 'en-US',
'engine': 'tts.test',
'language': 'en_US',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
'voice': None,
})
# ---
# name: test_audio_pipeline_with_enhancements.6
dict({
'tts_output': dict({
'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",
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%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': 'test',
'language': 'en-US',
'engine': 'tts.test',
'language': 'en_US',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
'voice': None,
})
# ---
# name: test_audio_pipeline_with_wake_word_no_timeout.8
dict({
'tts_output': dict({
'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",
'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D",
'mime_type': 'audio/mpeg',
'token': 'test_token.mp3',
'url': '/api/tts_proxy/test_token.mp3',
+162 -11
View File
@@ -40,6 +40,7 @@ from . import MANY_LANGUAGES, process_events
from .conftest import (
MockSTTProvider,
MockSTTProviderEntity,
MockTTSEntity,
MockTTSProvider,
MockWakeWordEntity,
make_10ms_chunk,
@@ -62,6 +63,12 @@ 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."""
@@ -283,6 +290,7 @@ 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", {})
@@ -430,6 +438,7 @@ 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,
@@ -474,6 +483,7 @@ 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:
@@ -504,6 +514,7 @@ 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:
@@ -825,7 +836,7 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None:
async def test_tts_audio_output(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_tts_provider: MockTTSProvider,
mock_tts_entity: MockTTSProvider,
init_components,
pipeline_data: assist_pipeline.pipeline.PipelineData,
mock_chat_session: chat_session.ChatSession,
@@ -869,7 +880,7 @@ async def test_tts_audio_output(
== 1
)
with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio:
with patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio:
await pipeline_input.execute()
for event in events:
@@ -881,14 +892,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_provider.supported_options)
extra_options = set(options).difference(mock_tts_entity.supported_options)
assert len(extra_options) == 0, extra_options
async def test_tts_wav_preferred_format(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_tts_provider: MockTTSProvider,
mock_tts_entity: MockTTSEntity,
init_components,
mock_chat_session: chat_session.ChatSession,
pipeline_data: assist_pipeline.pipeline.PipelineData,
@@ -920,7 +931,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_provider.supported_options or [])
supported_options = list(mock_tts_entity.supported_options or [])
supported_options.extend(
[
tts.ATTR_PREFERRED_FORMAT,
@@ -931,8 +942,8 @@ async def test_tts_wav_preferred_format(
)
with (
patch.object(mock_tts_provider, "_supported_options", supported_options),
patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio,
patch.object(mock_tts_entity, "_supported_options", supported_options),
patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio,
):
await pipeline_input.execute()
@@ -955,7 +966,7 @@ async def test_tts_wav_preferred_format(
async def test_tts_dict_preferred_format(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_tts_provider: MockTTSProvider,
mock_tts_entity: MockTTSEntity,
init_components,
mock_chat_session: chat_session.ChatSession,
pipeline_data: assist_pipeline.pipeline.PipelineData,
@@ -992,7 +1003,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_provider.supported_options or [])
supported_options = list(mock_tts_entity.supported_options or [])
supported_options.extend(
[
tts.ATTR_PREFERRED_FORMAT,
@@ -1003,8 +1014,8 @@ async def test_tts_dict_preferred_format(
)
with (
patch.object(mock_tts_provider, "_supported_options", supported_options),
patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio,
patch.object(mock_tts_entity, "_supported_options", supported_options),
patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio,
):
await pipeline_input.execute()
@@ -1545,3 +1556,143 @@ 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": "test",
"tts_language": "en-US",
"tts_voice": "james_earl_jones",
"tts_engine": "tts.test",
"tts_language": "en_US",
"tts_voice": None,
"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": "test",
"tts_language": "en-US",
"tts_voice": "james_earl_jones",
"tts_engine": "tts.test",
"tts_language": "en_US",
"tts_voice": None,
"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": "test",
"tts_language": "en-US",
"tts_voice": "james_earl_jones",
"tts_engine": "tts.test",
"tts_language": "en_US",
"tts_voice": None,
"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