diff --git a/.coveragerc b/.coveragerc
index bcd4e349668..fa0bf2fbd4c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -359,7 +359,6 @@ omit =
homeassistant/components/escea/__init__.py
homeassistant/components/escea/climate.py
homeassistant/components/escea/discovery.py
- homeassistant/components/esphome/manager.py
homeassistant/components/etherscan/sensor.py
homeassistant/components/eufy/*
homeassistant/components/eufylife_ble/__init__.py
diff --git a/build.yaml b/build.yaml
index 824d580913d..d0baa4ac18e 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
- aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.01.0
- armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.01.0
- armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.01.0
- amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.01.0
- i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.01.0
+ aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.0
+ armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.0
+ armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.0
+ amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.0
+ i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py
index 2ce8adc30d6..6b0adcb52cf 100644
--- a/homeassistant/components/adax/climate.py
+++ b/homeassistant/components/adax/climate.py
@@ -74,6 +74,7 @@ class AdaxDevice(ClimateEntity):
)
_attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
"""Initialize the heater."""
diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py
index d4f3c05902c..870a001a10f 100644
--- a/homeassistant/components/advantage_air/climate.py
+++ b/homeassistant/components/advantage_air/climate.py
@@ -83,6 +83,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
_attr_max_temp = 32
_attr_min_temp = 16
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an AdvantageAir AC unit."""
@@ -202,11 +203,16 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
"""AdvantageAir MyTemp Zone control."""
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT_COOL]
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = PRECISION_WHOLE
_attr_max_temp = 32
_attr_min_temp = 16
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
"""Initialize an AdvantageAir Zone control."""
diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py
index b562e837ff4..4228fea50d7 100644
--- a/homeassistant/components/airthings_ble/config_flow.py
+++ b/homeassistant/components/airthings_ble/config_flow.py
@@ -23,6 +23,13 @@ from .const import DOMAIN, MFCT_ID
_LOGGER = logging.getLogger(__name__)
+SERVICE_UUIDS = [
+ "b42e1f6e-ade7-11e4-89d3-123b93f75cba",
+ "b42e4a8e-ade7-11e4-89d3-123b93f75cba",
+ "b42e1c08-ade7-11e4-89d3-123b93f75cba",
+ "b42e3882-ade7-11e4-89d3-123b93f75cba",
+]
+
@dataclasses.dataclass
class Discovery:
@@ -147,6 +154,9 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
if MFCT_ID not in discovery_info.manufacturer_data:
continue
+ if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
+ continue
+
try:
device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError:
diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py
index f5a0e1b109e..2b4cae18086 100644
--- a/homeassistant/components/airzone/climate.py
+++ b/homeassistant/components/airzone/climate.py
@@ -117,6 +117,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
_attr_name = None
_speeds: dict[int, str] = {}
_speeds_reverse: dict[str, int] = {}
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
@@ -129,7 +130,11 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
super().__init__(coordinator, entry, system_zone_id, zone_data)
self._attr_unique_id = f"{self._attr_unique_id}_{system_zone_id}"
- self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ self._attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
self._attr_target_temperature_step = API_TEMPERATURE_STEP
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
self.get_airzone_value(AZD_TEMP_UNIT)
diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py
index e076edc1f5b..73333d346c5 100644
--- a/homeassistant/components/airzone_cloud/climate.py
+++ b/homeassistant/components/airzone_cloud/climate.py
@@ -144,8 +144,13 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
"""Define an Airzone Cloud climate."""
_attr_name = None
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
@callback
def _handle_coordinator_update(self) -> None:
diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py
index fc192d8658f..58b2334260e 100644
--- a/homeassistant/components/ambiclimate/climate.py
+++ b/homeassistant/components/ambiclimate/climate.py
@@ -153,10 +153,15 @@ class AmbiclimateEntity(ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 1
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_has_entity_name = True
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, heater: AmbiclimateDevice, store: Store[dict[str, Any]]) -> None:
"""Initialize the thermostat."""
diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py
index 9b2729f141e..a5f119e3a2b 100644
--- a/homeassistant/components/atag/climate.py
+++ b/homeassistant/components/atag/climate.py
@@ -46,6 +46,7 @@ class AtagThermostat(AtagEntity, ClimateEntity):
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, atag_id):
"""Initialize an Atag climate device."""
diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py
index f97647fff0e..dd07e137e5e 100644
--- a/homeassistant/components/auth/__init__.py
+++ b/homeassistant/components/auth/__init__.py
@@ -578,6 +578,7 @@ def websocket_refresh_tokens(
connection.send_result(msg["id"], tokens)
+@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "auth/delete_refresh_token",
@@ -585,8 +586,7 @@ def websocket_refresh_tokens(
}
)
@websocket_api.ws_require_user()
-@websocket_api.async_response
-async def websocket_delete_refresh_token(
+def websocket_delete_refresh_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle a delete refresh token request."""
@@ -601,6 +601,7 @@ async def websocket_delete_refresh_token(
connection.send_result(msg["id"], {})
+@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "auth/delete_all_refresh_tokens",
@@ -609,8 +610,7 @@ async def websocket_delete_refresh_token(
}
)
@websocket_api.ws_require_user()
-@websocket_api.async_response
-async def websocket_delete_all_refresh_tokens(
+def websocket_delete_all_refresh_tokens(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle delete all refresh tokens request."""
diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py
index 531659e901f..907e8ff2356 100644
--- a/homeassistant/components/baf/climate.py
+++ b/homeassistant/components/baf/climate.py
@@ -33,10 +33,15 @@ async def async_setup_entry(
class BAFAutoComfort(BAFEntity, ClimateEntity):
"""BAF climate auto comfort."""
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY]
_attr_translation_key = "auto_comfort"
+ _enable_turn_on_off_backwards_compatibility = False
@callback
def _async_update_attrs(self) -> None:
diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py
index 0ca8b1a3acc..b9cce73de75 100644
--- a/homeassistant/components/balboa/climate.py
+++ b/homeassistant/components/balboa/climate.py
@@ -63,6 +63,7 @@ class BalboaClimateEntity(BalboaEntity, ClimateEntity):
)
_attr_translation_key = DOMAIN
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, client: SpaClient) -> None:
"""Initialize the climate entity."""
diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py
index e4ac8985ebd..1350f1f29a2 100644
--- a/homeassistant/components/blebox/climate.py
+++ b/homeassistant/components/blebox/climate.py
@@ -53,8 +53,13 @@ async def async_setup_entry(
class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity):
"""Representation of a BleBox climate feature (saunaBox)."""
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
@property
def hvac_modes(self):
diff --git a/homeassistant/components/broadlink/climate.py b/homeassistant/components/broadlink/climate.py
index 6937d6bb0da..dd37d270f9e 100644
--- a/homeassistant/components/broadlink/climate.py
+++ b/homeassistant/components/broadlink/climate.py
@@ -35,9 +35,14 @@ class BroadlinkThermostat(ClimateEntity, BroadlinkEntity):
_attr_has_entity_name = True
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO]
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_target_temperature_step = PRECISION_HALVES
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, device: BroadlinkDevice) -> None:
"""Initialize the climate entity."""
diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py
index 609d5ab6e83..511701cb538 100644
--- a/homeassistant/components/bsblan/climate.py
+++ b/homeassistant/components/bsblan/climate.py
@@ -73,12 +73,16 @@ class BSBLANClimate(
_attr_name = None
# Determine preset modes
_attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_preset_modes = PRESET_MODES
# Determine hvac modes
_attr_hvac_modes = HVAC_MODES
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py
index dcb321d5ebb..e41e43c3a3c 100644
--- a/homeassistant/components/camera/img_util.py
+++ b/homeassistant/components/camera/img_util.py
@@ -98,6 +98,6 @@ class TurboJPEGSingleton:
TurboJPEGSingleton.__instance = TurboJPEG()
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
- "Error loading libturbojpeg; Cameras may impact HomeKit performance"
+ "Error loading libturbojpeg; Camera snapshot performance will be sub-optimal"
)
TurboJPEGSingleton.__instance = False
diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py
index 2b2aca0b229..a678868ee18 100644
--- a/homeassistant/components/co2signal/config_flow.py
+++ b/homeassistant/components/co2signal/config_flow.py
@@ -4,8 +4,11 @@ from __future__ import annotations
from collections.abc import Mapping
from typing import Any
-from aioelectricitymaps import ElectricityMaps
-from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken
+from aioelectricitymaps import (
+ ElectricityMaps,
+ ElectricityMapsError,
+ ElectricityMapsInvalidTokenError,
+)
import voluptuous as vol
from homeassistant import config_entries
@@ -146,7 +149,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
await fetch_latest_carbon_intensity(self.hass, em, data)
- except InvalidToken:
+ except ElectricityMapsInvalidTokenError:
errors["base"] = "invalid_auth"
except ElectricityMapsError:
errors["base"] = "unknown"
diff --git a/homeassistant/components/co2signal/coordinator.py b/homeassistant/components/co2signal/coordinator.py
index 115c976b465..b06bee38bc4 100644
--- a/homeassistant/components/co2signal/coordinator.py
+++ b/homeassistant/components/co2signal/coordinator.py
@@ -4,9 +4,12 @@ from __future__ import annotations
from datetime import timedelta
import logging
-from aioelectricitymaps import ElectricityMaps
-from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken
-from aioelectricitymaps.models import CarbonIntensityResponse
+from aioelectricitymaps import (
+ CarbonIntensityResponse,
+ ElectricityMaps,
+ ElectricityMapsError,
+ ElectricityMapsInvalidTokenError,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -43,7 +46,7 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]):
return await fetch_latest_carbon_intensity(
self.hass, self.client, self.config_entry.data
)
- except InvalidToken as err:
+ except ElectricityMapsInvalidTokenError as err:
raise ConfigEntryAuthFailed from err
except ElectricityMapsError as err:
raise UpdateFailed(str(err)) from err
diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json
index 87f2b5c2db0..a4cbed00684 100644
--- a/homeassistant/components/co2signal/manifest.json
+++ b/homeassistant/components/co2signal/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioelectricitymaps"],
- "requirements": ["aioelectricitymaps==0.2.0"]
+ "requirements": ["aioelectricitymaps==0.3.0"]
}
diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py
index 7ca7fec115f..09b0e8e2310 100644
--- a/homeassistant/components/conversation/__init__.py
+++ b/homeassistant/components/conversation/__init__.py
@@ -349,7 +349,7 @@ async def websocket_hass_agent_debug(
},
# Slot values that would be received by the intent
"slots": { # direct access to values
- entity_key: entity.value
+ entity_key: entity.text or entity.value
for entity_key, entity in result.entities.items()
},
# Extra slot details, such as the originally matched text
diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py
index c9119935213..fb33d87e107 100644
--- a/homeassistant/components/conversation/default_agent.py
+++ b/homeassistant/components/conversation/default_agent.py
@@ -1,4 +1,5 @@
"""Standard conversation implementation for Home Assistant."""
+
from __future__ import annotations
import asyncio
@@ -12,22 +13,15 @@ import re
from typing import IO, Any
from hassil.expression import Expression, ListReference, Sequence
-from hassil.intents import (
- Intents,
- ResponseType,
- SlotList,
- TextSlotList,
- WildcardSlotList,
-)
+from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
from hassil.recognize import (
MISSING_ENTITY,
RecognizeResult,
- UnmatchedEntity,
UnmatchedTextEntity,
recognize_all,
)
from hassil.util import merge_dict
-from home_assistant_intents import get_intents, get_languages
+from home_assistant_intents import ErrorKey, get_intents, get_languages
import yaml
from homeassistant import core, setup
@@ -238,7 +232,10 @@ class DefaultAgent(AbstractConversationAgent):
)
)
- # Use last non-empty result as response
+ # Use last non-empty result as response.
+ #
+ # There may be multiple copies of a trigger running when editing in
+ # the UI, so it's critical that we filter out empty responses here.
response_text: str | None = None
for trigger_response in trigger_responses:
response_text = response_text or trigger_response
@@ -246,7 +243,7 @@ class DefaultAgent(AbstractConversationAgent):
# Convert to conversation result
response = intent.IntentResponse(language=language)
response.response_type = intent.IntentResponseType.ACTION_DONE
- response.async_set_speech(response_text or "")
+ response.async_set_speech(response_text or "Done")
return ConversationResult(response=response)
@@ -259,7 +256,7 @@ class DefaultAgent(AbstractConversationAgent):
return _make_error_result(
language,
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
- self._get_error_text(ResponseType.NO_INTENT, lang_intents),
+ self._get_error_text(ErrorKey.NO_INTENT, lang_intents),
conversation_id,
)
@@ -268,14 +265,14 @@ class DefaultAgent(AbstractConversationAgent):
_LOGGER.debug(
"Recognized intent '%s' for template '%s' but had unmatched: %s",
result.intent.name,
- result.intent_sentence.text
- if result.intent_sentence is not None
- else "",
+ (
+ result.intent_sentence.text
+ if result.intent_sentence is not None
+ else ""
+ ),
result.unmatched_entities_list,
)
- error_response_type, error_response_args = _get_unmatched_response(
- result.unmatched_entities
- )
+ error_response_type, error_response_args = _get_unmatched_response(result)
return _make_error_result(
language,
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
@@ -291,7 +288,8 @@ class DefaultAgent(AbstractConversationAgent):
# Slot values to pass to the intent
slots = {
- entity.name: {"value": entity.value} for entity in result.entities_list
+ entity.name: {"value": entity.value, "text": entity.text or entity.value}
+ for entity in result.entities_list
}
try:
@@ -325,7 +323,7 @@ class DefaultAgent(AbstractConversationAgent):
return _make_error_result(
language,
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
- self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
+ self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
conversation_id,
)
except intent.IntentUnexpectedError:
@@ -333,7 +331,7 @@ class DefaultAgent(AbstractConversationAgent):
return _make_error_result(
language,
intent.IntentResponseErrorCode.UNKNOWN,
- self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
+ self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
conversation_id,
)
@@ -480,9 +478,11 @@ class DefaultAgent(AbstractConversationAgent):
for entity_name, entity_value in recognize_result.entities.items()
},
# First matched or unmatched state
- "state": template.TemplateState(self.hass, state1)
- if state1 is not None
- else None,
+ "state": (
+ template.TemplateState(self.hass, state1)
+ if state1 is not None
+ else None
+ ),
"query": {
# Entity states that matched the query (e.g, "on")
"matched": [
@@ -740,7 +740,7 @@ class DefaultAgent(AbstractConversationAgent):
if not entity:
# Default name
- entity_names.append((state.name, state.name, context))
+ entity_names.append((state.name, state.entity_id, context))
continue
if entity.aliases:
@@ -748,10 +748,10 @@ class DefaultAgent(AbstractConversationAgent):
if not alias.strip():
continue
- entity_names.append((alias, alias, context))
+ entity_names.append((alias, state.entity_id, context))
# Default name
- entity_names.append((state.name, state.name, context))
+ entity_names.append((state.name, state.entity_id, context))
# Expose all areas
areas = ar.async_get(self.hass)
@@ -791,11 +791,11 @@ class DefaultAgent(AbstractConversationAgent):
if device_area is None:
return None
- return {"area": device_area.id}
+ return {"area": {"value": device_area.id, "text": device_area.name}}
def _get_error_text(
self,
- response_type: ResponseType,
+ error_key: ErrorKey,
lang_intents: LanguageIntents | None,
**response_args,
) -> str:
@@ -803,7 +803,7 @@ class DefaultAgent(AbstractConversationAgent):
if lang_intents is None:
return _DEFAULT_ERROR_TEXT
- response_key = response_type.value
+ response_key = error_key.value
response_str = (
lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT
)
@@ -916,59 +916,72 @@ def _make_error_result(
return ConversationResult(response, conversation_id)
-def _get_unmatched_response(
- unmatched_entities: dict[str, UnmatchedEntity],
-) -> tuple[ResponseType, dict[str, Any]]:
- error_response_type = ResponseType.NO_INTENT
- error_response_args: dict[str, Any] = {}
+def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str, Any]]:
+ """Get key and template arguments for error when there are unmatched intent entities/slots."""
- if unmatched_name := unmatched_entities.get("name"):
- # Unmatched device or entity
- assert isinstance(unmatched_name, UnmatchedTextEntity)
- error_response_type = ResponseType.NO_ENTITY
- error_response_args["entity"] = unmatched_name.text
+ # Filter out non-text and missing context entities
+ unmatched_text: dict[str, str] = {
+ key: entity.text.strip()
+ for key, entity in result.unmatched_entities.items()
+ if isinstance(entity, UnmatchedTextEntity) and entity.text != MISSING_ENTITY
+ }
- elif unmatched_area := unmatched_entities.get("area"):
- # Unmatched area
- assert isinstance(unmatched_area, UnmatchedTextEntity)
- error_response_type = ResponseType.NO_AREA
- error_response_args["area"] = unmatched_area.text
+ if unmatched_area := unmatched_text.get("area"):
+ # area only
+ return ErrorKey.NO_AREA, {"area": unmatched_area}
- return error_response_type, error_response_args
+ # Area may still have matched
+ matched_area: str | None = None
+ if matched_area_entity := result.entities.get("area"):
+ matched_area = matched_area_entity.text.strip()
+
+ if unmatched_name := unmatched_text.get("name"):
+ if matched_area:
+ # device in area
+ return ErrorKey.NO_ENTITY_IN_AREA, {
+ "entity": unmatched_name,
+ "area": matched_area,
+ }
+
+ # device only
+ return ErrorKey.NO_ENTITY, {"entity": unmatched_name}
+
+ # Default error
+ return ErrorKey.NO_INTENT, {}
def _get_no_states_matched_response(
no_states_error: intent.NoStatesMatchedError,
-) -> tuple[ResponseType, dict[str, Any]]:
- """Return error response type and template arguments for error."""
- if not (
- no_states_error.area
- and (no_states_error.device_classes or no_states_error.domains)
- ):
- # Device class and domain must be paired with an area for the error
- # message.
- return ResponseType.NO_INTENT, {}
+) -> tuple[ErrorKey, dict[str, Any]]:
+ """Return key and template arguments for error when intent returns no matching states."""
- error_response_args: dict[str, Any] = {"area": no_states_error.area}
-
- # Check device classes first, since it's more specific than domain
+ # Device classes should be checked before domains
if no_states_error.device_classes:
- # No exposed entities of a particular class in an area.
- # Example: "close the bedroom windows"
- #
- # Only use the first device class for the error message
- error_response_args["device_class"] = next(iter(no_states_error.device_classes))
+ device_class = next(iter(no_states_error.device_classes)) # first device class
+ if no_states_error.area:
+ # device_class in area
+ return ErrorKey.NO_DEVICE_CLASS_IN_AREA, {
+ "device_class": device_class,
+ "area": no_states_error.area,
+ }
- return ResponseType.NO_DEVICE_CLASS, error_response_args
+ # device_class only
+ return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
- # No exposed entities of a domain in an area.
- # Example: "turn on lights in kitchen"
- assert no_states_error.domains
- #
- # Only use the first domain for the error message
- error_response_args["domain"] = next(iter(no_states_error.domains))
+ if no_states_error.domains:
+ domain = next(iter(no_states_error.domains)) # first domain
+ if no_states_error.area:
+ # domain in area
+ return ErrorKey.NO_DOMAIN_IN_AREA, {
+ "domain": domain,
+ "area": no_states_error.area,
+ }
- return ResponseType.NO_DOMAIN, error_response_args
+ # domain only
+ return ErrorKey.NO_DOMAIN, {"domain": domain}
+
+ # Default error
+ return ErrorKey.NO_INTENT, {}
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json
index 89dd880f69e..e4317052b04 100644
--- a/homeassistant/components/conversation/manifest.json
+++ b/homeassistant/components/conversation/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
- "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.1.29"]
+ "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.2"]
}
diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py
index d38bb69f3e1..4600135c1e5 100644
--- a/homeassistant/components/conversation/trigger.py
+++ b/homeassistant/components/conversation/trigger.py
@@ -98,7 +98,12 @@ async def async_attach_trigger(
# mypy does not understand the type narrowing, unclear why
return automation_result.conversation_response # type: ignore[return-value]
- return "Done"
+ # It's important to return None here instead of a string.
+ #
+ # When editing in the UI, a copy of this trigger is registered.
+ # If we return a string from here, there is a race condition between the
+ # two trigger copies for who will provide a response.
+ return None
default_agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT)
assert isinstance(default_agent, DefaultAgent)
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index eb1d0d6b672..35a0e810c9e 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -100,6 +100,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity):
TYPE = DOMAIN
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None:
"""Set up thermostat device."""
@@ -119,7 +120,11 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity):
HVAC_MODE_TO_DECONZ[item]: item for item in self._attr_hvac_modes
}
- self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ self._attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
if device.fan_mode:
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py
index b857f98e2da..745a2473939 100644
--- a/homeassistant/components/demo/climate.py
+++ b/homeassistant/components/demo/climate.py
@@ -97,6 +97,7 @@ class DemoClimate(ClimateEntity):
_attr_name = None
_attr_should_poll = False
_attr_translation_key = "ubercool"
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
@@ -137,6 +138,9 @@ class DemoClimate(ClimateEntity):
self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
+ self._attr_supported_features |= (
+ ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
+ )
self._target_temperature = target_temperature
self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement
diff --git a/homeassistant/components/duquesne_light/__init__.py b/homeassistant/components/duquesne_light/__init__.py
new file mode 100644
index 00000000000..33c35ecb4cd
--- /dev/null
+++ b/homeassistant/components/duquesne_light/__init__.py
@@ -0,0 +1 @@
+"""Virtual integration: Duquesne Light."""
diff --git a/homeassistant/components/duquesne_light/manifest.json b/homeassistant/components/duquesne_light/manifest.json
new file mode 100644
index 00000000000..3cb01757950
--- /dev/null
+++ b/homeassistant/components/duquesne_light/manifest.json
@@ -0,0 +1,6 @@
+{
+ "domain": "duquesne_light",
+ "name": "Duquesne Light",
+ "integration_type": "virtual",
+ "supported_by": "opower"
+}
diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py
index 39c61b3ce23..db3c60fa9e7 100644
--- a/homeassistant/components/ecovacs/config_flow.py
+++ b/homeassistant/components/ecovacs/config_flow.py
@@ -75,7 +75,7 @@ async def _validate_input(
rest_config = create_rest_config(
aiohttp_client.async_get_clientsession(hass),
device_id=device_id,
- country=country,
+ alpha_2_country=country,
override_rest_url=rest_url,
)
@@ -266,6 +266,10 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
# If not we will inform the user about the mismatch.
error = None
placeholders = None
+
+ # Convert the country to upper case as ISO 3166-1 alpha-2 country codes are upper case
+ user_input[CONF_COUNTRY] = user_input[CONF_COUNTRY].upper()
+
if len(user_input[CONF_COUNTRY]) != 2:
error = "invalid_country_length"
placeholders = {"countries_url": "https://www.iso.org/obp/ui/#search/code/"}
diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py
index 27a1996c3e9..27b64db20b6 100644
--- a/homeassistant/components/ecovacs/controller.py
+++ b/homeassistant/components/ecovacs/controller.py
@@ -49,7 +49,7 @@ class EcovacsController:
create_rest_config(
aiohttp_client.async_get_clientsession(self._hass),
device_id=self._device_id,
- country=country,
+ alpha_2_country=country,
override_rest_url=config.get(CONF_OVERRIDE_REST_URL),
),
config[CONF_USERNAME],
diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json
index 3472e4746f8..34760ea6aca 100644
--- a/homeassistant/components/ecovacs/manifest.json
+++ b/homeassistant/components/ecovacs/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
- "requirements": ["py-sucks==0.9.8", "deebot-client==5.0.0"]
+ "requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"]
}
diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json
index f56b65a4e46..7a456483877 100644
--- a/homeassistant/components/ecovacs/strings.json
+++ b/homeassistant/components/ecovacs/strings.json
@@ -47,13 +47,13 @@
"name": "Relocate"
},
"reset_lifespan_brush": {
- "name": "Reset brush lifespan"
+ "name": "Reset main brush lifespan"
},
"reset_lifespan_filter": {
"name": "Reset filter lifespan"
},
"reset_lifespan_side_brush": {
- "name": "Reset side brush lifespan"
+ "name": "Reset side brushes lifespan"
}
},
"image": {
@@ -79,13 +79,13 @@
}
},
"lifespan_brush": {
- "name": "Brush lifespan"
+ "name": "Main brush lifespan"
},
"lifespan_filter": {
"name": "Filter lifespan"
},
"lifespan_side_brush": {
- "name": "Side brush lifespan"
+ "name": "Side brushes lifespan"
},
"network_ip": {
"name": "IP address"
@@ -100,7 +100,7 @@
"name": "Area cleaned"
},
"stats_time": {
- "name": "Time cleaned"
+ "name": "Cleaning duration"
},
"total_stats_area": {
"name": "Total area cleaned"
@@ -109,12 +109,12 @@
"name": "Total cleanings"
},
"total_stats_time": {
- "name": "Total time cleaned"
+ "name": "Total cleaning duration"
}
},
"select": {
"water_amount": {
- "name": "Water amount",
+ "name": "Water flow level",
"state": {
"high": "High",
"low": "Low",
@@ -137,7 +137,7 @@
"name": "Advanced mode"
},
"carpet_auto_fan_boost": {
- "name": "Carpet auto fan speed boost"
+ "name": "Carpet auto-boost suction"
},
"clean_preference": {
"name": "Clean preference"
diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json
index 0671a7adb1d..c68902560b9 100644
--- a/homeassistant/components/elgato/manifest.json
+++ b/homeassistant/components/elgato/manifest.json
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
- "requirements": ["elgato==5.1.1"],
+ "requirements": ["elgato==5.1.2"],
"zeroconf": ["_elg._tcp.local."]
}
diff --git a/homeassistant/components/elvia/config_flow.py b/homeassistant/components/elvia/config_flow.py
index e65c93b09a6..fb50842e39b 100644
--- a/homeassistant/components/elvia/config_flow.py
+++ b/homeassistant/components/elvia/config_flow.py
@@ -35,8 +35,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._api_token = api_token = user_input[CONF_API_TOKEN]
client = Elvia(meter_value_token=api_token).meter_value()
try:
+ end_time = dt_util.utcnow()
results = await client.get_meter_values(
- start_time=(dt_util.now() - timedelta(hours=1)).isoformat()
+ start_time=(end_time - timedelta(hours=1)).isoformat(),
+ end_time=end_time.isoformat(),
)
except ElviaError.AuthError as exception:
diff --git a/homeassistant/components/elvia/importer.py b/homeassistant/components/elvia/importer.py
index 3fc79240254..097db51cab8 100644
--- a/homeassistant/components/elvia/importer.py
+++ b/homeassistant/components/elvia/importer.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, cast
-from elvia import Elvia
+from elvia import Elvia, error as ElviaError
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.statistics import (
@@ -38,11 +38,18 @@ class ElviaImporter:
self.client = Elvia(meter_value_token=api_token).meter_value()
self.metering_point_id = metering_point_id
- async def _fetch_hourly_data(self, since: datetime) -> list[MeterValueTimeSeries]:
+ async def _fetch_hourly_data(
+ self,
+ since: datetime,
+ until: datetime,
+ ) -> list[MeterValueTimeSeries]:
"""Fetch hourly data."""
- LOGGER.debug("Fetching hourly data since %s", since)
+ start_time = since.isoformat()
+ end_time = until.isoformat()
+ LOGGER.debug("Fetching hourly data %s - %s", start_time, end_time)
all_data = await self.client.get_meter_values(
- start_time=since.isoformat(),
+ start_time=start_time,
+ end_time=end_time,
metering_point_ids=[self.metering_point_id],
)
return all_data["meteringpoints"][0]["metervalue"]["timeSeries"]
@@ -61,18 +68,37 @@ class ElviaImporter:
)
if not last_stats:
- # First time we insert 1 years of data (if available)
- hourly_data = await self._fetch_hourly_data(
- since=dt_util.now() - timedelta(days=365)
- )
+ # First time we insert 3 years of data (if available)
+ hourly_data: list[MeterValueTimeSeries] = []
+ until = dt_util.utcnow()
+ for year in (3, 2, 1):
+ try:
+ year_hours = await self._fetch_hourly_data(
+ since=until - timedelta(days=365 * year),
+ until=until - timedelta(days=365 * (year - 1)),
+ )
+ except ElviaError.ElviaException:
+ # This will raise if the contract have no data for the
+ # year, we can safely ignore this
+ continue
+ hourly_data.extend(year_hours)
+
if hourly_data is None or len(hourly_data) == 0:
+ LOGGER.error("No data available for the metering point")
return
last_stats_time = None
_sum = 0.0
else:
- hourly_data = await self._fetch_hourly_data(
- since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"])
- )
+ try:
+ hourly_data = await self._fetch_hourly_data(
+ since=dt_util.utc_from_timestamp(
+ last_stats[statistic_id][0]["end"]
+ ),
+ until=dt_util.utcnow(),
+ )
+ except ElviaError.ElviaException as err:
+ LOGGER.error("Error fetching data: %s", err)
+ return
if (
hourly_data is None
diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py
index f197574c30a..59f37d3a078 100644
--- a/homeassistant/components/esphome/manager.py
+++ b/homeassistant/components/esphome/manager.py
@@ -352,7 +352,6 @@ class ESPHomeManager:
if self.voice_assistant_udp_server is not None:
_LOGGER.warning("Voice assistant UDP server was not stopped")
self.voice_assistant_udp_server.stop()
- self.voice_assistant_udp_server.close()
self.voice_assistant_udp_server = None
hass = self.hass
diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py
index de6b521d980..7c5c74d58ee 100644
--- a/homeassistant/components/esphome/voice_assistant.py
+++ b/homeassistant/components/esphome/voice_assistant.py
@@ -1,4 +1,5 @@
"""ESPHome voice assistant support."""
+
from __future__ import annotations
import asyncio
@@ -67,7 +68,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
"""Receive UDP packets and forward them to the voice assistant."""
started = False
- stopped = False
+ stop_requested = False
transport: asyncio.DatagramTransport | None = None
remote_addr: tuple[str, int] | None = None
@@ -92,6 +93,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
self._tts_done = asyncio.Event()
self._tts_task: asyncio.Task | None = None
+ @property
+ def is_running(self) -> bool:
+ """True if the the UDP server is started and hasn't been asked to stop."""
+ return self.started and (not self.stop_requested)
+
async def start_server(self) -> int:
"""Start accepting connections."""
@@ -99,7 +105,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
"""Accept connection."""
if self.started:
raise RuntimeError("Can only start once")
- if self.stopped:
+ if self.stop_requested:
raise RuntimeError("No longer accepting connections")
self.started = True
@@ -124,7 +130,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
@callback
def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
"""Handle incoming UDP packet."""
- if not self.started or self.stopped:
+ if not self.is_running:
return
if self.remote_addr is None:
self.remote_addr = addr
@@ -142,19 +148,19 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
def stop(self) -> None:
"""Stop the receiver."""
self.queue.put_nowait(b"")
- self.started = False
- self.stopped = True
+ self.close()
def close(self) -> None:
"""Close the receiver."""
self.started = False
- self.stopped = True
+ self.stop_requested = True
+
if self.transport is not None:
self.transport.close()
async def _iterate_packets(self) -> AsyncIterable[bytes]:
"""Iterate over incoming packets."""
- if not self.started or self.stopped:
+ if not self.is_running:
raise RuntimeError("Not running")
while data := await self.queue.get():
@@ -303,8 +309,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
async def _send_tts(self, media_id: str) -> None:
"""Send TTS audio to device via UDP."""
+ # Always send stream start/end events
+ self.handle_event(VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {})
+
try:
- if self.transport is None:
+ if (not self.is_running) or (self.transport is None):
return
extension, data = await tts.async_get_media_source_audio(
@@ -337,15 +346,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
_LOGGER.debug("Sending %d bytes of audio", audio_bytes_size)
- self.handle_event(
- VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}
- )
-
bytes_per_sample = stt.AudioBitRates.BITRATE_16 // 8
sample_offset = 0
samples_left = audio_bytes_size // bytes_per_sample
- while samples_left > 0:
+ while (samples_left > 0) and self.is_running:
bytes_offset = sample_offset * bytes_per_sample
chunk: bytes = audio_bytes[bytes_offset : bytes_offset + 1024]
samples_in_chunk = len(chunk) // bytes_per_sample
diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py
index a1c46f3d331..8b74d31cc0d 100644
--- a/homeassistant/components/evohome/climate.py
+++ b/homeassistant/components/evohome/climate.py
@@ -156,6 +156,7 @@ class EvoClimateEntity(EvoDevice, ClimateEntity):
"""Base for an evohome Climate device."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
@property
def hvac_modes(self) -> list[HVACMode]:
@@ -190,7 +191,10 @@ class EvoZone(EvoChild, EvoClimateEntity):
]
self._attr_supported_features = (
- ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
+ ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None:
@@ -372,6 +376,9 @@ class EvoController(EvoClimateEntity):
]
if self._attr_preset_modes:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
+ self._attr_supported_features |= (
+ ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
+ )
async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None:
"""Process a service request (system mode) for a controller.
diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py
index 18fef8dbe7a..42b8a5c0446 100644
--- a/homeassistant/components/fibaro/climate.py
+++ b/homeassistant/components/fibaro/climate.py
@@ -126,6 +126,8 @@ async def async_setup_entry(
class FibaroThermostat(FibaroDevice, ClimateEntity):
"""Representation of a Fibaro Thermostat."""
+ _enable_turn_on_off_backwards_compatibility = False
+
def __init__(self, fibaro_device: DeviceModel) -> None:
"""Initialize the Fibaro device."""
super().__init__(fibaro_device)
@@ -209,6 +211,11 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
if mode in OPMODES_PRESET:
self._attr_preset_modes.append(OPMODES_PRESET[mode])
+ if HVACMode.OFF in self._attr_hvac_modes and len(self._attr_hvac_modes) > 1:
+ self._attr_supported_features |= (
+ ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
+ )
+
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
_LOGGER.debug(
diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py
index b833617f2ca..85d5e9f4eac 100644
--- a/homeassistant/components/flexit/climate.py
+++ b/homeassistant/components/flexit/climate.py
@@ -69,6 +69,7 @@ class Flexit(ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, hub: ModbusHub, modbus_slave: int | None, name: str | None
diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py
index 7740bed73e1..0d8a381a014 100644
--- a/homeassistant/components/flexit_bacnet/climate.py
+++ b/homeassistant/components/flexit_bacnet/climate.py
@@ -62,13 +62,17 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
]
_attr_supported_features = (
- ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
+ ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_target_temperature_step = PRECISION_HALVES
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_max_temp = MAX_TEMP
_attr_min_temp = MIN_TEMP
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator: FlexitCoordinator) -> None:
"""Initialize the Flexit unit."""
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 2b005c7e1ad..039328b9cac 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
- "requirements": ["home-assistant-frontend==20240131.0"]
+ "requirements": ["home-assistant-frontend==20240202.0"]
}
diff --git a/homeassistant/components/google_assistant/data_redaction.py b/homeassistant/components/google_assistant/data_redaction.py
new file mode 100644
index 00000000000..ae6fe5f7098
--- /dev/null
+++ b/homeassistant/components/google_assistant/data_redaction.py
@@ -0,0 +1,36 @@
+"""Helpers to redact Google Assistant data when logging."""
+from __future__ import annotations
+
+from collections.abc import Callable
+from typing import Any
+
+from homeassistant.core import callback
+from homeassistant.helpers.redact import async_redact_data, partial_redact
+
+REQUEST_MSG_TO_REDACT: dict[str, Callable[[str], str]] = {
+ "agentUserId": partial_redact,
+ "uuid": partial_redact,
+ "webhookId": partial_redact,
+}
+
+RESPONSE_MSG_TO_REDACT = REQUEST_MSG_TO_REDACT | {id: partial_redact}
+
+SYNC_MSG_TO_REDACT = REQUEST_MSG_TO_REDACT
+
+
+@callback
+def async_redact_request_msg(msg: dict[str, Any]) -> dict[str, Any]:
+ """Mask sensitive data in message."""
+ return async_redact_data(msg, REQUEST_MSG_TO_REDACT)
+
+
+@callback
+def async_redact_response_msg(msg: dict[str, Any]) -> dict[str, Any]:
+ """Mask sensitive data in message."""
+ return async_redact_data(msg, RESPONSE_MSG_TO_REDACT)
+
+
+@callback
+def async_redact_sync_msg(msg: dict[str, Any]) -> dict[str, Any]:
+ """Mask sensitive data in message."""
+ return async_redact_data(msg, SYNC_MSG_TO_REDACT)
diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py
index f3d0d24f7c8..d75ebb49509 100644
--- a/homeassistant/components/google_assistant/helpers.py
+++ b/homeassistant/components/google_assistant/helpers.py
@@ -32,6 +32,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.network import get_url
+from homeassistant.helpers.redact import partial_redact
from homeassistant.helpers.storage import Store
from homeassistant.util.dt import utcnow
@@ -48,6 +49,7 @@ from .const import (
STORE_AGENT_USER_IDS,
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
)
+from .data_redaction import async_redact_request_msg, async_redact_response_msg
from .error import SmartHomeError
SYNC_DELAY = 15
@@ -332,8 +334,8 @@ class AbstractConfig(ABC):
_LOGGER.debug(
"Register webhook handler %s for agent user id %s",
- webhook_id,
- user_agent_id,
+ partial_redact(webhook_id),
+ partial_redact(user_agent_id),
)
try:
webhook.async_register(
@@ -348,8 +350,8 @@ class AbstractConfig(ABC):
except ValueError:
_LOGGER.warning(
"Webhook handler %s for agent user id %s is already defined!",
- webhook_id,
- user_agent_id,
+ partial_redact(webhook_id),
+ partial_redact(user_agent_id),
)
setup_successful = False
break
@@ -374,8 +376,8 @@ class AbstractConfig(ABC):
webhook_id = self.get_local_webhook_id(agent_user_id)
_LOGGER.debug(
"Unregister webhook handler %s for agent user id %s",
- webhook_id,
- agent_user_id,
+ partial_redact(webhook_id),
+ partial_redact(agent_user_id),
)
webhook.async_unregister(self.hass, webhook_id)
@@ -410,7 +412,7 @@ class AbstractConfig(ABC):
"Received local message from %s (JS %s):\n%s\n",
request.remote,
request.headers.get("HA-Cloud-Version", "unknown"),
- pprint.pformat(payload),
+ pprint.pformat(async_redact_request_msg(payload)),
)
if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None:
@@ -421,8 +423,8 @@ class AbstractConfig(ABC):
"Cannot process request for webhook %s as no linked agent user is"
" found:\n%s\n"
),
- webhook_id,
- pprint.pformat(payload),
+ partial_redact(webhook_id),
+ pprint.pformat(async_redact_request_msg(payload)),
)
webhook.async_unregister(self.hass, webhook_id)
return None
@@ -441,7 +443,10 @@ class AbstractConfig(ABC):
)
if _LOGGER.isEnabledFor(logging.DEBUG):
- _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result))
+ _LOGGER.debug(
+ "Responding to local message:\n%s\n",
+ pprint.pformat(async_redact_response_msg(result)),
+ )
return json_response(result)
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index b8c57812540..7d8cc752342 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -18,6 +18,11 @@ from .const import (
EVENT_QUERY_RECEIVED,
EVENT_SYNC_RECEIVED,
)
+from .data_redaction import (
+ async_redact_request_msg,
+ async_redact_response_msg,
+ async_redact_sync_msg,
+)
from .error import SmartHomeError
from .helpers import GoogleEntity, RequestData, async_get_entities
@@ -42,7 +47,11 @@ async def async_handle_message(hass, config, user_id, message, source):
response = await _process(hass, data, message)
if response and "errorCode" in response["payload"]:
- _LOGGER.error("Error handling message %s: %s", message, response["payload"])
+ _LOGGER.error(
+ "Error handling message %s: %s",
+ async_redact_request_msg(message),
+ async_redact_response_msg(response["payload"]),
+ )
return response
@@ -118,7 +127,7 @@ async def async_devices_sync(
devices = await async_devices_sync_response(hass, data.config, agent_user_id)
response = create_sync_response(agent_user_id, devices)
- _LOGGER.debug("Syncing entities response: %s", response)
+ _LOGGER.debug("Syncing entities response: %s", async_redact_sync_msg(response))
return response
diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py
index d5d25397f2a..135d9c6c28f 100644
--- a/homeassistant/components/gpsd/sensor.py
+++ b/homeassistant/components/gpsd/sensor.py
@@ -1,6 +1,8 @@
-"""Support for GPSD."""
+"""Sensor platform for GPSD integration."""
from __future__ import annotations
+from collections.abc import Callable
+from dataclasses import dataclass
import logging
from typing import Any
@@ -15,6 +17,7 @@ from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
+ SensorEntityDescription,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
@@ -24,6 +27,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PORT,
+ EntityCategory,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.config_validation as cv
@@ -43,6 +47,28 @@ ATTR_SPEED = "speed"
DEFAULT_NAME = "GPS"
+_MODE_VALUES = {2: "2d_fix", 3: "3d_fix"}
+
+
+@dataclass(frozen=True, kw_only=True)
+class GpsdSensorDescription(SensorEntityDescription):
+ """Class describing GPSD sensor entities."""
+
+ value_fn: Callable[[AGPS3mechanism], str | None]
+
+
+SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = (
+ GpsdSensorDescription(
+ key="mode",
+ translation_key="mode",
+ name=None,
+ entity_category=EntityCategory.DIAGNOSTIC,
+ device_class=SensorDeviceClass.ENUM,
+ options=list(_MODE_VALUES.values()),
+ value_fn=lambda agps_thread: _MODE_VALUES.get(agps_thread.data_stream.mode),
+ ),
+)
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -64,7 +90,9 @@ async def async_setup_entry(
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
config_entry.entry_id,
+ description,
)
+ for description in SENSOR_TYPES
]
)
@@ -101,23 +129,23 @@ class GpsdSensor(SensorEntity):
"""Representation of a GPS receiver available via GPSD."""
_attr_has_entity_name = True
- _attr_name = None
- _attr_translation_key = "mode"
- _attr_device_class = SensorDeviceClass.ENUM
- _attr_options = ["2d_fix", "3d_fix"]
+
+ entity_description: GpsdSensorDescription
def __init__(
self,
host: str,
port: int,
unique_id: str,
+ description: GpsdSensorDescription,
) -> None:
"""Initialize the GPSD sensor."""
+ self.entity_description = description
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
entry_type=DeviceEntryType.SERVICE,
)
- self._attr_unique_id = f"{unique_id}-mode"
+ self._attr_unique_id = f"{unique_id}-{self.entity_description.key}"
self.agps_thread = AGPS3mechanism()
self.agps_thread.stream_data(host=host, port=port)
@@ -126,11 +154,7 @@ class GpsdSensor(SensorEntity):
@property
def native_value(self) -> str | None:
"""Return the state of GPSD."""
- if self.agps_thread.data_stream.mode == 3:
- return "3d_fix"
- if self.agps_thread.data_stream.mode == 2:
- return "2d_fix"
- return None
+ return self.entity_description.value_fn(self.agps_thread)
@property
def extra_state_attributes(self) -> dict[str, Any]:
diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py
index 5a113491891..c8689cdaa1c 100644
--- a/homeassistant/components/group/light.py
+++ b/homeassistant/components/group/light.py
@@ -30,6 +30,7 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
LightEntityFeature,
+ filter_supported_color_modes,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -162,6 +163,9 @@ class LightGroup(GroupEntity, LightEntity):
if mode:
self.mode = all
+ self._attr_color_mode = ColorMode.UNKNOWN
+ self._attr_supported_color_modes = {ColorMode.ONOFF}
+
async def async_turn_on(self, **kwargs: Any) -> None:
"""Forward the turn_on command to all lights in the light group."""
data = {
@@ -261,26 +265,36 @@ class LightGroup(GroupEntity, LightEntity):
effects_count = Counter(itertools.chain(all_effects))
self._attr_effect = effects_count.most_common(1)[0][0]
- self._attr_color_mode = None
- all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE))
- if all_color_modes:
- # Report the most common color mode, select brightness and onoff last
- color_mode_count = Counter(itertools.chain(all_color_modes))
- if ColorMode.ONOFF in color_mode_count:
- color_mode_count[ColorMode.ONOFF] = -1
- if ColorMode.BRIGHTNESS in color_mode_count:
- color_mode_count[ColorMode.BRIGHTNESS] = 0
- self._attr_color_mode = color_mode_count.most_common(1)[0][0]
-
- self._attr_supported_color_modes = None
+ supported_color_modes = {ColorMode.ONOFF}
all_supported_color_modes = list(
find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES)
)
if all_supported_color_modes:
# Merge all color modes.
- self._attr_supported_color_modes = cast(
- set[str], set().union(*all_supported_color_modes)
+ supported_color_modes = filter_supported_color_modes(
+ cast(set[ColorMode], set().union(*all_supported_color_modes))
)
+ self._attr_supported_color_modes = supported_color_modes
+
+ self._attr_color_mode = ColorMode.UNKNOWN
+ all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE))
+ if all_color_modes:
+ # Report the most common color mode, select brightness and onoff last
+ color_mode_count = Counter(itertools.chain(all_color_modes))
+ if ColorMode.ONOFF in color_mode_count:
+ if ColorMode.ONOFF in supported_color_modes:
+ color_mode_count[ColorMode.ONOFF] = -1
+ else:
+ color_mode_count.pop(ColorMode.ONOFF)
+ if ColorMode.BRIGHTNESS in color_mode_count:
+ if ColorMode.BRIGHTNESS in supported_color_modes:
+ color_mode_count[ColorMode.BRIGHTNESS] = 0
+ else:
+ color_mode_count.pop(ColorMode.BRIGHTNESS)
+ if color_mode_count:
+ self._attr_color_mode = color_mode_count.most_common(1)[0][0]
+ else:
+ self._attr_color_mode = next(iter(supported_color_modes))
self._attr_supported_features = LightEntityFeature(0)
for support in find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py
index 8cc4ec569dd..0ca85da3fa2 100644
--- a/homeassistant/components/homekit_controller/climate.py
+++ b/homeassistant/components/homekit_controller/climate.py
@@ -139,6 +139,7 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity):
"""The base HomeKit Controller climate entity."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
@callback
def _async_reconfigure(self) -> None:
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index 799058b0e20..1617b907a26 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
- "requirements": ["aiohomekit==3.1.3"],
+ "requirements": ["aiohomekit==3.1.4"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}
diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py
index c79d99276b1..baabf4ca4d8 100644
--- a/homeassistant/components/honeywell/__init__.py
+++ b/homeassistant/components/honeywell/__init__.py
@@ -8,7 +8,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.aiohttp_client import (
+ async_create_clientsession,
+ async_get_clientsession,
+)
from .const import (
_LOGGER,
@@ -48,9 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD]
- client = aiosomecomfort.AIOSomeComfort(
- username, password, session=async_get_clientsession(hass)
- )
+ if len(hass.config_entries.async_entries(DOMAIN)) > 1:
+ session = async_create_clientsession(hass)
+ else:
+ session = async_get_clientsession(hass)
+
+ client = aiosomecomfort.AIOSomeComfort(username, password, session=session)
try:
await client.login()
await client.discover()
@@ -76,7 +82,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if len(devices) == 0:
_LOGGER.debug("No devices found")
return False
-
data = HoneywellData(config_entry.entry_id, client, devices)
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index 72039e1300f..1038cdde80f 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -134,12 +134,17 @@ class KNXClimate(KnxEntity, ClimateEntity):
_device: XknxClimate
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of a KNX climate device."""
super().__init__(_create_climate(xknx, config))
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
- self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ self._attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON
+ )
+ if self._device.supports_on_off:
+ self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
if self.preset_modes:
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_target_temperature_step = self._device.temperature_step
diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py
index d728cfac890..0bd00177cc1 100644
--- a/homeassistant/components/lutron/light.py
+++ b/homeassistant/components/lutron/light.py
@@ -42,7 +42,7 @@ async def async_setup_entry(
lights = []
for area_name, device in entry_data.lights:
- if device.type == "CEILING_FAN_TYPE2":
+ if device.type == "CEILING_FAN_TYPE":
# If this is a fan, check to see if this entity already exists.
# If not, do not create a new one.
entity_id = ent_reg.async_get_entity_id(
diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json
index a0eb7f3cb5b..0838bcc3764 100644
--- a/homeassistant/components/matrix/manifest.json
+++ b/homeassistant/components/matrix/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/matrix",
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
- "requirements": ["matrix-nio==0.22.1", "Pillow==10.2.0"]
+ "requirements": ["matrix-nio==0.24.0", "Pillow==10.2.0"]
}
diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py
index a22f9174d2a..8769fc430d8 100644
--- a/homeassistant/components/matter/climate.py
+++ b/homeassistant/components/matter/climate.py
@@ -73,11 +73,8 @@ class MatterClimate(MatterEntity, ClimateEntity):
"""Representation of a Matter climate entity."""
_attr_temperature_unit: str = UnitOfTemperature.CELSIUS
- _attr_supported_features: ClimateEntityFeature = (
- ClimateEntityFeature.TARGET_TEMPERATURE
- | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
- )
_attr_hvac_mode: HVACMode = HVACMode.OFF
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
@@ -99,6 +96,13 @@ class MatterClimate(MatterEntity, ClimateEntity):
self._attr_hvac_modes.append(HVACMode.COOL)
if feature_map & ThermostatFeature.kAutoMode:
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
+ self._attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
+ | ClimateEntityFeature.TURN_OFF
+ )
+ if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF):
+ self._attr_supported_features |= ClimateEntityFeature.TURN_ON
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py
index 2ef451b04a7..f3d302fc209 100644
--- a/homeassistant/components/maxcube/climate.py
+++ b/homeassistant/components/maxcube/climate.py
@@ -68,8 +68,12 @@ class MaxCubeClimate(ClimateEntity):
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.HEAT]
_attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, handler, device):
"""Initialize MAX! Cube ClimateEntity."""
diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py
index 9d2a4f08257..ed37ff76b76 100644
--- a/homeassistant/components/melcloud/climate.py
+++ b/homeassistant/components/melcloud/climate.py
@@ -114,6 +114,7 @@ class MelCloudClimate(ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, device: MelCloudDevice) -> None:
"""Initialize the climate."""
@@ -137,6 +138,8 @@ class AtaDeviceClimate(MelCloudClimate):
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.SWING_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None:
diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py
index 9facb18ed05..f94c3af6d9a 100644
--- a/homeassistant/components/melissa/climate.py
+++ b/homeassistant/components/melissa/climate.py
@@ -57,9 +57,13 @@ class MelissaClimate(ClimateEntity):
_attr_hvac_modes = OP_MODES
_attr_supported_features = (
- ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
+ ClimateEntityFeature.FAN_MODE
+ | ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, api, serial_number, init_data):
"""Initialize the climate device."""
diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py
index d0b15f5d8ff..2e7b22da833 100644
--- a/homeassistant/components/mill/climate.py
+++ b/homeassistant/components/mill/climate.py
@@ -99,6 +99,7 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
)
_attr_target_temperature_step = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, coordinator: MillDataUpdateCoordinator, heater: mill.Heater
diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py
index 877d33afbcc..cdc1e7a6986 100644
--- a/homeassistant/components/modbus/base_platform.py
+++ b/homeassistant/components/modbus/base_platform.py
@@ -182,7 +182,6 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
self._data_type = config[CONF_DATA_TYPE]
self._structure: str = config[CONF_STRUCTURE]
self._scale = config[CONF_SCALE]
- self._precision = config.get(CONF_PRECISION, 2)
self._offset = config[CONF_OFFSET]
self._slave_count = config.get(CONF_SLAVE_COUNT, None) or config.get(
CONF_VIRTUAL_COUNT, 0
@@ -196,11 +195,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
DataType.UINT32,
DataType.UINT64,
)
- if self._value_is_int:
- if self._min_value:
- self._min_value = round(self._min_value)
- if self._max_value:
- self._max_value = round(self._max_value)
+ if not self._value_is_int:
+ self._precision = config.get(CONF_PRECISION, 2)
+ else:
+ self._precision = config.get(CONF_PRECISION, 0)
def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
"""Do swap as needed."""
@@ -235,13 +233,13 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
return None
val: float | int = self._scale * entry + self._offset
if self._min_value is not None and val < self._min_value:
- return str(self._min_value)
+ val = self._min_value
if self._max_value is not None and val > self._max_value:
- return str(self._max_value)
+ val = self._max_value
if self._zero_suppress is not None and abs(val) <= self._zero_suppress:
return "0"
- if self._precision == 0 or self._value_is_int:
- return str(int(round(val, 0)))
+ if self._precision == 0:
+ return str(round(val))
return f"{float(val):.{self._precision}f}"
def unpack_structure_result(self, registers: list[int]) -> str | None:
diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py
index 71c01d20205..637478fffd4 100644
--- a/homeassistant/components/modbus/climate.py
+++ b/homeassistant/components/modbus/climate.py
@@ -97,7 +97,12 @@ async def async_setup_platform(
class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
"""Representation of a Modbus Thermostat."""
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py
index 76d8e270ffe..37eae23ba82 100644
--- a/homeassistant/components/modbus/validators.py
+++ b/homeassistant/components/modbus/validators.py
@@ -203,141 +203,6 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict:
return config
-def scan_interval_validator(config: dict) -> dict:
- """Control scan_interval."""
- for hub in config:
- minimum_scan_interval = DEFAULT_SCAN_INTERVAL
- for component, conf_key in PLATFORMS:
- if conf_key not in hub:
- continue
-
- for entry in hub[conf_key]:
- scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
- if scan_interval == 0:
- continue
- if scan_interval < 5:
- _LOGGER.warning(
- (
- "%s %s scan_interval(%d) is lower than 5 seconds, "
- "which may cause Home Assistant stability issues"
- ),
- component,
- entry.get(CONF_NAME),
- scan_interval,
- )
- entry[CONF_SCAN_INTERVAL] = scan_interval
- minimum_scan_interval = min(scan_interval, minimum_scan_interval)
- if (
- CONF_TIMEOUT in hub
- and hub[CONF_TIMEOUT] > minimum_scan_interval - 1
- and minimum_scan_interval > 1
- ):
- _LOGGER.warning(
- "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval",
- hub.get(CONF_NAME, ""),
- hub[CONF_TIMEOUT],
- minimum_scan_interval - 1,
- )
- hub[CONF_TIMEOUT] = minimum_scan_interval - 1
- return config
-
-
-def duplicate_entity_validator(config: dict) -> dict:
- """Control scan_interval."""
- for hub_index, hub in enumerate(config):
- for component, conf_key in PLATFORMS:
- if conf_key not in hub:
- continue
- names: set[str] = set()
- errors: list[int] = []
- addresses: set[str] = set()
- for index, entry in enumerate(hub[conf_key]):
- name = entry[CONF_NAME]
- addr = str(entry[CONF_ADDRESS])
- if CONF_INPUT_TYPE in entry:
- addr += "_" + str(entry[CONF_INPUT_TYPE])
- elif CONF_WRITE_TYPE in entry:
- addr += "_" + str(entry[CONF_WRITE_TYPE])
- if CONF_COMMAND_ON in entry:
- addr += "_" + str(entry[CONF_COMMAND_ON])
- if CONF_COMMAND_OFF in entry:
- addr += "_" + str(entry[CONF_COMMAND_OFF])
- inx = entry.get(CONF_SLAVE, None) or entry.get(CONF_DEVICE_ADDRESS, 0)
- addr += "_" + str(inx)
- entry_addrs: set[str] = set()
- entry_addrs.add(addr)
-
- if CONF_TARGET_TEMP in entry:
- a = str(entry[CONF_TARGET_TEMP])
- a += "_" + str(inx)
- entry_addrs.add(a)
- if CONF_HVAC_MODE_REGISTER in entry:
- a = str(entry[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS])
- a += "_" + str(inx)
- entry_addrs.add(a)
- if CONF_FAN_MODE_REGISTER in entry:
- a = str(
- entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]
- if isinstance(entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS], int)
- else entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS][0]
- )
- a += "_" + str(inx)
- entry_addrs.add(a)
-
- dup_addrs = entry_addrs.intersection(addresses)
-
- if len(dup_addrs) > 0:
- for addr in dup_addrs:
- err = (
- f"Modbus {component}/{name} address {addr} is duplicate, second"
- " entry not loaded!"
- )
- _LOGGER.warning(err)
- errors.append(index)
- elif name in names:
- err = (
- f"Modbus {component}/{name} is duplicate, second entry not"
- " loaded!"
- )
- _LOGGER.warning(err)
- errors.append(index)
- else:
- names.add(name)
- addresses.update(entry_addrs)
-
- for i in reversed(errors):
- del config[hub_index][conf_key][i]
- return config
-
-
-def duplicate_modbus_validator(config: dict) -> dict:
- """Control modbus connection for duplicates."""
- hosts: set[str] = set()
- names: set[str] = set()
- errors = []
- for index, hub in enumerate(config):
- name = hub.get(CONF_NAME, DEFAULT_HUB)
- if hub[CONF_TYPE] == SERIAL:
- host = hub[CONF_PORT]
- else:
- host = f"{hub[CONF_HOST]}_{hub[CONF_PORT]}"
- if host in hosts:
- err = f"Modbus {name} contains duplicate host/port {host}, not loaded!"
- _LOGGER.warning(err)
- errors.append(index)
- elif name in names:
- err = f"Modbus {name} is duplicate, second entry not loaded!"
- _LOGGER.warning(err)
- errors.append(index)
- else:
- hosts.add(host)
- names.add(name)
-
- for i in reversed(errors):
- del config[i]
- return config
-
-
def register_int_list_validator(value: Any) -> Any:
"""Check if a register (CONF_ADRESS) is an int or a list having only 1 register."""
if isinstance(value, int) and value >= 0:
@@ -354,7 +219,125 @@ def register_int_list_validator(value: Any) -> Any:
def check_config(config: dict) -> dict:
"""Do final config check."""
- config2 = duplicate_modbus_validator(config)
- config3 = scan_interval_validator(config2)
- config4 = duplicate_entity_validator(config3)
- return config4
+ hosts: set[str] = set()
+ hub_names: set[str] = set()
+ hub_name_inx = 0
+ minimum_scan_interval = 0
+ ent_names: set[str] = set()
+ ent_addr: set[str] = set()
+
+ def validate_modbus(hub: dict, hub_name_inx: int) -> bool:
+ """Validate modbus entries."""
+ host: str = (
+ hub[CONF_PORT]
+ if hub[CONF_TYPE] == SERIAL
+ else f"{hub[CONF_HOST]}_{hub[CONF_PORT]}"
+ )
+ if CONF_NAME not in hub:
+ hub[CONF_NAME] = (
+ DEFAULT_HUB if not hub_name_inx else f"{DEFAULT_HUB}_{hub_name_inx}"
+ )
+ hub_name_inx += 1
+ err = f"Modbus host/port {host} is missing name, added {hub[CONF_NAME]}!"
+ _LOGGER.warning(err)
+ name = hub[CONF_NAME]
+ if host in hosts or name in hub_names:
+ err = f"Modbus {name} host/port {host} is duplicate, not loaded!"
+ _LOGGER.warning(err)
+ return False
+ hosts.add(host)
+ hub_names.add(name)
+ return True
+
+ def validate_entity(
+ hub_name: str,
+ entity: dict,
+ minimum_scan_interval: int,
+ ent_names: set,
+ ent_addr: set,
+ ) -> bool:
+ """Validate entity."""
+ name = entity[CONF_NAME]
+ addr = str(entity[CONF_ADDRESS])
+ scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ if scan_interval < 5:
+ _LOGGER.warning(
+ (
+ "%s %s scan_interval(%d) is lower than 5 seconds, "
+ "which may cause Home Assistant stability issues"
+ ),
+ hub_name,
+ name,
+ scan_interval,
+ )
+ entity[CONF_SCAN_INTERVAL] = scan_interval
+ minimum_scan_interval = min(scan_interval, minimum_scan_interval)
+ for conf_type in (
+ CONF_INPUT_TYPE,
+ CONF_WRITE_TYPE,
+ CONF_COMMAND_ON,
+ CONF_COMMAND_OFF,
+ ):
+ if conf_type in entity:
+ addr += f"_{entity[conf_type]}"
+ inx = entity.get(CONF_SLAVE, None) or entity.get(CONF_DEVICE_ADDRESS, 0)
+ addr += f"_{inx}"
+ loc_addr: set[str] = {addr}
+
+ if CONF_TARGET_TEMP in entity:
+ loc_addr.add(f"{entity[CONF_TARGET_TEMP]}_{inx}")
+ if CONF_HVAC_MODE_REGISTER in entity:
+ loc_addr.add(f"{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
+ if CONF_FAN_MODE_REGISTER in entity:
+ loc_addr.add(f"{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
+
+ dup_addrs = ent_addr.intersection(loc_addr)
+ if len(dup_addrs) > 0:
+ for addr in dup_addrs:
+ err = (
+ f"Modbus {hub_name}/{name} address {addr} is duplicate, second"
+ " entry not loaded!"
+ )
+ _LOGGER.warning(err)
+ return False
+ if name in ent_names:
+ err = f"Modbus {hub_name}/{name} is duplicate, second entry not loaded!"
+ _LOGGER.warning(err)
+ return False
+ ent_names.add(name)
+ ent_addr.update(loc_addr)
+ return True
+
+ hub_inx = 0
+ while hub_inx < len(config):
+ hub = config[hub_inx]
+ if not validate_modbus(hub, hub_name_inx):
+ del config[hub_inx]
+ continue
+ for _component, conf_key in PLATFORMS:
+ if conf_key not in hub:
+ continue
+ entity_inx = 0
+ entities = hub[conf_key]
+ minimum_scan_interval = 9999
+ while entity_inx < len(entities):
+ if not validate_entity(
+ hub[CONF_NAME],
+ entities[entity_inx],
+ minimum_scan_interval,
+ ent_names,
+ ent_addr,
+ ):
+ del entities[entity_inx]
+ else:
+ entity_inx += 1
+
+ if hub[CONF_TIMEOUT] >= minimum_scan_interval:
+ hub[CONF_TIMEOUT] = minimum_scan_interval - 1
+ _LOGGER.warning(
+ "Modbus %s timeout is adjusted(%d) due to scan_interval",
+ hub[CONF_NAME],
+ hub[CONF_TIMEOUT],
+ )
+ hub_inx += 1
+ return config
diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py
index 23a39084f9f..063628d6d32 100644
--- a/homeassistant/components/moehlenhoff_alpha2/climate.py
+++ b/homeassistant/components/moehlenhoff_alpha2/climate.py
@@ -46,6 +46,7 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity):
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_preset_modes = [PRESET_AUTO, PRESET_DAY, PRESET_NIGHT]
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator: Alpha2BaseCoordinator, heat_area_id: str) -> None:
"""Initialize Alpha2 ClimateEntity."""
diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py
index 3df9db0d5d0..94311eeda61 100644
--- a/homeassistant/components/mqtt/climate.py
+++ b/homeassistant/components/mqtt/climate.py
@@ -610,6 +610,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
_attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED
_attr_target_temperature_low: float | None = None
_attr_target_temperature_high: float | None = None
+ _enable_turn_on_off_backwards_compatibility = False
@staticmethod
def config_schema() -> vol.Schema:
diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py
index d532135304a..0058fca021e 100644
--- a/homeassistant/components/mysensors/climate.py
+++ b/homeassistant/components/mysensors/climate.py
@@ -70,11 +70,12 @@ class MySensorsHVAC(mysensors.device.MySensorsChildEntity, ClimateEntity):
"""Representation of a MySensors HVAC."""
_attr_hvac_modes = OPERATION_LIST
+ _enable_turn_on_off_backwards_compatibility = False
@property
def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features."""
- features = ClimateEntityFeature(0)
+ features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SPEED in self._values:
features = features | ClimateEntityFeature.FAN_MODE
diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py
index 03fb79eb78e..2d0186b2bfd 100644
--- a/homeassistant/components/nest/climate.py
+++ b/homeassistant/components/nest/climate.py
@@ -100,6 +100,7 @@ class ThermostatEntity(ClimateEntity):
_attr_has_entity_name = True
_attr_should_poll = False
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, device: Device) -> None:
"""Initialize ThermostatEntity."""
@@ -246,7 +247,7 @@ class ThermostatEntity(ClimateEntity):
def _get_supported_features(self) -> ClimateEntityFeature:
"""Compute the bitmap of supported features from the current state."""
- features = ClimateEntityFeature(0)
+ features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
if HVACMode.HEAT_COOL in self.hvac_modes:
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
if HVACMode.HEAT in self.hvac_modes or HVACMode.COOL in self.hvac_modes:
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index 721e453e834..db12efb2f01 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -190,6 +190,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity):
_attr_supported_features = SUPPORT_FLAGS
_attr_target_temperature_step = PRECISION_HALVES
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, netatmo_device: NetatmoRoom) -> None:
"""Initialize the sensor."""
diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py
index 32ac8b5320a..63caeb445b7 100644
--- a/homeassistant/components/nexia/climate.py
+++ b/homeassistant/components/nexia/climate.py
@@ -153,6 +153,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
"""Provides Nexia Climate support."""
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone
diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py
index 38a3a5f825c..3a89f4f6022 100644
--- a/homeassistant/components/nibe_heatpump/climate.py
+++ b/homeassistant/components/nibe_heatpump/climate.py
@@ -72,6 +72,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity):
_attr_target_temperature_step = 0.5
_attr_max_temp = 35.0
_attr_min_temp = 5.0
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py
index 7041d097f3e..ca8ee08885d 100644
--- a/homeassistant/components/nobo_hub/climate.py
+++ b/homeassistant/components/nobo_hub/climate.py
@@ -81,6 +81,7 @@ class NoboZone(ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 1
# Need to poll to get preset change when in HVACMode.AUTO, so can't set _attr_should_poll = False
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, zone_id, hub: nobo, override_type) -> None:
"""Initialize the climate device."""
diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py
index 13a46c0b32f..b2ebbfa8485 100644
--- a/homeassistant/components/nuheat/climate.py
+++ b/homeassistant/components/nuheat/climate.py
@@ -78,6 +78,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity):
_attr_has_entity_name = True
_attr_name = None
_attr_preset_modes = PRESET_MODES
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, thermostat, temperature_unit):
"""Initialize the thermostat."""
diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py
index 50ba6c964f3..1a96078c003 100644
--- a/homeassistant/components/octoprint/__init__.py
+++ b/homeassistant/components/octoprint/__init__.py
@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
+from typing import cast
import aiohttp
from pyoctoprintapi import OctoprintClient
@@ -11,24 +12,28 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_BINARY_SENSORS,
+ CONF_DEVICE_ID,
CONF_HOST,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_PATH,
CONF_PORT,
+ CONF_PROFILE_NAME,
CONF_SENSORS,
CONF_SSL,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
-from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
+from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.config_validation as cv
+import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify as util_slugify
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
-from .const import DOMAIN
+from .const import CONF_BAUDRATE, DOMAIN, SERVICE_CONNECT
from .coordinator import OctoprintDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -122,6 +127,15 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
+SERVICE_CONNECT_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_DEVICE_ID): cv.string,
+ vol.Optional(CONF_PROFILE_NAME): cv.string,
+ vol.Optional(CONF_PORT): cv.string,
+ vol.Optional(CONF_BAUDRATE): cv.positive_int,
+ }
+)
+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the OctoPrint component."""
@@ -194,6 +208,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ async def async_printer_connect(call: ServiceCall) -> None:
+ """Connect to a printer."""
+ client = async_get_client_for_service_call(hass, call)
+ await client.connect(
+ printer_profile=call.data.get(CONF_PROFILE_NAME),
+ port=call.data.get(CONF_PORT),
+ baud_rate=call.data.get(CONF_BAUDRATE),
+ )
+
+ if not hass.services.has_service(DOMAIN, SERVICE_CONNECT):
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_CONNECT,
+ async_printer_connect,
+ schema=SERVICE_CONNECT_SCHEMA,
+ )
+
return True
@@ -205,3 +236,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
+
+
+def async_get_client_for_service_call(
+ hass: HomeAssistant, call: ServiceCall
+) -> OctoprintClient:
+ """Get the client related to a service call (by device ID)."""
+ device_id = call.data[CONF_DEVICE_ID]
+ device_registry = dr.async_get(hass)
+
+ if device_entry := device_registry.async_get(device_id):
+ for entry_id in device_entry.config_entries:
+ if data := hass.data[DOMAIN].get(entry_id):
+ return cast(OctoprintClient, data["client"])
+
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="missing_client",
+ translation_placeholders={
+ "device_id": device_id,
+ },
+ )
diff --git a/homeassistant/components/octoprint/const.py b/homeassistant/components/octoprint/const.py
index df22cb8d8f8..2d2a9e4a907 100644
--- a/homeassistant/components/octoprint/const.py
+++ b/homeassistant/components/octoprint/const.py
@@ -3,3 +3,6 @@
DOMAIN = "octoprint"
DEFAULT_NAME = "OctoPrint"
+
+SERVICE_CONNECT = "printer_connect"
+CONF_BAUDRATE = "baudrate"
diff --git a/homeassistant/components/octoprint/services.yaml b/homeassistant/components/octoprint/services.yaml
new file mode 100644
index 00000000000..2cb4a6f3c2d
--- /dev/null
+++ b/homeassistant/components/octoprint/services.yaml
@@ -0,0 +1,27 @@
+printer_connect:
+ fields:
+ device_id:
+ required: true
+ selector:
+ device:
+ integration: octoprint
+ profile_name:
+ required: false
+ selector:
+ text:
+ port:
+ required: false
+ selector:
+ text:
+ baudrate:
+ required: false
+ selector:
+ select:
+ options:
+ - "9600"
+ - "19200"
+ - "38400"
+ - "57600"
+ - "115200"
+ - "230400"
+ - "250000"
diff --git a/homeassistant/components/octoprint/strings.json b/homeassistant/components/octoprint/strings.json
index 63d9753ee1d..e9df0ed755c 100644
--- a/homeassistant/components/octoprint/strings.json
+++ b/homeassistant/components/octoprint/strings.json
@@ -35,5 +35,34 @@
"progress": {
"get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'."
}
+ },
+ "exceptions": {
+ "missing_client": {
+ "message": "No client for device ID: {device_id}"
+ }
+ },
+ "services": {
+ "printer_connect": {
+ "name": "Connect to a printer",
+ "description": "Instructs the octoprint server to connect to a printer.",
+ "fields": {
+ "device_id": {
+ "name": "Server",
+ "description": "The server that should connect."
+ },
+ "profile_name": {
+ "name": "Profile name",
+ "description": "Printer profile to connect with."
+ },
+ "port": {
+ "name": "Serial port",
+ "description": "Port name to connect on."
+ },
+ "baudrate": {
+ "name": "Baudrate",
+ "description": "Baud rate."
+ }
+ }
+ }
}
}
diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py
index 1b600b25d94..86c770ec82d 100644
--- a/homeassistant/components/oem/climate.py
+++ b/homeassistant/components/oem/climate.py
@@ -66,8 +66,13 @@ class ThermostatDevice(ClimateEntity):
"""Interface class for the oemthermostat module."""
_attr_hvac_modes = SUPPORT_HVAC
- _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _attr_supported_features = (
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
+ )
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, thermostat, name):
"""Initialize the device."""
diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py
index 2840cde704b..e7e30588f8a 100644
--- a/homeassistant/components/onewire/binary_sensor.py
+++ b/homeassistant/components/onewire/binary_sensor.py
@@ -38,7 +38,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
key=f"sensed.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"sensed_{id.lower()}",
+ translation_key="sensed_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_A_B
),
@@ -47,7 +48,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
key=f"sensed.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"sensed_{id}",
+ translation_key="sensed_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_7
),
@@ -56,7 +58,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
key=f"sensed.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"sensed_{id.lower()}",
+ translation_key="sensed_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_A_B
),
@@ -72,7 +75,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.PROBLEM,
- translation_key=f"hub_short_{id}",
+ translation_key="hub_short_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_3
),
diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py
index cc8b14b5d6e..a7d199c21a9 100644
--- a/homeassistant/components/onewire/sensor.py
+++ b/homeassistant/components/onewire/sensor.py
@@ -236,7 +236,8 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
native_unit_of_measurement="count",
read_mode=READ_MODE_INT,
state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key=f"counter_{id.lower()}",
+ translation_key="counter_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_A_B
),
@@ -276,7 +277,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
native_unit_of_measurement=UnitOfPressure.CBAR,
read_mode=READ_MODE_FLOAT,
state_class=SensorStateClass.MEASUREMENT,
- translation_key=f"moisture_{id}",
+ translation_key="moisture_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_3
),
@@ -396,7 +398,8 @@ def get_entities(
description,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
- translation_key=f"wetness_{s_id}",
+ translation_key="wetness_id",
+ translation_placeholders={"id": s_id},
)
override_key = None
if description.override_key:
diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json
index 753f244cfe9..8dbcbdf8978 100644
--- a/homeassistant/components/onewire/strings.json
+++ b/homeassistant/components/onewire/strings.json
@@ -21,55 +21,16 @@
},
"entity": {
"binary_sensor": {
- "sensed_a": {
- "name": "Sensed A"
+ "sensed_id": {
+ "name": "Sensed {id}"
},
- "sensed_b": {
- "name": "Sensed B"
- },
- "sensed_0": {
- "name": "Sensed 0"
- },
- "sensed_1": {
- "name": "Sensed 1"
- },
- "sensed_2": {
- "name": "Sensed 2"
- },
- "sensed_3": {
- "name": "Sensed 3"
- },
- "sensed_4": {
- "name": "Sensed 4"
- },
- "sensed_5": {
- "name": "Sensed 5"
- },
- "sensed_6": {
- "name": "Sensed 6"
- },
- "sensed_7": {
- "name": "Sensed 7"
- },
- "hub_short_0": {
- "name": "Hub short on branch 0"
- },
- "hub_short_1": {
- "name": "Hub short on branch 1"
- },
- "hub_short_2": {
- "name": "Hub short on branch 2"
- },
- "hub_short_3": {
- "name": "Hub short on branch 3"
+ "hub_short_id": {
+ "name": "Hub short on branch {id}"
}
},
"sensor": {
- "counter_a": {
- "name": "Counter A"
- },
- "counter_b": {
- "name": "Counter B"
+ "counter_id": {
+ "name": "Counter {id}"
},
"humidity_hih3600": {
"name": "HIH3600 humidity"
@@ -86,17 +47,8 @@
"humidity_raw": {
"name": "Raw humidity"
},
- "moisture_1": {
- "name": "Moisture 1"
- },
- "moisture_2": {
- "name": "Moisture 2"
- },
- "moisture_3": {
- "name": "Moisture 3"
- },
- "moisture_4": {
- "name": "Moisture 4"
+ "moisture_id": {
+ "name": "Moisture {id}"
},
"thermocouple_temperature_k": {
"name": "Thermocouple K temperature"
@@ -113,121 +65,31 @@
"voltage_vis_gradient": {
"name": "VIS voltage gradient"
},
- "wetness_0": {
- "name": "Wetness 0"
- },
- "wetness_1": {
- "name": "Wetness 1"
- },
- "wetness_2": {
- "name": "Wetness 2"
- },
- "wetness_3": {
- "name": "Wetness 3"
+ "wetness_id": {
+ "name": "Wetness {id}"
}
},
"switch": {
- "hub_branch_0": {
- "name": "Hub branch 0"
- },
- "hub_branch_1": {
- "name": "Hub branch 1"
- },
- "hub_branch_2": {
- "name": "Hub branch 2"
- },
- "hub_branch_3": {
- "name": "Hub branch 3"
+ "hub_branch_id": {
+ "name": "Hub branch {id}"
},
"iad": {
"name": "Current A/D control"
},
- "latch_0": {
- "name": "Latch 0"
+ "latch_id": {
+ "name": "Latch {id}"
},
- "latch_1": {
- "name": "Latch 1"
+ "leaf_sensor_id": {
+ "name": "Leaf sensor {id}"
},
- "latch_2": {
- "name": "Latch 2"
- },
- "latch_3": {
- "name": "Latch 3"
- },
- "latch_4": {
- "name": "Latch 4"
- },
- "latch_5": {
- "name": "Latch 5"
- },
- "latch_6": {
- "name": "Latch 6"
- },
- "latch_7": {
- "name": "Latch 7"
- },
- "latch_a": {
- "name": "Latch A"
- },
- "latch_b": {
- "name": "Latch B"
- },
- "leaf_sensor_0": {
- "name": "Leaf sensor 0"
- },
- "leaf_sensor_1": {
- "name": "Leaf sensor 1"
- },
- "leaf_sensor_2": {
- "name": "Leaf sensor 2"
- },
- "leaf_sensor_3": {
- "name": "Leaf sensor 3"
- },
- "moisture_sensor_0": {
- "name": "Moisture sensor 0"
- },
- "moisture_sensor_1": {
- "name": "Moisture sensor 1"
- },
- "moisture_sensor_2": {
- "name": "Moisture sensor 2"
- },
- "moisture_sensor_3": {
- "name": "Moisture sensor 3"
+ "moisture_sensor_id": {
+ "name": "Moisture sensor {id}"
},
"pio": {
"name": "Programmed input-output"
},
- "pio_0": {
- "name": "Programmed input-output 0"
- },
- "pio_1": {
- "name": "Programmed input-output 1"
- },
- "pio_2": {
- "name": "Programmed input-output 2"
- },
- "pio_3": {
- "name": "Programmed input-output 3"
- },
- "pio_4": {
- "name": "Programmed input-output 4"
- },
- "pio_5": {
- "name": "Programmed input-output 5"
- },
- "pio_6": {
- "name": "Programmed input-output 6"
- },
- "pio_7": {
- "name": "Programmed input-output 7"
- },
- "pio_a": {
- "name": "Programmed input-output A"
- },
- "pio_b": {
- "name": "Programmed input-output B"
+ "pio_id": {
+ "name": "Programmed input-output {id}"
}
}
},
diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py
index db9e8f5b0f8..00a3f8f65f4 100644
--- a/homeassistant/components/onewire/switch.py
+++ b/homeassistant/components/onewire/switch.py
@@ -42,7 +42,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key=f"PIO.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"pio_{id.lower()}",
+ translation_key="pio_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_A_B
]
@@ -51,7 +52,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key=f"latch.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"latch_{id.lower()}",
+ translation_key="latch_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_A_B
]
@@ -71,7 +73,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key=f"PIO.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"pio_{id}",
+ translation_key="pio_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_7
]
@@ -80,7 +83,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key=f"latch.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"latch_{id}",
+ translation_key="latch_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_7
]
@@ -90,7 +94,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
key=f"PIO.{id}",
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
- translation_key=f"pio_{id.lower()}",
+ translation_key="pio_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_A_B
),
@@ -106,7 +111,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
- translation_key=f"hub_branch_{id}",
+ translation_key="hub_branch_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_3
),
@@ -117,7 +123,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
- translation_key=f"leaf_sensor_{id}",
+ translation_key="leaf_sensor_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_3
]
@@ -127,7 +134,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
entity_registry_enabled_default=False,
read_mode=READ_MODE_BOOL,
entity_category=EntityCategory.CONFIG,
- translation_key=f"moisture_sensor_{id}",
+ translation_key="moisture_sensor_id",
+ translation_placeholders={"id": str(id)},
)
for id in DEVICE_KEYS_0_3
]
diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py
index bcad621eb82..0b9cd1862be 100644
--- a/homeassistant/components/opentherm_gw/climate.py
+++ b/homeassistant/components/opentherm_gw/climate.py
@@ -84,6 +84,7 @@ class OpenThermClimate(ClimateEntity):
_away_state_a = False
_away_state_b = False
_current_operation: HVACAction | None = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, gw_dev, options):
"""Initialize the device."""
diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py
index 867e977276d..2678986574d 100644
--- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py
+++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py
@@ -53,6 +53,7 @@ class AtlanticElectricalHeater(OverkizEntity, ClimateEntity):
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
@property
def hvac_mode(self) -> HVACMode:
diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py
index 14237b4601b..36e958fb49c 100644
--- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py
+++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py
@@ -75,6 +75,7 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
| ClimateEntityFeature.TURN_ON
)
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py
index b053611de9b..fefaa75a114 100644
--- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py
+++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py
@@ -45,6 +45,7 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity):
_attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py
index 115a30a7c36..5876f7df4a7 100644
--- a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py
+++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py
@@ -54,6 +54,7 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity):
| ClimateEntityFeature.TURN_ON
)
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py
index 90bc3e40404..25dab7c1d7e 100644
--- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py
+++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py
@@ -83,6 +83,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py
index 1ef0f9bf400..fe9f20b05fc 100644
--- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py
+++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py
@@ -30,6 +30,7 @@ class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity):
_attr_supported_features = (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
+ _enable_turn_on_off_backwards_compatibility = False
@property
def hvac_mode(self) -> HVACMode:
diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py
index 162b9b4fce6..9b956acd014 100644
--- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py
+++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py
@@ -90,6 +90,7 @@ class HitachiAirToAirHeatPumpHLRRWIFI(OverkizEntity, ClimateEntity):
_attr_target_temperature_step = 1.0
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py
index cc470dee032..f98865456e1 100644
--- a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py
+++ b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py
@@ -81,6 +81,7 @@ class SomfyHeatingTemperatureInterface(OverkizEntity, ClimateEntity):
# Both min and max temp values have been retrieved from the Somfy Application.
_attr_min_temp = 15.0
_attr_max_temp = 26.0
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py
index 9a81b6d5bd3..2b6840b463d 100644
--- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py
+++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py
@@ -64,6 +64,7 @@ class SomfyThermostat(OverkizEntity, ClimateEntity):
_attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ]
_attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
# Both min and max temp values have been retrieved from the Somfy Application.
_attr_min_temp = 15.0
diff --git a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py
index b58c29a6121..79c360a5f93 100644
--- a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py
+++ b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py
@@ -58,6 +58,7 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity):
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py
index 24aeb9e4f4e..9e17cd32e9c 100644
--- a/homeassistant/components/powerwall/sensor.py
+++ b/homeassistant/components/powerwall/sensor.py
@@ -113,12 +113,6 @@ POWERWALL_INSTANT_SENSORS = (
)
-def _get_battery_charge(battery_data: BatteryResponse) -> float:
- """Get the current value in %."""
- ratio = float(battery_data.energy_remaining) / float(battery_data.capacity)
- return round(100 * ratio, 1)
-
-
BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [
PowerwallSensorEntityDescription[BatteryResponse, int](
key="battery_capacity",
@@ -202,16 +196,6 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [
suggested_display_precision=1,
value_fn=lambda battery_data: battery_data.energy_remaining,
),
- PowerwallSensorEntityDescription[BatteryResponse, float](
- key="charge",
- translation_key="charge",
- entity_category=EntityCategory.DIAGNOSTIC,
- state_class=SensorStateClass.MEASUREMENT,
- device_class=SensorDeviceClass.BATTERY,
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=_get_battery_charge,
- ),
PowerwallSensorEntityDescription[BatteryResponse, str](
key="grid_state",
translation_key="grid_state",
diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py
index 5f841441d59..797fd751197 100644
--- a/homeassistant/components/proliphix/climate.py
+++ b/homeassistant/components/proliphix/climate.py
@@ -60,6 +60,7 @@ class ProliphixThermostat(ClimateEntity):
_attr_precision = PRECISION_TENTHS
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, pdp):
"""Initialize the thermostat."""
diff --git a/homeassistant/components/proximity/diagnostics.py b/homeassistant/components/proximity/diagnostics.py
new file mode 100644
index 00000000000..ba5e1f53722
--- /dev/null
+++ b/homeassistant/components/proximity/diagnostics.py
@@ -0,0 +1,49 @@
+"""Diagnostics support for Proximity."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.components.device_tracker import ATTR_GPS, ATTR_IP, ATTR_MAC
+from homeassistant.components.diagnostics import async_redact_data
+from homeassistant.components.person import ATTR_USER_ID
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN
+from .coordinator import ProximityDataUpdateCoordinator
+
+TO_REDACT = {
+ ATTR_GPS,
+ ATTR_IP,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ ATTR_MAC,
+ ATTR_USER_ID,
+ "context",
+}
+
+
+async def async_get_config_entry_diagnostics(
+ hass: HomeAssistant, entry: ConfigEntry
+) -> dict[str, Any]:
+ """Return diagnostics for a config entry."""
+ coordinator: ProximityDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+ diag_data = {
+ "entry": entry.as_dict(),
+ }
+
+ tracked_states: dict[str, dict] = {}
+ for tracked_entity_id in coordinator.tracked_entities:
+ if (state := hass.states.get(tracked_entity_id)) is None:
+ continue
+ tracked_states[tracked_entity_id] = state.as_dict()
+
+ diag_data["data"] = {
+ "proximity": coordinator.data.proximity,
+ "entities": coordinator.data.entities,
+ "entity_mapping": coordinator.entity_mapping,
+ "tracked_states": async_redact_data(tracked_states, TO_REDACT),
+ }
+ return diag_data
diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py
index f5ea14e8f4e..4ab57fd6821 100644
--- a/homeassistant/components/radiotherm/climate.py
+++ b/homeassistant/components/radiotherm/climate.py
@@ -106,6 +106,7 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
_attr_precision = PRECISION_HALVES
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None:
"""Initialize the thermostat."""
@@ -113,7 +114,10 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity):
self._attr_unique_id = self.init_data.mac
self._attr_fan_modes = CT30_FAN_OPERATION_LIST
self._attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.FAN_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
if not isinstance(self.device, radiotherm.thermostat.CT80):
return
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index 8dd539f84f3..0b63bb8daa2 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -794,4 +794,6 @@ def purge_entity_data(
_LOGGER.debug("Purging entity data hasn't fully completed yet")
return False
+ _purge_old_entity_ids(instance, session)
+
return True
diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py
index c8c0d76690d..5d747c8f345 100644
--- a/homeassistant/components/schluter/climate.py
+++ b/homeassistant/components/schluter/climate.py
@@ -81,6 +81,7 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity):
_attr_hvac_modes = [HVACMode.HEAT]
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, serial_number, api, session_id):
"""Initialize the thermostat."""
diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py
index 7cdfbba10c0..6d95f06a49c 100644
--- a/homeassistant/components/screenlogic/climate.py
+++ b/homeassistant/components/screenlogic/climate.py
@@ -81,8 +81,12 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity):
entity_description: ScreenLogicClimateDescription
_attr_hvac_modes = SUPPORTED_MODES
_attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, entity_description) -> None:
"""Initialize a ScreenLogic climate entity."""
diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py
index a718cac88fb..bcc851e02ae 100644
--- a/homeassistant/components/sensibo/climate.py
+++ b/homeassistant/components/sensibo/climate.py
@@ -191,6 +191,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
_attr_name = None
_attr_precision = PRECISION_TENTHS
_attr_translation_key = "climate_device"
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, coordinator: SensiboDataUpdateCoordinator, device_id: str
diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index 3c3eaeb78e3..e0a2d5f75c4 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sentry",
"integration_type": "service",
"iot_class": "cloud_polling",
- "requirements": ["sentry-sdk==1.39.2"]
+ "requirements": ["sentry-sdk==1.40.0"]
}
diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py
index a94941ac642..c921e1ac1da 100644
--- a/homeassistant/components/senz/climate.py
+++ b/homeassistant/components/senz/climate.py
@@ -45,6 +45,7 @@ class SENZClimate(CoordinatorEntity, ClimateEntity):
_attr_min_temp = 5
_attr_has_entity_name = True
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py
index 9c43c0b57b8..59343ca6d2f 100644
--- a/homeassistant/components/shelly/climate.py
+++ b/homeassistant/components/shelly/climate.py
@@ -167,6 +167,7 @@ class BlockSleepingClimate(
)
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
@@ -448,6 +449,7 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity):
)
_attr_target_temperature_step = RPC_THERMOSTAT_SETTINGS["step"]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
"""Initialize."""
diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py
index 656a198f42b..4c2afa45b7f 100644
--- a/homeassistant/components/smartthings/climate.py
+++ b/homeassistant/components/smartthings/climate.py
@@ -162,6 +162,8 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
"""Define a SmartThings climate entities."""
+ _enable_turn_on_off_backwards_compatibility = False
+
def __init__(self, device):
"""Init the class."""
super().__init__(device)
@@ -343,6 +345,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
"""Define a SmartThings Air Conditioner."""
_hvac_modes: list[HVACMode]
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, device) -> None:
"""Init the class."""
diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py
index 9f1802e7327..4921fca022d 100644
--- a/homeassistant/components/smarttub/climate.py
+++ b/homeassistant/components/smarttub/climate.py
@@ -67,6 +67,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity):
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_preset_modes = list(PRESET_MODES.values())
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, spa):
"""Initialize the entity."""
diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py
index 90cadcdad37..05b69c54c50 100644
--- a/homeassistant/components/sonos/entity.py
+++ b/homeassistant/components/sonos/entity.py
@@ -76,6 +76,10 @@ class SonosEntity(Entity):
@property
def device_info(self) -> DeviceInfo:
"""Return information about the device."""
+ suggested_area: str | None = None
+ if not self.speaker.battery_info:
+ # Only set suggested area for non-portable devices
+ suggested_area = self.speaker.zone_name
return DeviceInfo(
identifiers={(DOMAIN, self.soco.uid)},
name=self.speaker.zone_name,
@@ -86,7 +90,7 @@ class SonosEntity(Entity):
(dr.CONNECTION_UPNP, f"uuid:{self.speaker.uid}"),
},
manufacturer="Sonos",
- suggested_area=self.speaker.zone_name,
+ suggested_area=suggested_area,
configuration_url=f"http://{self.soco.ip_address}:1400/support/review",
)
diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py
index 88cce6c52d7..cedd1b3dd90 100644
--- a/homeassistant/components/stiebel_eltron/climate.py
+++ b/homeassistant/components/stiebel_eltron/climate.py
@@ -73,9 +73,13 @@ class StiebelEltron(ClimateEntity):
_attr_hvac_modes = SUPPORT_HVAC
_attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, name, ste_data):
"""Initialize the unit."""
diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py
index a510b5b7414..d87b711e376 100644
--- a/homeassistant/components/swiss_public_transport/__init__.py
+++ b/homeassistant/components/swiss_public_transport/__init__.py
@@ -89,7 +89,9 @@ async def async_migrate_entry(
device_registry, config_entry_id=config_entry.entry_id
)
for dev in device_entries:
- device_registry.async_remove_device(dev.id)
+ device_registry.async_update_device(
+ dev.id, remove_config_entry_id=config_entry.entry_id
+ )
entity_id = entity_registry.async_get_entity_id(
Platform.SENSOR, DOMAIN, "None_departure"
@@ -105,12 +107,13 @@ async def async_migrate_entry(
)
# Set a valid unique id for config entries
- config_entry.unique_id = new_unique_id
config_entry.minor_version = 2
- hass.config_entries.async_update_entry(config_entry)
+ hass.config_entries.async_update_entry(config_entry, unique_id=new_unique_id)
_LOGGER.debug(
- "Migration to minor version %s successful", config_entry.minor_version
+ "Migration to version %s.%s successful",
+ config_entry.version,
+ config_entry.minor_version,
)
return True
diff --git a/homeassistant/components/switchbot_cloud/climate.py b/homeassistant/components/switchbot_cloud/climate.py
index 803669c806d..d184063939a 100644
--- a/homeassistant/components/switchbot_cloud/climate.py
+++ b/homeassistant/components/switchbot_cloud/climate.py
@@ -80,6 +80,7 @@ class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature = 21
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
async def _do_send_command(
self,
diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py
index 1193638c10e..dd0d6a22a08 100644
--- a/homeassistant/components/tado/climate.py
+++ b/homeassistant/components/tado/climate.py
@@ -131,7 +131,10 @@ def create_climate_entity(tado, name: str, zone_id: int, device_info: dict):
zone_type = capabilities["type"]
support_flags = (
- ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
+ ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
supported_hvac_modes = [
TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF],
@@ -221,6 +224,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
_attr_name = None
_attr_translation_key = DOMAIN
_available = False
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/tankerkoenig/diagnostics.py b/homeassistant/components/tankerkoenig/diagnostics.py
index 811ec07ef19..d5fd7c8cada 100644
--- a/homeassistant/components/tankerkoenig/diagnostics.py
+++ b/homeassistant/components/tankerkoenig/diagnostics.py
@@ -1,6 +1,7 @@
"""Diagnostics support for Tankerkoenig."""
from __future__ import annotations
+from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
@@ -27,6 +28,9 @@ async def async_get_config_entry_diagnostics(
diag_data = {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
- "data": coordinator.data,
+ "data": {
+ station_id: asdict(price_info)
+ for station_id, price_info in coordinator.data.items()
+ },
}
return diag_data
diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json
index bf8896196ef..adea5b96490 100644
--- a/homeassistant/components/tankerkoenig/manifest.json
+++ b/homeassistant/components/tankerkoenig/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tankerkoenig",
"iot_class": "cloud_polling",
"loggers": ["aiotankerkoenig"],
- "requirements": ["aiotankerkoenig==0.2.0"]
+ "requirements": ["aiotankerkoenig==0.3.0"]
}
diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json
index 19ad9e5ddeb..79cd0289724 100644
--- a/homeassistant/components/template/strings.json
+++ b/homeassistant/components/template/strings.json
@@ -75,6 +75,7 @@
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
+ "tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]",
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
@@ -127,6 +128,7 @@
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
+ "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py
index 09933d628fe..67d3d4ba22e 100644
--- a/homeassistant/components/tesla_wall_connector/sensor.py
+++ b/homeassistant/components/tesla_wall_connector/sensor.py
@@ -153,7 +153,9 @@ WALL_CONNECTOR_SENSORS = [
key="session_energy_wh",
translation_key="session_energy_wh",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
+ suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].session_energy_wh,
+ device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.MEASUREMENT,
),
WallConnectorSensorDescription(
diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py
index b626d3ef759..748acbb8552 100644
--- a/homeassistant/components/teslemetry/climate.py
+++ b/homeassistant/components/teslemetry/climate.py
@@ -45,6 +45,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
| ClimateEntityFeature.PRESET_MODE
)
_attr_preset_modes = ["off", "keep", "dog", "camp"]
+ _enable_turn_on_off_backwards_compatibility = False
@property
def hvac_mode(self) -> HVACMode | None:
diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py
index 594098cddfe..ff0cf661475 100644
--- a/homeassistant/components/tessie/binary_sensor.py
+++ b/homeassistant/components/tessie/binary_sensor.py
@@ -41,6 +41,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
key="charge_state_charging_state",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
is_on=lambda x: x == "Charging",
+ entity_registry_enabled_default=False,
),
TessieBinarySensorEntityDescription(
key="charge_state_preconditioning_enabled",
@@ -62,17 +63,14 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
),
TessieBinarySensorEntityDescription(
key="climate_state_auto_seat_climate_left",
- device_class=BinarySensorDeviceClass.HEAT,
entity_category=EntityCategory.DIAGNOSTIC,
),
TessieBinarySensorEntityDescription(
key="climate_state_auto_seat_climate_right",
- device_class=BinarySensorDeviceClass.HEAT,
entity_category=EntityCategory.DIAGNOSTIC,
),
TessieBinarySensorEntityDescription(
key="climate_state_auto_steering_wheel_heat",
- device_class=BinarySensorDeviceClass.HEAT,
entity_category=EntityCategory.DIAGNOSTIC,
),
TessieBinarySensorEntityDescription(
diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py
index d143771ee2c..8eb69d619ff 100644
--- a/homeassistant/components/tessie/climate.py
+++ b/homeassistant/components/tessie/climate.py
@@ -56,6 +56,7 @@ class TessieClimateEntity(TessieEntity, ClimateEntity):
TessieClimateKeeper.DOG,
TessieClimateKeeper.CAMP,
]
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py
index 591d4652274..8ec063bf47c 100644
--- a/homeassistant/components/tessie/const.py
+++ b/homeassistant/components/tessie/const.py
@@ -68,3 +68,13 @@ class TessieChargeCableLockStates(StrEnum):
ENGAGED = "Engaged"
DISENGAGED = "Disengaged"
+
+
+TessieChargeStates = {
+ "Starting": "starting",
+ "Charging": "charging",
+ "Stopped": "stopped",
+ "Complete": "complete",
+ "Disconnected": "disconnected",
+ "NoPower": "no_power",
+}
diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json
new file mode 100644
index 00000000000..caf0524f2e8
--- /dev/null
+++ b/homeassistant/components/tessie/icons.json
@@ -0,0 +1,23 @@
+{
+ "entity": {
+ "sensor": {
+ "drive_state_shift_state": {
+ "default": "mdi:car-shift-pattern"
+ },
+ "drive_state_active_route_destination": {
+ "default": "mdi:map-marker"
+ }
+ },
+ "binary_sensor": {
+ "climate_state_auto_seat_climate_left": {
+ "default": "mdi:car-seat-heater"
+ },
+ "climate_state_auto_seat_climate_right": {
+ "default": "mdi:car-seat-heater"
+ },
+ "climate_state_auto_steering_wheel_heat": {
+ "default": "mdi:steering"
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py
index ae9e06b2b35..7c126754fb5 100644
--- a/homeassistant/components/tessie/sensor.py
+++ b/homeassistant/components/tessie/sensor.py
@@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from homeassistant.util.variance import ignore_variance
-from .const import DOMAIN
+from .const import DOMAIN, TessieChargeStates
from .coordinator import TessieStateUpdateCoordinator
from .entity import TessieEntity
@@ -54,6 +54,13 @@ class TessieSensorEntityDescription(SensorEntityDescription):
DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
+ TessieSensorEntityDescription(
+ key="charge_state_charging_state",
+ icon="mdi:ev-station",
+ options=list(TessieChargeStates.values()),
+ device_class=SensorDeviceClass.ENUM,
+ value_fn=lambda value: TessieChargeStates[cast(str, value)],
+ ),
TessieSensorEntityDescription(
key="charge_state_usable_battery_level",
state_class=SensorStateClass.MEASUREMENT,
@@ -122,7 +129,6 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
),
TessieSensorEntityDescription(
key="drive_state_shift_state",
- icon="mdi:car-shift-pattern",
options=["p", "d", "r", "n"],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda x: x.lower() if isinstance(x, str) else x,
@@ -231,7 +237,6 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
),
TessieSensorEntityDescription(
key="drive_state_active_route_destination",
- icon="mdi:map-marker",
entity_category=EntityCategory.DIAGNOSTIC,
),
)
diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json
index 8340557843d..01e6a654163 100644
--- a/homeassistant/components/tessie/strings.json
+++ b/homeassistant/components/tessie/strings.json
@@ -67,6 +67,17 @@
}
},
"sensor": {
+ "charge_state_charging_state": {
+ "name": "Charging",
+ "state": {
+ "starting": "Starting",
+ "charging": "Charging",
+ "disconnected": "Disconnected",
+ "stopped": "Stopped",
+ "complete": "Complete",
+ "no_power": "No power"
+ }
+ },
"charge_state_usable_battery_level": {
"name": "Battery level"
},
@@ -258,7 +269,7 @@
"climate_state_auto_seat_climate_right": {
"name": "Auto seat climate right"
},
- "climate_state_auto_steering_wheel_heater": {
+ "climate_state_auto_steering_wheel_heat": {
"name": "Auto steering wheel heater"
},
"climate_state_cabin_overheat_protection": {
diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py
index 2e764b5c637..7e5999b7f02 100644
--- a/homeassistant/components/tfiac/climate.py
+++ b/homeassistant/components/tfiac/climate.py
@@ -83,8 +83,11 @@ class TfiacClimate(ClimateEntity):
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, hass, client):
"""Init class."""
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index 467cd2bfd77..52e18c9c6a2 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -64,129 +64,129 @@ PARALLEL_UPDATES = 0
RT_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="averagePower",
- name="average power",
+ translation_key="average_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key="power",
- name="power",
+ translation_key="power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key="powerProduction",
- name="power production",
+ translation_key="power_production",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key="minPower",
- name="min power",
+ translation_key="min_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key="maxPower",
- name="max power",
+ translation_key="max_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key="accumulatedConsumption",
- name="accumulated consumption",
+ translation_key="accumulated_consumption",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="accumulatedConsumptionLastHour",
- name="accumulated consumption current hour",
+ translation_key="accumulated_consumption_last_hour",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="estimatedHourConsumption",
- name="Estimated consumption current hour",
+ translation_key="estimated_hour_consumption",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="accumulatedProduction",
- name="accumulated production",
+ translation_key="accumulated_production",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="accumulatedProductionLastHour",
- name="accumulated production current hour",
+ translation_key="accumulated_production_last_hour",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="lastMeterConsumption",
- name="last meter consumption",
+ translation_key="last_meter_consumption",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="lastMeterProduction",
- name="last meter production",
+ translation_key="last_meter_production",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key="voltagePhase1",
- name="voltage phase1",
+ translation_key="voltage_phase1",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="voltagePhase2",
- name="voltage phase2",
+ translation_key="voltage_phase2",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="voltagePhase3",
- name="voltage phase3",
+ translation_key="voltage_phase3",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="currentL1",
- name="current L1",
+ translation_key="current_l1",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="currentL2",
- name="current L2",
+ translation_key="current_l2",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="currentL3",
- name="current L3",
+ translation_key="current_l3",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="signalStrength",
- name="signal strength",
+ translation_key="signal_strength",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
state_class=SensorStateClass.MEASUREMENT,
@@ -194,19 +194,19 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = (
),
SensorEntityDescription(
key="accumulatedReward",
- name="accumulated reward",
+ translation_key="accumulated_reward",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="accumulatedCost",
- name="accumulated cost",
+ translation_key="accumulated_cost",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="powerFactor",
- name="power factor",
+ translation_key="power_factor",
device_class=SensorDeviceClass.POWER_FACTOR,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -216,23 +216,23 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = (
SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="month_cost",
- name="Monthly cost",
+ translation_key="month_cost",
device_class=SensorDeviceClass.MONETARY,
),
SensorEntityDescription(
key="peak_hour",
- name="Monthly peak hour consumption",
+ translation_key="peak_hour",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="peak_hour_time",
- name="Time of max hour consumption",
+ translation_key="peak_hour_time",
device_class=SensorDeviceClass.TIMESTAMP,
),
SensorEntityDescription(
key="month_cons",
- name="Monthly net consumption",
+ translation_key="month_cons",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
@@ -305,6 +305,8 @@ async def async_setup_entry(
class TibberSensor(SensorEntity):
"""Representation of a generic Tibber sensor."""
+ _attr_has_entity_name = True
+
def __init__(
self, *args: Any, tibber_home: tibber.TibberHome, **kwargs: Any
) -> None:
@@ -335,6 +337,9 @@ class TibberSensor(SensorEntity):
class TibberSensorElPrice(TibberSensor):
"""Representation of a Tibber sensor for el price."""
+ _attr_state_class = SensorStateClass.MEASUREMENT
+ _attr_translation_key = "electricity_price"
+
def __init__(self, tibber_home: tibber.TibberHome) -> None:
"""Initialize the sensor."""
super().__init__(tibber_home=tibber_home)
@@ -355,8 +360,6 @@ class TibberSensorElPrice(TibberSensor):
"off_peak_2": None,
}
self._attr_icon = ICON
- self._attr_name = f"Electricity price {self._home_name}"
- self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_unique_id = self._tibber_home.home_id
self._model = "Price Sensor"
@@ -424,7 +427,6 @@ class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"])
self._attr_unique_id = (
f"{self._tibber_home.home_id}_{self.entity_description.key}"
)
- self._attr_name = f"{entity_description.name} {self._home_name}"
if entity_description.key == "month_cost":
self._attr_native_unit_of_measurement = self._tibber_home.currency
@@ -452,7 +454,6 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"])
self._model = "Tibber Pulse"
self._device_name = f"{self._model} {self._home_name}"
- self._attr_name = f"{description.name} {self._home_name}"
self._attr_native_value = initial_state
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}"
diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json
index c7cef9f4657..af14c96674d 100644
--- a/homeassistant/components/tibber/strings.json
+++ b/homeassistant/components/tibber/strings.json
@@ -1,4 +1,89 @@
{
+ "entity": {
+ "sensor": {
+ "electricity_price": {
+ "name": "Electricity price"
+ },
+ "month_cost": {
+ "name": "Monthly cost"
+ },
+ "peak_hour": {
+ "name": "Monthly peak hour consumption"
+ },
+ "peak_hour_time": {
+ "name": "Time of max hour consumption"
+ },
+ "month_cons": {
+ "name": "Monthly net consumption"
+ },
+ "average_power": {
+ "name": "Average power"
+ },
+ "power": {
+ "name": "Power"
+ },
+ "power_production": {
+ "name": "Power production"
+ },
+ "min_power": {
+ "name": "Min power"
+ },
+ "max_power": {
+ "name": "Max power"
+ },
+ "accumulated_consumption": {
+ "name": "Accumulated consumption"
+ },
+ "accumulated_consumption_last_hour": {
+ "name": "Accumulated consumption current hour"
+ },
+ "estimated_hour_consumption": {
+ "name": "Estimated consumption current hour"
+ },
+ "accumulated_production": {
+ "name": "Accumulated production"
+ },
+ "accumulated_production_last_hour": {
+ "name": "Accumulated production current hour"
+ },
+ "last_meter_consumption": {
+ "name": "Last meter consumption"
+ },
+ "last_meter_production": {
+ "name": "Last meter production"
+ },
+ "voltage_phase1": {
+ "name": "Voltage phase1"
+ },
+ "voltage_phase2": {
+ "name": "Voltage phase2"
+ },
+ "voltage_phase3": {
+ "name": "Voltage phase3"
+ },
+ "current_l1": {
+ "name": "Current L1"
+ },
+ "current_l2": {
+ "name": "Current L2"
+ },
+ "current_l3": {
+ "name": "Current L3"
+ },
+ "signal_strength": {
+ "name": "Signal strength"
+ },
+ "accumulated_reward": {
+ "name": "Accumulated reward"
+ },
+ "accumulated_cost": {
+ "name": "Accumulated cost"
+ },
+ "power_factor": {
+ "name": "Power factor"
+ }
+ }
+ },
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py
index 05afce41ff3..033a4c5b51c 100644
--- a/homeassistant/components/tolo/climate.py
+++ b/homeassistant/components/tolo/climate.py
@@ -58,6 +58,7 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity):
)
_attr_target_temperature_step = 1
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry
diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py
index cc51bb03fec..16fbdbdd356 100644
--- a/homeassistant/components/toon/climate.py
+++ b/homeassistant/components/toon/climate.py
@@ -51,6 +51,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py
index ed3d4500db1..5004646a667 100644
--- a/homeassistant/components/touchline/climate.py
+++ b/homeassistant/components/touchline/climate.py
@@ -69,6 +69,7 @@ class Touchline(ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, touchline_thermostat):
"""Initialize the Touchline device."""
diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json
index 15748e83737..a91e7e5a46f 100644
--- a/homeassistant/components/tplink/manifest.json
+++ b/homeassistant/components/tplink/manifest.json
@@ -156,6 +156,10 @@
"hostname": "k[lps]*",
"macaddress": "54AF97*"
},
+ {
+ "hostname": "l[59]*",
+ "macaddress": "54AF97*"
+ },
{
"hostname": "k[lps]*",
"macaddress": "AC15A2*"
@@ -177,21 +181,41 @@
"macaddress": "5CE931*"
},
{
- "hostname": "l5*",
+ "hostname": "l[59]*",
"macaddress": "3C52A1*"
},
{
"hostname": "l5*",
"macaddress": "5C628B*"
},
+ {
+ "hostname": "tp*",
+ "macaddress": "5C628B*"
+ },
{
"hostname": "p1*",
"macaddress": "482254*"
},
+ {
+ "hostname": "s5*",
+ "macaddress": "482254*"
+ },
{
"hostname": "p1*",
"macaddress": "30DE4B*"
},
+ {
+ "hostname": "p1*",
+ "macaddress": "3C52A1*"
+ },
+ {
+ "hostname": "tp*",
+ "macaddress": "3C52A1*"
+ },
+ {
+ "hostname": "s5*",
+ "macaddress": "3C52A1*"
+ },
{
"hostname": "l9*",
"macaddress": "A842A1*"
@@ -199,11 +223,51 @@
{
"hostname": "l9*",
"macaddress": "3460F9*"
+ },
+ {
+ "hostname": "hs*",
+ "macaddress": "704F57*"
+ },
+ {
+ "hostname": "k[lps]*",
+ "macaddress": "74DA88*"
+ },
+ {
+ "hostname": "p3*",
+ "macaddress": "788CB5*"
+ },
+ {
+ "hostname": "p1*",
+ "macaddress": "CC32E5*"
+ },
+ {
+ "hostname": "k[lps]*",
+ "macaddress": "CC32E5*"
+ },
+ {
+ "hostname": "hs*",
+ "macaddress": "CC32E5*"
+ },
+ {
+ "hostname": "k[lps]*",
+ "macaddress": "D80D17*"
+ },
+ {
+ "hostname": "k[lps]*",
+ "macaddress": "D84732*"
+ },
+ {
+ "hostname": "p1*",
+ "macaddress": "F0A731*"
+ },
+ {
+ "hostname": "l9*",
+ "macaddress": "F0A731*"
}
],
"documentation": "https://www.home-assistant.io/integrations/tplink",
"iot_class": "local_polling",
"loggers": ["kasa"],
"quality_scale": "platinum",
- "requirements": ["python-kasa[speedups]==0.6.2"]
+ "requirements": ["python-kasa[speedups]==0.6.2.1"]
}
diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json
index 978a0b2f507..c3b9e540ab6 100644
--- a/homeassistant/components/traccar/manifest.json
+++ b/homeassistant/components/traccar/manifest.json
@@ -5,7 +5,7 @@
"config_flow": true,
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/traccar",
- "iot_class": "local_polling",
+ "iot_class": "cloud_push",
"loggers": ["pytraccar"],
"requirements": ["pytraccar==2.0.0", "stringcase==1.2.0"]
}
diff --git a/homeassistant/components/traccar_server/coordinator.py b/homeassistant/components/traccar_server/coordinator.py
index 337d0dcafbb..90c910e6062 100644
--- a/homeassistant/components/traccar_server/coordinator.py
+++ b/homeassistant/components/traccar_server/coordinator.py
@@ -93,10 +93,9 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
skip_accuracy_filter = False
for custom_attr in self.custom_attributes:
- attr[custom_attr] = getattr(
- device["attributes"],
+ attr[custom_attr] = device["attributes"].get(
custom_attr,
- getattr(position["attributes"], custom_attr, None),
+ position["attributes"].get(custom_attr, None),
)
if custom_attr in self.skip_accuracy_filter_for:
skip_accuracy_filter = True
@@ -151,13 +150,16 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
device = get_device(event["deviceId"], devices)
self.hass.bus.async_fire(
# This goes against two of the HA core guidelines:
- # 1. Event names should be prefixed with the domain name of the integration
+ # 1. Event names should be prefixed with the domain name of
+ # the integration
# 2. This should be event entities
- # However, to not break it for those who currently use the "old" integration, this is kept as is.
+ #
+ # However, to not break it for those who currently use
+ # the "old" integration, this is kept as is.
f"traccar_{EVENTS[event['type']]}",
{
"device_traccar_id": event["deviceId"],
- "device_name": getattr(device, "name", None),
+ "device_name": device["name"] if device else None,
"type": event["type"],
"serverTime": event["eventTime"],
"attributes": event["attributes"],
diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py
index 2abcc6398fb..226d942e465 100644
--- a/homeassistant/components/traccar_server/device_tracker.py
+++ b/homeassistant/components/traccar_server/device_tracker.py
@@ -51,12 +51,13 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device specific attributes."""
+ geofence_name = self.traccar_geofence["name"] if self.traccar_geofence else None
return {
**self.traccar_attributes,
ATTR_ADDRESS: self.traccar_position["address"],
ATTR_ALTITUDE: self.traccar_position["altitude"],
ATTR_CATEGORY: self.traccar_device["category"],
- ATTR_GEOFENCE: getattr(self.traccar_geofence, "name", None),
+ ATTR_GEOFENCE: geofence_name,
ATTR_MOTION: self.traccar_position["attributes"].get("motion", False),
ATTR_SPEED: self.traccar_position["speed"],
ATTR_STATUS: self.traccar_device["status"],
diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json
index c6ab0bab893..6ec89261b3d 100644
--- a/homeassistant/components/twinkly/manifest.json
+++ b/homeassistant/components/twinkly/manifest.json
@@ -6,6 +6,9 @@
"dhcp": [
{
"hostname": "twinkly_*"
+ },
+ {
+ "hostname": "twinkly-*"
}
],
"documentation": "https://www.home-assistant.io/integrations/twinkly",
diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py
index ecdddd19289..9afbfc683a8 100644
--- a/homeassistant/components/velbus/climate.py
+++ b/homeassistant/components/velbus/climate.py
@@ -41,6 +41,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity):
_attr_hvac_mode = HVACMode.HEAT
_attr_hvac_modes = [HVACMode.HEAT]
_attr_preset_modes = list(PRESET_MODES)
+ _enable_turn_on_off_backwards_compatibility = False
@property
def target_temperature(self) -> float | None:
diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py
index 6359cc19e57..a9ee56c4dbb 100644
--- a/homeassistant/components/venstar/climate.py
+++ b/homeassistant/components/venstar/climate.py
@@ -108,6 +108,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity):
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO]
_attr_precision = PRECISION_HALVES
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
@@ -130,6 +131,8 @@ class VenstarThermostat(VenstarEntity, ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
if self._client.mode == self._client.MODE_AUTO:
diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py
index 85c1851b20e..93d0fbf2aee 100644
--- a/homeassistant/components/vera/climate.py
+++ b/homeassistant/components/vera/climate.py
@@ -53,6 +53,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity):
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData
diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py
index cadb9b6788d..19a60602540 100644
--- a/homeassistant/components/verisure/binary_sensor.py
+++ b/homeassistant/components/verisure/binary_sensor.py
@@ -58,7 +58,6 @@ class VerisureDoorWindowSensor(
area = self.coordinator.data["door_window"][self.serial_number]["area"]
return DeviceInfo(
name=area,
- suggested_area=area,
manufacturer="Verisure",
model="Shock Sensor Detector",
identifiers={(DOMAIN, self.serial_number)},
diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py
index a240d45cf7e..e0505328245 100644
--- a/homeassistant/components/verisure/camera.py
+++ b/homeassistant/components/verisure/camera.py
@@ -71,7 +71,6 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera)
area = self.coordinator.data["cameras"][self.serial_number]["device"]["area"]
return DeviceInfo(
name=area,
- suggested_area=area,
manufacturer="Verisure",
model="SmartCam",
identifiers={(DOMAIN, self.serial_number)},
diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py
index 1a81b437116..8e57c9695c0 100644
--- a/homeassistant/components/verisure/lock.py
+++ b/homeassistant/components/verisure/lock.py
@@ -77,7 +77,6 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt
area = self.coordinator.data["locks"][self.serial_number]["device"]["area"]
return DeviceInfo(
name=area,
- suggested_area=area,
manufacturer="Verisure",
model="Lockguard Smartlock",
identifiers={(DOMAIN, self.serial_number)},
diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py
index 0fb16aa87c4..51947484dca 100644
--- a/homeassistant/components/verisure/sensor.py
+++ b/homeassistant/components/verisure/sensor.py
@@ -68,7 +68,6 @@ class VerisureThermometer(
area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
return DeviceInfo(
name=area,
- suggested_area=area,
manufacturer="Verisure",
model=DEVICE_TYPE_NAME.get(device_type, device_type),
identifiers={(DOMAIN, self.serial_number)},
@@ -119,7 +118,6 @@ class VerisureHygrometer(
area = self.coordinator.data["climate"][self.serial_number]["device"]["area"]
return DeviceInfo(
name=area,
- suggested_area=area,
manufacturer="Verisure",
model=DEVICE_TYPE_NAME.get(device_type, device_type),
identifiers={(DOMAIN, self.serial_number)},
diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py
index 427ca5e6ea8..96992cadb75 100644
--- a/homeassistant/components/verisure/switch.py
+++ b/homeassistant/components/verisure/switch.py
@@ -53,7 +53,6 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch
]
return DeviceInfo(
name=area,
- suggested_area=area,
manufacturer="Verisure",
model="SmartPlug",
identifiers={(DOMAIN, self.serial_number)},
diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py
index 603a42bae41..a2b2f3ac769 100644
--- a/homeassistant/components/vicare/__init__.py
+++ b/homeassistant/components/vicare/__init__.py
@@ -10,6 +10,7 @@ from typing import Any
from PyViCare.PyViCare import PyViCare
from PyViCare.PyViCareDevice import Device
+from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
from PyViCare.PyViCareUtils import (
PyViCareInvalidConfigurationError,
PyViCareInvalidCredentialsError,
@@ -85,15 +86,16 @@ def setup_vicare_api(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Set up PyVicare API."""
vicare_api = vicare_login(hass, entry.data)
- for device in vicare_api.devices:
- _LOGGER.info(
+ device_config_list = get_supported_devices(vicare_api.devices)
+
+ for device in device_config_list:
+ _LOGGER.debug(
"Found device: %s (online: %s)", device.getModel(), str(device.isOnline())
)
# Currently we only support a single device
- device_list = vicare_api.devices
- device = device_list[0]
- hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_list
+ device = device_config_list[0]
+ hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_config_list
hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG] = device
hass.data[DOMAIN][entry.entry_id][VICARE_API] = getattr(
device,
@@ -113,3 +115,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return unload_ok
+
+
+def get_supported_devices(
+ devices: list[PyViCareDeviceConfig],
+) -> list[PyViCareDeviceConfig]:
+ """Remove unsupported devices from the list."""
+ return [
+ device_config
+ for device_config in devices
+ if device_config.getModel() not in ["Heatbox1", "Heatbox2_SRC"]
+ ]
diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py
index c92a9883baf..fab496f63a6 100644
--- a/homeassistant/components/vicare/climate.py
+++ b/homeassistant/components/vicare/climate.py
@@ -148,6 +148,7 @@ class ViCareClimate(ViCareEntity, ClimateEntity):
_attr_preset_modes = list(HA_TO_VICARE_PRESET_HEATING)
_current_action: bool | None = None
_current_mode: str | None = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py
index 87bfcf7b146..32ae4af0fe7 100644
--- a/homeassistant/components/vicare/config_flow.py
+++ b/homeassistant/components/vicare/config_flow.py
@@ -118,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Invoke when a Viessmann MAC address is discovered on the network."""
formatted_mac = format_mac(discovery_info.macaddress)
- _LOGGER.info("Found device with mac %s", formatted_mac)
+ _LOGGER.debug("Found device with mac %s", formatted_mac)
await self.async_set_unique_id(formatted_mac)
self._abort_if_unique_id_configured()
diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py
index 39b4bd032dc..a8a21c7e787 100644
--- a/homeassistant/components/vicare/sensor.py
+++ b/homeassistant/components/vicare/sensor.py
@@ -145,6 +145,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getDomesticHotWaterMaxTemperature(),
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_min_temperature",
@@ -153,6 +154,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getDomesticHotWaterMinTemperature(),
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_gas_consumption_today",
@@ -167,6 +169,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(),
unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_gas_consumption_heating_this_month",
@@ -174,6 +177,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(),
unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_gas_consumption_heating_this_year",
@@ -181,6 +185,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(),
unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="gas_consumption_heating_today",
@@ -195,6 +200,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(),
unit_getter=lambda api: api.getGasConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="gas_consumption_heating_this_month",
@@ -202,6 +208,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(),
unit_getter=lambda api: api.getGasConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="gas_consumption_heating_this_year",
@@ -209,6 +216,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasConsumptionHeatingThisYear(),
unit_getter=lambda api: api.getGasConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="gas_consumption_fuelcell_today",
@@ -287,6 +295,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentMonth(),
unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="gas_summary_consumption_heating_currentyear",
@@ -295,6 +304,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentYear(),
unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="gas_summary_consumption_heating_lastsevendays",
@@ -303,6 +313,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasSummaryConsumptionHeatingLastSevenDays(),
unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_gas_summary_consumption_heating_currentday",
@@ -319,6 +330,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentMonth(),
unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_gas_summary_consumption_heating_currentyear",
@@ -327,6 +339,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentYear(),
unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="hotwater_gas_summary_consumption_heating_lastsevendays",
@@ -335,6 +348,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterLastSevenDays(),
unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="energy_summary_consumption_heating_currentday",
@@ -351,6 +365,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentMonth(),
unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="energy_summary_consumption_heating_currentyear",
@@ -359,6 +374,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentYear(),
unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="energy_summary_consumption_heating_lastsevendays",
@@ -367,6 +383,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerSummaryConsumptionHeatingLastSevenDays(),
unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="energy_dhw_summary_consumption_heating_currentday",
@@ -383,6 +400,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentMonth(),
unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="energy_dhw_summary_consumption_heating_currentyear",
@@ -391,6 +409,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentYear(),
unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="energy_summary_dhw_consumption_heating_lastsevendays",
@@ -399,6 +418,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterLastSevenDays(),
unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="power_production_current",
@@ -423,6 +443,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerProductionThisWeek(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="power_production_this_month",
@@ -431,6 +452,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerProductionThisMonth(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="power_production_this_year",
@@ -439,6 +461,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
value_getter=lambda api: api.getPowerProductionThisYear(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="solar storage temperature",
@@ -473,6 +496,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
unit_getter=lambda api: api.getSolarPowerProductionUnit(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="solar power production this month",
@@ -482,6 +506,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
unit_getter=lambda api: api.getSolarPowerProductionUnit(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="solar power production this year",
@@ -491,6 +516,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
unit_getter=lambda api: api.getSolarPowerProductionUnit(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="power consumption today",
@@ -509,6 +535,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
unit_getter=lambda api: api.getPowerConsumptionUnit(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="power consumption this month",
@@ -518,6 +545,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
unit_getter=lambda api: api.getPowerConsumptionUnit(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="power consumption this year",
@@ -527,6 +555,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
unit_getter=lambda api: api.getPowerConsumptionUnit(),
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="buffer top temperature",
@@ -615,6 +644,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.HOURS,
value_getter=lambda api: api.getHoursLoadClass1(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="compressor_hours_loadclass2",
@@ -623,6 +653,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.HOURS,
value_getter=lambda api: api.getHoursLoadClass2(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="compressor_hours_loadclass3",
@@ -631,6 +662,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.HOURS,
value_getter=lambda api: api.getHoursLoadClass3(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="compressor_hours_loadclass4",
@@ -639,6 +671,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.HOURS,
value_getter=lambda api: api.getHoursLoadClass4(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="compressor_hours_loadclass5",
@@ -647,6 +680,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.HOURS,
value_getter=lambda api: api.getHoursLoadClass5(),
state_class=SensorStateClass.TOTAL_INCREASING,
+ entity_registry_enabled_default=False,
),
ViCareSensorEntityDescription(
key="compressor_phase",
@@ -658,41 +692,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
)
-def _build_entity(
- vicare_api,
- device_config: PyViCareDeviceConfig,
- entity_description: ViCareSensorEntityDescription,
-):
- """Create a ViCare sensor entity."""
- if is_supported(entity_description.key, entity_description, vicare_api):
- return ViCareSensor(
- vicare_api,
- device_config,
- entity_description,
- )
- return None
-
-
-async def _entities_from_descriptions(
- hass: HomeAssistant,
- entities: list[ViCareSensor],
- sensor_descriptions: tuple[ViCareSensorEntityDescription, ...],
- iterables,
- config_entry: ConfigEntry,
-) -> None:
- """Create entities from descriptions and list of burners/circuits."""
- for description in sensor_descriptions:
- for current in iterables:
- entity = await hass.async_add_executor_job(
- _build_entity,
- current,
- hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG],
- description,
- )
- if entity:
- entities.append(entity)
-
-
def _build_entities(
device: PyViCareDevice,
device_config: PyViCareDeviceConfig,
diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py
index 2d38d713859..48b9b99c1e2 100644
--- a/homeassistant/components/whirlpool/climate.py
+++ b/homeassistant/components/whirlpool/climate.py
@@ -103,10 +103,13 @@ class AirConEntity(ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_swing_modes = SUPPORTED_SWING_MODES
_attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py
index 4c4f6682ffa..949d2330347 100644
--- a/homeassistant/components/xs1/climate.py
+++ b/homeassistant/components/xs1/climate.py
@@ -54,6 +54,7 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity):
_attr_hvac_mode = HVACMode.HEAT
_attr_hvac_modes = [HVACMode.HEAT]
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, device, sensor):
"""Initialize the actuator."""
diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py
index 6e4495ee0b9..a1e2fdd90a2 100644
--- a/homeassistant/components/yolink/climate.py
+++ b/homeassistant/components/yolink/climate.py
@@ -62,6 +62,7 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity):
"""YoLink Climate Entity."""
_attr_name = None
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
@@ -86,6 +87,8 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity):
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
@callback
diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py
index 40da264d695..cbc759e7008 100644
--- a/homeassistant/components/zha/climate.py
+++ b/homeassistant/components/zha/climate.py
@@ -141,6 +141,7 @@ class Thermostat(ZhaEntity, ClimateEntity):
_attr_precision = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key: str = "thermostat"
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
"""Initialize ZHA Thermostat instance."""
diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py
index 1364dbe107a..fbada765cde 100644
--- a/homeassistant/components/zhong_hong/climate.py
+++ b/homeassistant/components/zhong_hong/climate.py
@@ -128,9 +128,13 @@ class ZhongHongClimate(ClimateEntity):
]
_attr_should_poll = False
_attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.FAN_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
+ _enable_turn_on_off_backwards_compatibility = False
def __init__(self, hub, addr_out, addr_in):
"""Set up the ZhongHong climate devices."""
diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py
index 33d1e6dfa63..876cf60b4cb 100644
--- a/homeassistant/components/zwave_js/button.py
+++ b/homeassistant/components/zwave_js/button.py
@@ -86,13 +86,13 @@ class ZWaveNodePingButton(ButtonEntity):
_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
+ _attr_translation_key = "ping"
def __init__(self, driver: Driver, node: ZwaveNode) -> None:
"""Initialize a ping Z-Wave device button entity."""
self.node = node
# Entity class attributes
- self._attr_name = "Ping"
self._base_unique_id = get_valueless_base_unique_id(driver, node)
self._attr_unique_id = f"{self._base_unique_id}.ping"
# device may not be precreated in main handler yet
diff --git a/homeassistant/components/zwave_js/icons.json b/homeassistant/components/zwave_js/icons.json
index 2280811d3da..2956cf2c6e0 100644
--- a/homeassistant/components/zwave_js/icons.json
+++ b/homeassistant/components/zwave_js/icons.json
@@ -1,14 +1,34 @@
{
"entity": {
+ "button": {
+ "ping": {
+ "default": "mdi:crosshairs-gps"
+ }
+ },
"sensor": {
+ "can": {
+ "default": "mdi:car-brake-alert"
+ },
+ "commands_dropped": {
+ "default": "mdi:trash-can"
+ },
"controller_status": {
"default": "mdi:help-rhombus",
"state": {
+ "jammed": "mdi:lock",
"ready": "mdi:check",
- "unresponsive": "mdi:bell-off",
- "jammed": "mdi:lock"
+ "unresponsive": "mdi:bell-off"
}
},
+ "last_seen": {
+ "default": "mdi:timer-sync"
+ },
+ "messages_dropped": {
+ "default": "mdi:trash-can"
+ },
+ "nak": {
+ "default": "mdi:hand-back-left-off"
+ },
"node_status": {
"default": "mdi:help-rhombus",
"state": {
@@ -18,7 +38,36 @@
"dead": "mdi:robot-dead",
"unknown": "mdi:help-rhombus"
}
+ },
+ "successful_commands": {
+ "default": "mdi:check"
+ },
+ "successful_messages": {
+ "default": "mdi:check"
+ },
+ "timeout_ack": {
+ "default": "mdi:ear-hearing-off"
+ },
+ "timeout_callback": {
+ "default": "mdi:timer-sand-empty"
+ },
+ "timeout_response": {
+ "default": "mdi:timer-sand-empty"
}
}
+ },
+ "services": {
+ "bulk_set_partial_config_parameters": "mdi:cogs",
+ "clear_lock_usercode": "mdi:eraser",
+ "invoke_cc_api": "mdi:api",
+ "multicast_set_value": "mdi:list-box",
+ "ping": "mdi:crosshairs-gps",
+ "refresh_notifications": "mdi:bell",
+ "refresh_value": "mdi:refresh",
+ "reset_meter": "mdi:meter-electric",
+ "set_config_parameter": "mdi:cog",
+ "set_lock_configuration": "mdi:shield-lock",
+ "set_lock_usercode": "mdi:lock-smart",
+ "set_value": "mdi:form-textbox"
}
}
diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py
index 0b9defc5f62..0240725ca2d 100644
--- a/homeassistant/components/zwave_js/sensor.py
+++ b/homeassistant/components/zwave_js/sensor.py
@@ -350,55 +350,61 @@ class ZWaveJSStatisticsSensorEntityDescription(SensorEntityDescription):
ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
ZWaveJSStatisticsSensorEntityDescription(
key="messagesTX",
- name="Successful messages (TX)",
+ translation_key="successful_messages",
+ translation_placeholders={"direction": "TX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="messagesRX",
- name="Successful messages (RX)",
+ translation_key="successful_messages",
+ translation_placeholders={"direction": "RX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="messagesDroppedTX",
- name="Messages dropped (TX)",
+ translation_key="messages_dropped",
+ translation_placeholders={"direction": "TX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="messagesDroppedRX",
- name="Messages dropped (RX)",
+ translation_key="messages_dropped",
+ translation_placeholders={"direction": "RX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
- key="NAK",
- name="Messages not accepted",
+ key="NAK", translation_key="nak", state_class=SensorStateClass.TOTAL
+ ),
+ ZWaveJSStatisticsSensorEntityDescription(
+ key="CAN", translation_key="can", state_class=SensorStateClass.TOTAL
+ ),
+ ZWaveJSStatisticsSensorEntityDescription(
+ key="timeoutACK",
+ translation_key="timeout_ack",
state_class=SensorStateClass.TOTAL,
),
- ZWaveJSStatisticsSensorEntityDescription(
- key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL
- ),
- ZWaveJSStatisticsSensorEntityDescription(
- key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL
- ),
ZWaveJSStatisticsSensorEntityDescription(
key="timeoutResponse",
- name="Timed out responses",
+ translation_key="timeout_response",
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="timeoutCallback",
- name="Timed out callbacks",
+ translation_key="timeout_callback",
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel0.average",
- name="Average background RSSI (channel 0)",
+ translation_key="average_background_rssi",
+ translation_placeholders={"channel": "0"},
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
convert=convert_dict_of_dicts,
),
ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel0.current",
- name="Current background RSSI (channel 0)",
+ translation_key="current_background_rssi",
+ translation_placeholders={"channel": "0"},
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
@@ -406,14 +412,16 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
),
ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel1.average",
- name="Average background RSSI (channel 1)",
+ translation_key="average_background_rssi",
+ translation_placeholders={"channel": "1"},
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
convert=convert_dict_of_dicts,
),
ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel1.current",
- name="Current background RSSI (channel 1)",
+ translation_key="current_background_rssi",
+ translation_placeholders={"channel": "1"},
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
@@ -421,14 +429,16 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
),
ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel2.average",
- name="Average background RSSI (channel 2)",
+ translation_key="average_background_rssi",
+ translation_placeholders={"channel": "2"},
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
convert=convert_dict_of_dicts,
),
ZWaveJSStatisticsSensorEntityDescription(
key="backgroundRSSI.channel2.current",
- name="Current background RSSI (channel 2)",
+ translation_key="current_background_rssi",
+ translation_placeholders={"channel": "2"},
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
@@ -440,46 +450,50 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [
ZWaveJSStatisticsSensorEntityDescription(
key="commandsRX",
- name="Successful commands (RX)",
+ translation_key="successful_commands",
+ translation_placeholders={"direction": "RX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="commandsTX",
- name="Successful commands (TX)",
+ translation_key="successful_commands",
+ translation_placeholders={"direction": "TX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="commandsDroppedRX",
- name="Commands dropped (RX)",
+ translation_key="commands_dropped",
+ translation_placeholders={"direction": "RX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="commandsDroppedTX",
- name="Commands dropped (TX)",
+ translation_key="commands_dropped",
+ translation_placeholders={"direction": "TX"},
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="timeoutResponse",
- name="Timed out responses",
+ translation_key="timeout_response",
state_class=SensorStateClass.TOTAL,
),
ZWaveJSStatisticsSensorEntityDescription(
key="rtt",
- name="Round Trip Time",
+ translation_key="rtt",
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
ZWaveJSStatisticsSensorEntityDescription(
key="rssi",
- name="RSSI",
+ translation_key="rssi",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
),
ZWaveJSStatisticsSensorEntityDescription(
key="lastSeen",
- name="Last Seen",
+ translation_key="last_seen",
device_class=SensorDeviceClass.TIMESTAMP,
convert=(
lambda statistics, key: (
diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json
index db19c0fceeb..9e2317ba728 100644
--- a/homeassistant/components/zwave_js/strings.json
+++ b/homeassistant/components/zwave_js/strings.json
@@ -1,6 +1,133 @@
{
+ "config": {
+ "abort": {
+ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
+ "addon_info_failed": "Failed to get Z-Wave JS add-on info.",
+ "addon_install_failed": "Failed to install the Z-Wave JS add-on.",
+ "addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
+ "addon_start_failed": "Failed to start the Z-Wave JS add-on.",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "discovery_requires_supervisor": "Discovery requires the supervisor.",
+ "not_zwave_device": "Discovered device is not a Z-Wave device.",
+ "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on."
+ },
+ "error": {
+ "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_ws_url": "Invalid websocket URL",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "flow_title": "{name}",
+ "progress": {
+ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
+ "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "s0_legacy_key": "S0 Key (Legacy)",
+ "s2_access_control_key": "S2 Access Control Key",
+ "s2_authenticated_key": "S2 Authenticated Key",
+ "s2_unauthenticated_key": "S2 Unauthenticated Key",
+ "usb_path": "[%key:common::config_flow::data::usb_path%]"
+ },
+ "description": "The add-on will generate security keys if those fields are left empty.",
+ "title": "Enter the Z-Wave JS add-on configuration"
+ },
+ "hassio_confirm": {
+ "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on"
+ },
+ "install_addon": {
+ "title": "The Z-Wave JS add-on installation has started"
+ },
+ "manual": {
+ "data": {
+ "url": "[%key:common::config_flow::data::url%]"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Use the Z-Wave JS Supervisor add-on"
+ },
+ "description": "Do you want to use the Z-Wave JS Supervisor add-on?",
+ "title": "Select connection method"
+ },
+ "start_addon": {
+ "title": "The Z-Wave JS add-on is starting."
+ },
+ "usb_confirm": {
+ "description": "Do you want to set up {name} with the Z-Wave JS add-on?"
+ },
+ "zeroconf_confirm": {
+ "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?",
+ "title": "Discovered Z-Wave JS Server"
+ }
+ }
+ },
+ "device_automation": {
+ "action_type": {
+ "clear_lock_usercode": "Clear usercode on {entity_name}",
+ "ping": "Ping device",
+ "refresh_value": "Refresh the value(s) for {entity_name}",
+ "reset_meter": "Reset meters on {subtype}",
+ "set_config_parameter": "Set value of config parameter {subtype}",
+ "set_lock_usercode": "Set a usercode on {entity_name}",
+ "set_value": "Set value of a Z-Wave Value"
+ },
+ "condition_type": {
+ "config_parameter": "Config parameter {subtype} value",
+ "node_status": "Node status",
+ "value": "Current value of a Z-Wave Value"
+ },
+ "trigger_type": {
+ "event.notification.entry_control": "Sent an Entry Control notification",
+ "event.notification.notification": "Sent a notification",
+ "event.value_notification.basic": "Basic CC event on {subtype}",
+ "event.value_notification.central_scene": "Central Scene action on {subtype}",
+ "event.value_notification.scene_activation": "Scene Activation on {subtype}",
+ "state.node_status": "Node status changed",
+ "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}",
+ "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value"
+ }
+ },
"entity": {
+ "button": {
+ "ping": {
+ "name": "Ping"
+ }
+ },
"sensor": {
+ "average_background_rssi": {
+ "name": "Average background RSSI (channel {channel})"
+ },
+ "can": {
+ "name": "Collisions"
+ },
+ "commands_dropped": {
+ "name": "Commands dropped ({direction})"
+ },
+ "controller_status": {
+ "name": "Status",
+ "state": {
+ "jammed": "Jammed",
+ "ready": "Ready",
+ "unresponsive": "Unresponsive"
+ }
+ },
+ "current_background_rssi": {
+ "name": "Current background RSSI (channel {channel})"
+ },
+ "last_seen": {
+ "name": "Last seen"
+ },
+ "messages_dropped": {
+ "name": "Messages dropped ({direction})"
+ },
+ "nak": {
+ "name": "Messages not accepted"
+ },
"node_status": {
"name": "Node status",
"state": {
@@ -11,434 +138,354 @@
"unknown": "Unknown"
}
},
- "controller_status": {
- "name": "Status",
- "state": {
- "ready": "Ready",
- "unresponsive": "Unresponsive",
- "jammed": "Jammed"
- }
+ "rssi": {
+ "name": "RSSI"
+ },
+ "rtt": {
+ "name": "Round trip time"
+ },
+ "successful_commands": {
+ "name": "Successful commands ({direction})"
+ },
+ "successful_messages": {
+ "name": "Successful messages ({direction})"
+ },
+ "timeout_ack": {
+ "name": "Missing ACKs"
+ },
+ "timeout_callback": {
+ "name": "Timed out callbacks"
+ },
+ "timeout_response": {
+ "name": "Timed out responses"
}
}
},
- "config": {
- "flow_title": "{name}",
- "step": {
- "manual": {
- "data": {
- "url": "[%key:common::config_flow::data::url%]"
- }
- },
- "usb_confirm": {
- "description": "Do you want to set up {name} with the Z-Wave JS add-on?"
- },
- "on_supervisor": {
- "title": "Select connection method",
- "description": "Do you want to use the Z-Wave JS Supervisor add-on?",
- "data": {
- "use_addon": "Use the Z-Wave JS Supervisor add-on"
- }
- },
- "install_addon": {
- "title": "The Z-Wave JS add-on installation has started"
- },
- "configure_addon": {
- "title": "Enter the Z-Wave JS add-on configuration",
- "description": "The add-on will generate security keys if those fields are left empty.",
- "data": {
- "usb_path": "[%key:common::config_flow::data::usb_path%]",
- "s0_legacy_key": "S0 Key (Legacy)",
- "s2_authenticated_key": "S2 Authenticated Key",
- "s2_unauthenticated_key": "S2 Unauthenticated Key",
- "s2_access_control_key": "S2 Access Control Key"
- }
- },
- "start_addon": {
- "title": "The Z-Wave JS add-on is starting."
- },
- "hassio_confirm": {
- "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on"
- },
- "zeroconf_confirm": {
- "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?",
- "title": "Discovered Z-Wave JS Server"
- }
- },
- "error": {
- "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.",
- "invalid_ws_url": "Invalid websocket URL",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
- "addon_info_failed": "Failed to get Z-Wave JS add-on info.",
- "addon_install_failed": "Failed to install the Z-Wave JS add-on.",
- "addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
- "addon_start_failed": "Failed to start the Z-Wave JS add-on.",
- "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "discovery_requires_supervisor": "Discovery requires the supervisor.",
- "not_zwave_device": "Discovered device is not a Z-Wave device.",
- "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on."
- },
- "progress": {
- "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
- "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
- }
- },
- "options": {
- "step": {
- "manual": {
- "data": {
- "url": "[%key:common::config_flow::data::url%]"
- }
- },
- "on_supervisor": {
- "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]",
- "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]",
- "data": {
- "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]"
- }
- },
- "install_addon": {
- "title": "[%key:component::zwave_js::config::step::install_addon::title%]"
- },
- "configure_addon": {
- "title": "[%key:component::zwave_js::config::step::configure_addon::title%]",
- "description": "[%key:component::zwave_js::config::step::configure_addon::description%]",
- "data": {
- "usb_path": "[%key:common::config_flow::data::usb_path%]",
- "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]",
- "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]",
- "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]",
- "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]",
- "log_level": "Log level",
- "emulate_hardware": "Emulate Hardware"
- }
- },
- "start_addon": {
- "title": "[%key:component::zwave_js::config::step::start_addon::title%]"
- }
- },
- "error": {
- "invalid_ws_url": "[%key:component::zwave_js::config::error::invalid_ws_url%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "addon_info_failed": "[%key:component::zwave_js::config::abort::addon_info_failed%]",
- "addon_install_failed": "[%key:component::zwave_js::config::abort::addon_install_failed%]",
- "addon_set_config_failed": "[%key:component::zwave_js::config::abort::addon_set_config_failed%]",
- "addon_start_failed": "[%key:component::zwave_js::config::abort::addon_start_failed%]",
- "addon_get_discovery_info_failed": "[%key:component::zwave_js::config::abort::addon_get_discovery_info_failed%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device."
- },
- "progress": {
- "install_addon": "[%key:component::zwave_js::config::progress::install_addon%]",
- "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]"
- }
- },
- "device_automation": {
- "trigger_type": {
- "event.notification.entry_control": "Sent an Entry Control notification",
- "event.notification.notification": "Sent a notification",
- "event.value_notification.basic": "Basic CC event on {subtype}",
- "event.value_notification.central_scene": "Central Scene action on {subtype}",
- "event.value_notification.scene_activation": "Scene Activation on {subtype}",
- "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}",
- "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value",
- "state.node_status": "Node status changed"
- },
- "condition_type": {
- "node_status": "Node status",
- "config_parameter": "Config parameter {subtype} value",
- "value": "Current value of a Z-Wave Value"
- },
- "action_type": {
- "clear_lock_usercode": "Clear usercode on {entity_name}",
- "set_lock_usercode": "Set a usercode on {entity_name}",
- "set_config_parameter": "Set value of config parameter {subtype}",
- "set_value": "Set value of a Z-Wave Value",
- "refresh_value": "Refresh the value(s) for {entity_name}",
- "ping": "Ping device",
- "reset_meter": "Reset meters on {subtype}"
- }
- },
"issues": {
- "invalid_server_version": {
- "title": "Newer version of Z-Wave JS Server needed",
- "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue."
- },
"device_config_file_changed": {
- "title": "Device configuration file changed: {device_name}",
"fix_flow": {
+ "abort": {
+ "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.",
+ "issue_ignored": "Device config file update for {device_name} ignored."
+ },
"step": {
"init": {
+ "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background.",
"menu_options": {
"confirm": "Re-interview device",
"ignore": "Ignore device config update"
},
- "title": "Device configuration file changed: {device_name}",
- "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background."
+ "title": "Device configuration file changed: {device_name}"
}
- },
- "abort": {
- "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.",
- "issue_ignored": "Device config file update for {device_name} ignored."
}
+ },
+ "title": "Device configuration file changed: {device_name}"
+ },
+ "invalid_server_version": {
+ "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue.",
+ "title": "Newer version of Z-Wave JS Server needed"
+ }
+ },
+ "options": {
+ "abort": {
+ "addon_get_discovery_info_failed": "[%key:component::zwave_js::config::abort::addon_get_discovery_info_failed%]",
+ "addon_info_failed": "[%key:component::zwave_js::config::abort::addon_info_failed%]",
+ "addon_install_failed": "[%key:component::zwave_js::config::abort::addon_install_failed%]",
+ "addon_set_config_failed": "[%key:component::zwave_js::config::abort::addon_set_config_failed%]",
+ "addon_start_failed": "[%key:component::zwave_js::config::abort::addon_start_failed%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device."
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_ws_url": "[%key:component::zwave_js::config::error::invalid_ws_url%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "progress": {
+ "install_addon": "[%key:component::zwave_js::config::progress::install_addon%]",
+ "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]"
+ },
+ "step": {
+ "configure_addon": {
+ "data": {
+ "emulate_hardware": "Emulate Hardware",
+ "log_level": "Log level",
+ "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]",
+ "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]",
+ "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]",
+ "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]",
+ "usb_path": "[%key:common::config_flow::data::usb_path%]"
+ },
+ "description": "[%key:component::zwave_js::config::step::configure_addon::description%]",
+ "title": "[%key:component::zwave_js::config::step::configure_addon::title%]"
+ },
+ "install_addon": {
+ "title": "[%key:component::zwave_js::config::step::install_addon::title%]"
+ },
+ "manual": {
+ "data": {
+ "url": "[%key:common::config_flow::data::url%]"
+ }
+ },
+ "on_supervisor": {
+ "data": {
+ "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]"
+ },
+ "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]",
+ "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]"
+ },
+ "start_addon": {
+ "title": "[%key:component::zwave_js::config::step::start_addon::title%]"
}
}
},
"services": {
- "clear_lock_usercode": {
- "name": "Clear lock user code",
- "description": "Clears a user code from a lock.",
- "fields": {
- "code_slot": {
- "name": "Code slot",
- "description": "Code slot to clear code from."
- }
- }
- },
- "set_lock_usercode": {
- "name": "Set lock user code",
- "description": "Sets a user code on a lock.",
- "fields": {
- "code_slot": {
- "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]",
- "description": "Code slot to set the code."
- },
- "usercode": {
- "name": "Code",
- "description": "Lock code to set."
- }
- }
- },
- "set_config_parameter": {
- "name": "Set device configuration parameter",
- "description": "Changes the configuration parameters of your Z-Wave devices.",
- "fields": {
- "endpoint": {
- "name": "Endpoint",
- "description": "The configuration parameter's endpoint."
- },
- "parameter": {
- "name": "Parameter",
- "description": "The name (or ID) of the configuration parameter you want to configure."
- },
- "bitmask": {
- "name": "Bitmask",
- "description": "Target a specific bitmask (see the documentation for more information). Cannot be combined with value_size or value_format."
- },
- "value": {
- "name": "Value",
- "description": "The new value to set for this configuration parameter."
- },
- "value_size": {
- "name": "Value size",
- "description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask."
- },
- "value_format": {
- "name": "Value format",
- "description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask."
- }
- }
- },
"bulk_set_partial_config_parameters": {
- "name": "Bulk set partial configuration parameters (advanced).",
"description": "Allows for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time.",
"fields": {
"endpoint": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]",
- "description": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::description%]"
+ "description": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::description%]",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]"
},
"parameter": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::name%]",
- "description": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::description%]"
+ "description": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::description%]",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::name%]"
},
"value": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]",
- "description": "The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter."
+ "description": "The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter.",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]"
}
- }
+ },
+ "name": "Bulk set partial configuration parameters (advanced)."
},
- "refresh_value": {
- "name": "Refresh values",
- "description": "Force updates the values of a Z-Wave entity.",
+ "clear_lock_usercode": {
+ "description": "Clears a user code from a lock.",
"fields": {
- "entity_id": {
- "name": "Entities",
- "description": "Entities to refresh."
- },
- "refresh_all_values": {
- "name": "Refresh all values?",
- "description": "Whether to refresh all values (true) or just the primary value (false)."
+ "code_slot": {
+ "description": "Code slot to clear code from.",
+ "name": "Code slot"
}
- }
- },
- "set_value": {
- "name": "Set a value (advanced)",
- "description": "Changes any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.",
- "fields": {
- "command_class": {
- "name": "Command class",
- "description": "The ID of the command class for the value."
- },
- "endpoint": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]",
- "description": "The endpoint for the value."
- },
- "property": {
- "name": "Property",
- "description": "The ID of the property for the value."
- },
- "property_key": {
- "name": "Property key",
- "description": "The ID of the property key for the value."
- },
- "value": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]",
- "description": "The new value to set."
- },
- "options": {
- "name": "Options",
- "description": "Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set."
- },
- "wait_for_result": {
- "name": "Wait for result?",
- "description": "Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device."
- }
- }
- },
- "multicast_set_value": {
- "name": "Set a value on multiple devices via multicast (advanced)",
- "description": "Changes any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing.",
- "fields": {
- "broadcast": {
- "name": "Broadcast?",
- "description": "Whether command should be broadcast to all devices on the network."
- },
- "command_class": {
- "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]",
- "description": "[%key:component::zwave_js::services::set_value::fields::command_class::description%]"
- },
- "endpoint": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]",
- "description": "[%key:component::zwave_js::services::set_value::fields::endpoint::description%]"
- },
- "property": {
- "name": "[%key:component::zwave_js::services::set_value::fields::property::name%]",
- "description": "[%key:component::zwave_js::services::set_value::fields::property::description%]"
- },
- "property_key": {
- "name": "[%key:component::zwave_js::services::set_value::fields::property_key::name%]",
- "description": "[%key:component::zwave_js::services::set_value::fields::property_key::description%]"
- },
- "options": {
- "name": "[%key:component::zwave_js::services::set_value::fields::options::name%]",
- "description": "[%key:component::zwave_js::services::set_value::fields::options::description%]"
- },
- "value": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]",
- "description": "[%key:component::zwave_js::services::set_value::fields::value::description%]"
- }
- }
- },
- "ping": {
- "name": "Ping a node",
- "description": "Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep."
- },
- "reset_meter": {
- "name": "Reset meters on a node",
- "description": "Resets the meters on a node.",
- "fields": {
- "meter_type": {
- "name": "Meter type",
- "description": "The type of meter to reset. Not all meters support the ability to pick a meter type to reset."
- },
- "value": {
- "name": "Target value",
- "description": "The value that meters should be reset to. Not all meters support the ability to be reset to a specific value."
- }
- }
+ },
+ "name": "Clear lock user code"
},
"invoke_cc_api": {
- "name": "Invoke a Command Class API on a node (advanced)",
"description": "Calls a Command Class API on a node. Some Command Classes can't be fully controlled via the `set_value` service and require direct calls to the Command Class API.",
"fields": {
"command_class": {
- "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]",
- "description": "The ID of the command class that you want to issue a command to."
+ "description": "The ID of the command class that you want to issue a command to.",
+ "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]"
},
"endpoint": {
- "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]",
- "description": "The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted."
+ "description": "The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted.",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]"
},
"method_name": {
- "name": "Method name",
- "description": "The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods."
+ "description": "The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods.",
+ "name": "Method name"
},
"parameters": {
- "name": "Parameters",
- "description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters."
+ "description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters.",
+ "name": "Parameters"
}
- }
+ },
+ "name": "Invoke a Command Class API on a node (advanced)"
+ },
+ "multicast_set_value": {
+ "description": "Changes any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing.",
+ "fields": {
+ "broadcast": {
+ "description": "Whether command should be broadcast to all devices on the network.",
+ "name": "Broadcast?"
+ },
+ "command_class": {
+ "description": "[%key:component::zwave_js::services::set_value::fields::command_class::description%]",
+ "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]"
+ },
+ "endpoint": {
+ "description": "[%key:component::zwave_js::services::set_value::fields::endpoint::description%]",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]"
+ },
+ "options": {
+ "description": "[%key:component::zwave_js::services::set_value::fields::options::description%]",
+ "name": "[%key:component::zwave_js::services::set_value::fields::options::name%]"
+ },
+ "property": {
+ "description": "[%key:component::zwave_js::services::set_value::fields::property::description%]",
+ "name": "[%key:component::zwave_js::services::set_value::fields::property::name%]"
+ },
+ "property_key": {
+ "description": "[%key:component::zwave_js::services::set_value::fields::property_key::description%]",
+ "name": "[%key:component::zwave_js::services::set_value::fields::property_key::name%]"
+ },
+ "value": {
+ "description": "[%key:component::zwave_js::services::set_value::fields::value::description%]",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]"
+ }
+ },
+ "name": "Set a value on multiple devices via multicast (advanced)"
+ },
+ "ping": {
+ "description": "Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep.",
+ "name": "Ping a node"
},
"refresh_notifications": {
- "name": "Refresh notifications on a node (advanced)",
"description": "Refreshes notifications on a node based on notification type and optionally notification event.",
"fields": {
- "notification_type": {
- "name": "Notification Type",
- "description": "The Notification Type number as defined in the Z-Wave specs."
- },
"notification_event": {
- "name": "Notification Event",
- "description": "The Notification Event number as defined in the Z-Wave specs."
+ "description": "The Notification Event number as defined in the Z-Wave specs.",
+ "name": "Notification Event"
+ },
+ "notification_type": {
+ "description": "The Notification Type number as defined in the Z-Wave specs.",
+ "name": "Notification Type"
}
- }
+ },
+ "name": "Refresh notifications on a node (advanced)"
+ },
+ "refresh_value": {
+ "description": "Force updates the values of a Z-Wave entity.",
+ "fields": {
+ "entity_id": {
+ "description": "Entities to refresh.",
+ "name": "Entities"
+ },
+ "refresh_all_values": {
+ "description": "Whether to refresh all values (true) or just the primary value (false).",
+ "name": "Refresh all values?"
+ }
+ },
+ "name": "Refresh values"
+ },
+ "reset_meter": {
+ "description": "Resets the meters on a node.",
+ "fields": {
+ "meter_type": {
+ "description": "The type of meter to reset. Not all meters support the ability to pick a meter type to reset.",
+ "name": "Meter type"
+ },
+ "value": {
+ "description": "The value that meters should be reset to. Not all meters support the ability to be reset to a specific value.",
+ "name": "Target value"
+ }
+ },
+ "name": "Reset meters on a node"
+ },
+ "set_config_parameter": {
+ "description": "Changes the configuration parameters of your Z-Wave devices.",
+ "fields": {
+ "bitmask": {
+ "description": "Target a specific bitmask (see the documentation for more information). Cannot be combined with value_size or value_format.",
+ "name": "Bitmask"
+ },
+ "endpoint": {
+ "description": "The configuration parameter's endpoint.",
+ "name": "Endpoint"
+ },
+ "parameter": {
+ "description": "The name (or ID) of the configuration parameter you want to configure.",
+ "name": "Parameter"
+ },
+ "value": {
+ "description": "The new value to set for this configuration parameter.",
+ "name": "Value"
+ },
+ "value_format": {
+ "description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask.",
+ "name": "Value format"
+ },
+ "value_size": {
+ "description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask.",
+ "name": "Value size"
+ }
+ },
+ "name": "Set device configuration parameter"
},
"set_lock_configuration": {
- "name": "Set lock configuration",
"description": "Sets the configuration for a lock.",
"fields": {
- "operation_type": {
- "name": "Operation Type",
- "description": "The operation type of the lock."
- },
- "lock_timeout": {
- "name": "Lock timeout",
- "description": "Seconds until lock mode times out. Should only be used if operation type is `timed`."
- },
- "outside_handles_can_open_door_configuration": {
- "name": "Outside handles can open door configuration",
- "description": "A list of four booleans which indicate which outside handles can open the door."
- },
- "inside_handles_can_open_door_configuration": {
- "name": "Inside handles can open door configuration",
- "description": "A list of four booleans which indicate which inside handles can open the door."
- },
"auto_relock_time": {
- "name": "Auto relock time",
- "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`."
- },
- "hold_and_release_time": {
- "name": "Hold and release time",
- "description": "Duration in seconds the latch stays retracted."
- },
- "twist_assist": {
- "name": "Twist assist",
- "description": "Enable Twist Assist."
+ "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`.",
+ "name": "Auto relock time"
},
"block_to_block": {
- "name": "Block to block",
- "description": "Enable block-to-block functionality."
+ "description": "Enable block-to-block functionality.",
+ "name": "Block to block"
+ },
+ "hold_and_release_time": {
+ "description": "Duration in seconds the latch stays retracted.",
+ "name": "Hold and release time"
+ },
+ "inside_handles_can_open_door_configuration": {
+ "description": "A list of four booleans which indicate which inside handles can open the door.",
+ "name": "Inside handles can open door configuration"
+ },
+ "lock_timeout": {
+ "description": "Seconds until lock mode times out. Should only be used if operation type is `timed`.",
+ "name": "Lock timeout"
+ },
+ "operation_type": {
+ "description": "The operation type of the lock.",
+ "name": "Operation Type"
+ },
+ "outside_handles_can_open_door_configuration": {
+ "description": "A list of four booleans which indicate which outside handles can open the door.",
+ "name": "Outside handles can open door configuration"
+ },
+ "twist_assist": {
+ "description": "Enable Twist Assist.",
+ "name": "Twist assist"
}
- }
+ },
+ "name": "Set lock configuration"
+ },
+ "set_lock_usercode": {
+ "description": "Sets a user code on a lock.",
+ "fields": {
+ "code_slot": {
+ "description": "Code slot to set the code.",
+ "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]"
+ },
+ "usercode": {
+ "description": "Lock code to set.",
+ "name": "Code"
+ }
+ },
+ "name": "Set lock user code"
+ },
+ "set_value": {
+ "description": "Changes any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.",
+ "fields": {
+ "command_class": {
+ "description": "The ID of the command class for the value.",
+ "name": "Command class"
+ },
+ "endpoint": {
+ "description": "The endpoint for the value.",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]"
+ },
+ "options": {
+ "description": "Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set.",
+ "name": "Options"
+ },
+ "property": {
+ "description": "The ID of the property for the value.",
+ "name": "Property"
+ },
+ "property_key": {
+ "description": "The ID of the property key for the value.",
+ "name": "Property key"
+ },
+ "value": {
+ "description": "The new value to set.",
+ "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]"
+ },
+ "wait_for_result": {
+ "description": "Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device.",
+ "name": "Wait for result?"
+ }
+ },
+ "name": "Set a value (advanced)"
}
}
}
diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py
index 7d654311213..35e0d745619 100644
--- a/homeassistant/components/zwave_me/climate.py
+++ b/homeassistant/components/zwave_me/climate.py
@@ -56,6 +56,7 @@ class ZWaveMeClimate(ZWaveMeEntity, ClimateEntity):
_attr_hvac_mode = HVACMode.HEAT
_attr_hvac_modes = [HVACMode.HEAT]
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+ _enable_turn_on_off_backwards_compatibility = False
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py
index a087c8ac483..4f9f822e85e 100644
--- a/homeassistant/generated/dhcp.py
+++ b/homeassistant/generated/dhcp.py
@@ -788,6 +788,11 @@ DHCP: list[dict[str, str | bool]] = [
"hostname": "k[lps]*",
"macaddress": "54AF97*",
},
+ {
+ "domain": "tplink",
+ "hostname": "l[59]*",
+ "macaddress": "54AF97*",
+ },
{
"domain": "tplink",
"hostname": "k[lps]*",
@@ -815,7 +820,7 @@ DHCP: list[dict[str, str | bool]] = [
},
{
"domain": "tplink",
- "hostname": "l5*",
+ "hostname": "l[59]*",
"macaddress": "3C52A1*",
},
{
@@ -823,16 +828,41 @@ DHCP: list[dict[str, str | bool]] = [
"hostname": "l5*",
"macaddress": "5C628B*",
},
+ {
+ "domain": "tplink",
+ "hostname": "tp*",
+ "macaddress": "5C628B*",
+ },
{
"domain": "tplink",
"hostname": "p1*",
"macaddress": "482254*",
},
+ {
+ "domain": "tplink",
+ "hostname": "s5*",
+ "macaddress": "482254*",
+ },
{
"domain": "tplink",
"hostname": "p1*",
"macaddress": "30DE4B*",
},
+ {
+ "domain": "tplink",
+ "hostname": "p1*",
+ "macaddress": "3C52A1*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "tp*",
+ "macaddress": "3C52A1*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "s5*",
+ "macaddress": "3C52A1*",
+ },
{
"domain": "tplink",
"hostname": "l9*",
@@ -843,6 +873,56 @@ DHCP: list[dict[str, str | bool]] = [
"hostname": "l9*",
"macaddress": "3460F9*",
},
+ {
+ "domain": "tplink",
+ "hostname": "hs*",
+ "macaddress": "704F57*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "k[lps]*",
+ "macaddress": "74DA88*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "p3*",
+ "macaddress": "788CB5*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "p1*",
+ "macaddress": "CC32E5*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "k[lps]*",
+ "macaddress": "CC32E5*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "hs*",
+ "macaddress": "CC32E5*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "k[lps]*",
+ "macaddress": "D80D17*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "k[lps]*",
+ "macaddress": "D84732*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "p1*",
+ "macaddress": "F0A731*",
+ },
+ {
+ "domain": "tplink",
+ "hostname": "l9*",
+ "macaddress": "F0A731*",
+ },
{
"domain": "tuya",
"macaddress": "105A17*",
@@ -891,6 +971,10 @@ DHCP: list[dict[str, str | bool]] = [
"domain": "twinkly",
"hostname": "twinkly_*",
},
+ {
+ "domain": "twinkly",
+ "hostname": "twinkly-*",
+ },
{
"domain": "unifiprotect",
"macaddress": "B4FBE4*",
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 21186272bb6..c49882f4394 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1342,6 +1342,11 @@
"config_flow": true,
"iot_class": "local_push"
},
+ "duquesne_light": {
+ "name": "Duquesne Light",
+ "integration_type": "virtual",
+ "supported_by": "opower"
+ },
"dwd_weather_warnings": {
"name": "Deutscher Wetterdienst (DWD) Weather Warnings",
"integration_type": "service",
@@ -6168,7 +6173,7 @@
"traccar": {
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_polling",
+ "iot_class": "cloud_push",
"name": "Traccar Client"
},
"traccar_server": {
diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py
index 3486925b095..dd216a78648 100644
--- a/homeassistant/helpers/icon.py
+++ b/homeassistant/helpers/icon.py
@@ -13,7 +13,6 @@ from homeassistant.util.json import load_json_object
from .translation import build_resources
-ICON_LOAD_LOCK = "icon_load_lock"
ICON_CACHE = "icon_cache"
_LOGGER = logging.getLogger(__name__)
@@ -73,13 +72,14 @@ async def _async_get_component_icons(
class _IconsCache:
"""Cache for icons."""
- __slots__ = ("_hass", "_loaded", "_cache")
+ __slots__ = ("_hass", "_loaded", "_cache", "_lock")
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the cache."""
self._hass = hass
self._loaded: set[str] = set()
self._cache: dict[str, dict[str, Any]] = {}
+ self._lock = asyncio.Lock()
async def async_fetch(
self,
@@ -88,7 +88,13 @@ class _IconsCache:
) -> dict[str, dict[str, Any]]:
"""Load resources into the cache."""
if components_to_load := components - self._loaded:
- await self._async_load(components_to_load)
+ # Icons are never unloaded so if there are no components to load
+ # we can skip the lock which reduces contention
+ async with self._lock:
+ # Check components to load again, as another task might have loaded
+ # them while we were waiting for the lock.
+ if components_to_load := components - self._loaded:
+ await self._async_load(components_to_load)
return {
component: result
@@ -143,21 +149,19 @@ async def async_get_icons(
"""Return all icons of integrations.
If integration specified, load it for that one; otherwise default to loaded
- intgrations.
+ integrations.
"""
- lock = hass.data.setdefault(ICON_LOAD_LOCK, asyncio.Lock())
-
if integrations:
components = set(integrations)
else:
components = {
component for component in hass.config.components if "." not in component
}
- async with lock:
- if ICON_CACHE in hass.data:
- cache: _IconsCache = hass.data[ICON_CACHE]
- else:
- cache = hass.data[ICON_CACHE] = _IconsCache(hass)
+
+ if ICON_CACHE in hass.data:
+ cache: _IconsCache = hass.data[ICON_CACHE]
+ else:
+ cache = hass.data[ICON_CACHE] = _IconsCache(hass)
return await cache.async_fetch(category, components)
diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py
index 26468f1fdb7..fe399659a56 100644
--- a/homeassistant/helpers/intent.py
+++ b/homeassistant/helpers/intent.py
@@ -1,4 +1,5 @@
"""Module to coordinate user intentions."""
+
from __future__ import annotations
import asyncio
@@ -401,17 +402,21 @@ class ServiceIntentHandler(IntentHandler):
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
- name: str | None = slots.get("name", {}).get("value")
- if name == "all":
+ name_slot = slots.get("name", {})
+ entity_id: str | None = name_slot.get("value")
+ entity_name: str | None = name_slot.get("text")
+ if entity_id == "all":
# Don't match on name if targeting all entities
- name = None
+ entity_id = None
# Look up area first to fail early
- area_name = slots.get("area", {}).get("value")
+ area_slot = slots.get("area", {})
+ area_id = area_slot.get("value")
+ area_name = area_slot.get("text")
area: area_registry.AreaEntry | None = None
- if area_name is not None:
+ if area_id is not None:
areas = area_registry.async_get(hass)
- area = areas.async_get_area(area_name) or areas.async_get_area_by_name(
+ area = areas.async_get_area(area_id) or areas.async_get_area_by_name(
area_name
)
if area is None:
@@ -431,7 +436,7 @@ class ServiceIntentHandler(IntentHandler):
states = list(
async_match_states(
hass,
- name=name,
+ name=entity_id,
area=area,
domains=domains,
device_classes=device_classes,
@@ -442,8 +447,8 @@ class ServiceIntentHandler(IntentHandler):
if not states:
# No states matched constraints
raise NoStatesMatchedError(
- name=name,
- area=area_name,
+ name=entity_name or entity_id,
+ area=area_name or area_id,
domains=domains,
device_classes=device_classes,
)
diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py
index b9862907960..ba2486a196e 100644
--- a/homeassistant/helpers/json.py
+++ b/homeassistant/helpers/json.py
@@ -148,12 +148,17 @@ JSON_DUMP: Final = json_dumps
def _orjson_default_encoder(data: Any) -> str:
- """JSON encoder that uses orjson with hass defaults."""
+ """JSON encoder that uses orjson with hass defaults and returns a str."""
+ return _orjson_bytes_default_encoder(data).decode("utf-8")
+
+
+def _orjson_bytes_default_encoder(data: Any) -> bytes:
+ """JSON encoder that uses orjson with hass defaults and returns bytes."""
return orjson.dumps(
data,
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
default=json_encoder_default,
- ).decode("utf-8")
+ )
def save_json(
@@ -173,11 +178,13 @@ def save_json(
if encoder and encoder is not JSONEncoder:
# If they pass a custom encoder that is not the
# default JSONEncoder, we use the slow path of json.dumps
+ mode = "w"
dump = json.dumps
- json_data = json.dumps(data, indent=2, cls=encoder)
+ json_data: str | bytes = json.dumps(data, indent=2, cls=encoder)
else:
+ mode = "wb"
dump = _orjson_default_encoder
- json_data = _orjson_default_encoder(data)
+ json_data = _orjson_bytes_default_encoder(data)
except TypeError as error:
formatted_data = format_unserializable_data(
find_paths_unserializable_data(data, dump=dump)
@@ -186,10 +193,8 @@ def save_json(
_LOGGER.error(msg)
raise SerializationError(msg) from error
- if atomic_writes:
- write_utf8_file_atomic(filename, json_data, private)
- else:
- write_utf8_file(filename, json_data, private)
+ method = write_utf8_file_atomic if atomic_writes else write_utf8_file
+ method(filename, json_data, private, mode=mode)
def find_paths_unserializable_data(
diff --git a/homeassistant/helpers/redact.py b/homeassistant/helpers/redact.py
new file mode 100644
index 00000000000..f8df73b9180
--- /dev/null
+++ b/homeassistant/helpers/redact.py
@@ -0,0 +1,75 @@
+"""Helpers to redact sensitive data."""
+from __future__ import annotations
+
+from collections.abc import Callable, Iterable, Mapping
+from typing import Any, TypeVar, cast, overload
+
+from homeassistant.core import callback
+
+REDACTED = "**REDACTED**"
+
+_T = TypeVar("_T")
+_ValueT = TypeVar("_ValueT")
+
+
+def partial_redact(
+ x: str | Any, unmasked_prefix: int = 4, unmasked_suffix: int = 4
+) -> str:
+ """Mask part of a string with *."""
+ if not isinstance(x, str):
+ return REDACTED
+
+ unmasked = unmasked_prefix + unmasked_suffix
+ if len(x) < unmasked * 2:
+ return REDACTED
+
+ if not unmasked_prefix and not unmasked_suffix:
+ return REDACTED
+
+ suffix = x[-unmasked_suffix:] if unmasked_suffix else ""
+ return f"{x[:unmasked_prefix]}***{suffix}"
+
+
+@overload
+def async_redact_data( # type: ignore[overload-overlap]
+ data: Mapping, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]]
+) -> dict:
+ ...
+
+
+@overload
+def async_redact_data(
+ data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]]
+) -> _T:
+ ...
+
+
+@callback
+def async_redact_data(
+ data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]]
+) -> _T:
+ """Redact sensitive data in a dict."""
+ if not isinstance(data, (Mapping, list)):
+ return data
+
+ if isinstance(data, list):
+ return cast(_T, [async_redact_data(val, to_redact) for val in data])
+
+ redacted = {**data}
+
+ for key, value in redacted.items():
+ if value is None:
+ continue
+ if isinstance(value, str) and not value:
+ continue
+ if key in to_redact:
+ if isinstance(to_redact, Mapping):
+ redacted[key] = to_redact[key](value)
+ else:
+ redacted[key] = REDACTED
+ elif isinstance(value, Mapping):
+ redacted[key] = async_redact_data(value, to_redact)
+ elif isinstance(value, list):
+ redacted[key] = [async_redact_data(item, to_redact) for item in value]
+
+ return cast(_T, redacted)
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index 5a9786eb0fa..30516e3a099 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -608,6 +608,11 @@ async def async_get_all_descriptions(
# Files we loaded for missing descriptions
loaded: dict[str, JSON_TYPE] = {}
+ # We try to avoid making a copy in the event the cache is good,
+ # but now we must make a copy in case new services get added
+ # while we are loading the missing ones so we do not
+ # add the new ones to the cache without their descriptions
+ services = {domain: service.copy() for domain, service in services.items()}
if domains_with_missing_services:
ints_or_excs = await async_get_integrations(hass, domains_with_missing_services)
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index c5cea22795a..7746745da6b 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -19,17 +19,17 @@ bluetooth-data-tools==1.19.0
cached_ipaddress==0.3.0
certifi>=2021.5.30
ciso8601==2.3.0
-cryptography==42.0.1
+cryptography==42.0.2
dbus-fast==2.21.1
fnv-hash-fast==0.5.0
ha-av==10.1.1
ha-ffmpeg==3.1.0
habluetooth==2.4.0
hass-nabucasa==0.76.0
-hassil==1.6.0
+hassil==1.6.1
home-assistant-bluetooth==1.12.0
-home-assistant-frontend==20240131.0
-home-assistant-intents==2024.1.29
+home-assistant-frontend==20240202.0
+home-assistant-intents==2024.2.2
httpx==0.26.0
ifaddr==0.2.0
janus==1.0.0
diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py
index 06471eaca6a..1af65fa51d7 100644
--- a/homeassistant/util/file.py
+++ b/homeassistant/util/file.py
@@ -17,9 +17,7 @@ class WriteError(HomeAssistantError):
def write_utf8_file_atomic(
- filename: str,
- utf8_data: str,
- private: bool = False,
+ filename: str, utf8_data: bytes | str, private: bool = False, mode: str = "w"
) -> None:
"""Write a file and rename it into place using atomicwrites.
@@ -34,7 +32,7 @@ def write_utf8_file_atomic(
negatively impact performance.
"""
try:
- with AtomicWriter(filename, overwrite=True).open() as fdesc:
+ with AtomicWriter(filename, mode=mode, overwrite=True).open() as fdesc:
if not private:
os.fchmod(fdesc.fileno(), 0o644)
fdesc.write(utf8_data)
@@ -44,20 +42,18 @@ def write_utf8_file_atomic(
def write_utf8_file(
- filename: str,
- utf8_data: str,
- private: bool = False,
+ filename: str, utf8_data: bytes | str, private: bool = False, mode: str = "w"
) -> None:
"""Write a file and rename it into place.
Writes all or nothing.
"""
-
tmp_filename = ""
+ encoding = "utf-8" if "b" not in mode else None
try:
# Modern versions of Python tempfile create this file with mode 0o600
with tempfile.NamedTemporaryFile(
- mode="w", encoding="utf-8", dir=os.path.dirname(filename), delete=False
+ mode=mode, encoding=encoding, dir=os.path.dirname(filename), delete=False
) as fdesc:
fdesc.write(utf8_data)
tmp_filename = fdesc.name
diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py
index 630c39b3ad4..65f93020cc6 100644
--- a/homeassistant/util/json.py
+++ b/homeassistant/util/json.py
@@ -74,7 +74,7 @@ def load_json(
Defaults to returning empty dict if file is not found.
"""
try:
- with open(filename, encoding="utf-8") as fdesc:
+ with open(filename, mode="rb") as fdesc:
return orjson.loads(fdesc.read()) # type: ignore[no-any-return]
except FileNotFoundError:
# This is not a fatal error
diff --git a/pyproject.toml b/pyproject.toml
index d7680e5e871..24a50508722 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,7 @@ dependencies = [
"lru-dict==1.3.0",
"PyJWT==2.8.0",
# PyJWT has loose dependency. We want the latest one.
- "cryptography==42.0.1",
+ "cryptography==42.0.2",
# pyOpenSSL 23.2.0 is required to work with cryptography 41+
"pyOpenSSL==24.0.0",
"orjson==3.9.12",
diff --git a/requirements.txt b/requirements.txt
index 67aad2e9f0d..066855e718b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,7 +20,7 @@ ifaddr==0.2.0
Jinja2==3.1.3
lru-dict==1.3.0
PyJWT==2.8.0
-cryptography==42.0.1
+cryptography==42.0.2
pyOpenSSL==24.0.0
orjson==3.9.12
packaging>=23.1
diff --git a/requirements_all.txt b/requirements_all.txt
index da3092eab7a..a501710a518 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -233,7 +233,7 @@ aioeagle==1.1.0
aioecowitt==2023.5.0
# homeassistant.components.co2signal
-aioelectricitymaps==0.2.0
+aioelectricitymaps==0.3.0
# homeassistant.components.emonitor
aioemonitor==1.0.5
@@ -257,7 +257,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.homekit_controller
-aiohomekit==3.1.3
+aiohomekit==3.1.4
# homeassistant.components.http
aiohttp-fast-url-dispatcher==0.3.0
@@ -377,7 +377,7 @@ aioswitcher==3.4.1
aiosyncthing==0.5.1
# homeassistant.components.tankerkoenig
-aiotankerkoenig==0.2.0
+aiotankerkoenig==0.3.0
# homeassistant.components.tractive
aiotractive==0.5.6
@@ -684,7 +684,7 @@ debugpy==1.8.0
# decora==0.6
# homeassistant.components.ecovacs
-deebot-client==5.0.0
+deebot-client==5.1.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -758,7 +758,7 @@ ecoaliface==0.4.0
electrickiwi-api==0.8.5
# homeassistant.components.elgato
-elgato==5.1.1
+elgato==5.1.2
# homeassistant.components.eliqonline
eliqonline==1.2.2
@@ -1025,7 +1025,7 @@ hass-nabucasa==0.76.0
hass-splunk==0.1.1
# homeassistant.components.conversation
-hassil==1.6.0
+hassil==1.6.1
# homeassistant.components.jewish_calendar
hdate==0.10.4
@@ -1059,10 +1059,10 @@ hole==0.8.0
holidays==0.41
# homeassistant.components.frontend
-home-assistant-frontend==20240131.0
+home-assistant-frontend==20240202.0
# homeassistant.components.conversation
-home-assistant-intents==2024.1.29
+home-assistant-intents==2024.2.2
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -1256,7 +1256,7 @@ lxml==5.1.0
mac-vendor-lookup==0.1.12
# homeassistant.components.matrix
-matrix-nio==0.22.1
+matrix-nio==0.24.0
# homeassistant.components.maxcube
maxcube-api==0.4.3
@@ -2232,7 +2232,7 @@ python-join-api==0.0.9
python-juicenet==1.1.0
# homeassistant.components.tplink
-python-kasa[speedups]==0.6.2
+python-kasa[speedups]==0.6.2.1
# homeassistant.components.lirc
# python-lirc==1.2.3
@@ -2514,7 +2514,7 @@ sensorpro-ble==0.5.3
sensorpush-ble==1.6.2
# homeassistant.components.sentry
-sentry-sdk==1.39.2
+sentry-sdk==1.40.0
# homeassistant.components.sfr_box
sfrbox-api==0.0.8
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f8da20de5f7..b64fcb944eb 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -212,7 +212,7 @@ aioeagle==1.1.0
aioecowitt==2023.5.0
# homeassistant.components.co2signal
-aioelectricitymaps==0.2.0
+aioelectricitymaps==0.3.0
# homeassistant.components.emonitor
aioemonitor==1.0.5
@@ -233,7 +233,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.homekit_controller
-aiohomekit==3.1.3
+aiohomekit==3.1.4
# homeassistant.components.http
aiohttp-fast-url-dispatcher==0.3.0
@@ -350,7 +350,7 @@ aioswitcher==3.4.1
aiosyncthing==0.5.1
# homeassistant.components.tankerkoenig
-aiotankerkoenig==0.2.0
+aiotankerkoenig==0.3.0
# homeassistant.components.tractive
aiotractive==0.5.6
@@ -559,7 +559,7 @@ dbus-fast==2.21.1
debugpy==1.8.0
# homeassistant.components.ecovacs
-deebot-client==5.0.0
+deebot-client==5.1.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -618,7 +618,7 @@ easyenergy==2.1.0
electrickiwi-api==0.8.5
# homeassistant.components.elgato
-elgato==5.1.1
+elgato==5.1.2
# homeassistant.components.elkm1
elkm1-lib==2.2.6
@@ -830,7 +830,7 @@ habluetooth==2.4.0
hass-nabucasa==0.76.0
# homeassistant.components.conversation
-hassil==1.6.0
+hassil==1.6.1
# homeassistant.components.jewish_calendar
hdate==0.10.4
@@ -855,10 +855,10 @@ hole==0.8.0
holidays==0.41
# homeassistant.components.frontend
-home-assistant-frontend==20240131.0
+home-assistant-frontend==20240202.0
# homeassistant.components.conversation
-home-assistant-intents==2024.1.29
+home-assistant-intents==2024.2.2
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -998,7 +998,7 @@ lxml==5.1.0
mac-vendor-lookup==0.1.12
# homeassistant.components.matrix
-matrix-nio==0.22.1
+matrix-nio==0.24.0
# homeassistant.components.maxcube
maxcube-api==0.4.3
@@ -1708,7 +1708,7 @@ python-izone==1.2.9
python-juicenet==1.1.0
# homeassistant.components.tplink
-python-kasa[speedups]==0.6.2
+python-kasa[speedups]==0.6.2.1
# homeassistant.components.matter
python-matter-server==5.4.0
@@ -1918,7 +1918,7 @@ sensorpro-ble==0.5.3
sensorpush-ble==1.6.2
# homeassistant.components.sentry
-sentry-sdk==1.39.2
+sentry-sdk==1.40.0
# homeassistant.components.sfr_box
sfrbox-api==0.0.8
diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py
index da0c312bf28..231ec12cb5f 100644
--- a/tests/components/airthings_ble/__init__.py
+++ b/tests/components/airthings_ble/__init__.py
@@ -93,6 +93,50 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak(
time=0,
)
+VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak(
+ name="cc-cc-cc-cc-cc-cc",
+ address="cc:cc:cc:cc:cc:cc",
+ device=generate_ble_device(
+ address="cc:cc:cc:cc:cc:cc",
+ name="Airthings View Plus",
+ ),
+ rssi=-61,
+ manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
+ service_data={
+ "b42eb4a6-ade7-11e4-89d3-123b93f75cba": bytearray(
+ b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A"
+ ),
+ # Manufacturer
+ "00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"),
+ # Model
+ "00002a24-0000-1000-8000-00805f9b34fb": bytearray(b"2960"),
+ # Identifier
+ "00002a25-0000-1000-8000-00805f9b34fb": bytearray(b"123456"),
+ # SW Version
+ "00002a26-0000-1000-8000-00805f9b34fb": bytearray(b"A-BLE-1.12.1-master+0"),
+ # HW Version
+ "00002a27-0000-1000-8000-00805f9b34fb": bytearray(b"REV 1,0"),
+ },
+ service_uuids=[
+ "b42eb4a6-ade7-11e4-89d3-123b93f75cba",
+ "b42e90a2-ade7-11e4-89d3-123b93f75cba",
+ "b42e2a68-ade7-11e4-89d3-123b93f75cba",
+ "00002a29-0000-1000-8000-00805f9b34fb",
+ "00002a24-0000-1000-8000-00805f9b34fb",
+ "00002a25-0000-1000-8000-00805f9b34fb",
+ "00002a26-0000-1000-8000-00805f9b34fb",
+ "00002a27-0000-1000-8000-00805f9b34fb",
+ "b42e2d06-ade7-11e4-89d3-123b93f75cba",
+ ],
+ source="local",
+ advertisement=generate_advertisement_data(
+ manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
+ service_uuids=["b42e90a2-ade7-11e4-89d3-123b93f75cba"],
+ ),
+ connectable=True,
+ time=0,
+)
+
UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak(
name="unknown",
address="00:cc:cc:cc:cc:cc",
diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py
index bc009f03027..65ec91e69c2 100644
--- a/tests/components/airthings_ble/test_config_flow.py
+++ b/tests/components/airthings_ble/test_config_flow.py
@@ -12,6 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType
from . import (
UNKNOWN_SERVICE_INFO,
+ VIEW_PLUS_SERVICE_INFO,
WAVE_DEVICE_INFO,
WAVE_SERVICE_INFO,
patch_airthings_ble,
@@ -204,3 +205,16 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None:
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
+
+
+async def test_unsupported_device(hass: HomeAssistant) -> None:
+ """Test the user initiated form with an unsupported device."""
+ with patch(
+ "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info",
+ return_value=[UNKNOWN_SERVICE_INFO, VIEW_PLUS_SERVICE_INFO],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == FlowResultType.ABORT
+ assert result["reason"] == "no_devices_found"
diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py
index 5b1ade1ee49..29ce783f33a 100644
--- a/tests/components/co2signal/test_config_flow.py
+++ b/tests/components/co2signal/test_config_flow.py
@@ -1,10 +1,10 @@
"""Test the CO2 Signal config flow."""
from unittest.mock import AsyncMock, patch
-from aioelectricitymaps.exceptions import (
- ElectricityMapsDecodeError,
+from aioelectricitymaps import (
+ ElectricityMapsConnectionError,
ElectricityMapsError,
- InvalidToken,
+ ElectricityMapsInvalidTokenError,
)
import pytest
@@ -134,11 +134,11 @@ async def test_form_country(hass: HomeAssistant) -> None:
("side_effect", "err_code"),
[
(
- InvalidToken,
+ ElectricityMapsInvalidTokenError,
"invalid_auth",
),
(ElectricityMapsError("Something else"), "unknown"),
- (ElectricityMapsDecodeError("Boom"), "unknown"),
+ (ElectricityMapsConnectionError("Boom"), "unknown"),
],
ids=[
"invalid auth",
diff --git a/tests/components/co2signal/test_sensor.py b/tests/components/co2signal/test_sensor.py
index b79c8e04c23..4d663e1026b 100644
--- a/tests/components/co2signal/test_sensor.py
+++ b/tests/components/co2signal/test_sensor.py
@@ -2,10 +2,11 @@
from datetime import timedelta
from unittest.mock import AsyncMock
-from aioelectricitymaps.exceptions import (
- ElectricityMapsDecodeError,
+from aioelectricitymaps import (
+ ElectricityMapsConnectionError,
+ ElectricityMapsConnectionTimeoutError,
ElectricityMapsError,
- InvalidToken,
+ ElectricityMapsInvalidTokenError,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
@@ -42,7 +43,8 @@ async def test_sensor(
@pytest.mark.parametrize(
"error",
[
- ElectricityMapsDecodeError,
+ ElectricityMapsConnectionTimeoutError,
+ ElectricityMapsConnectionError,
ElectricityMapsError,
Exception,
],
@@ -93,8 +95,12 @@ async def test_sensor_reauth_triggered(
assert (state := hass.states.get("sensor.electricity_maps_co2_intensity"))
assert state.state == "45.9862319009581"
- electricity_maps.latest_carbon_intensity_by_coordinates.side_effect = InvalidToken
- electricity_maps.latest_carbon_intensity_by_country_code.side_effect = InvalidToken
+ electricity_maps.latest_carbon_intensity_by_coordinates.side_effect = (
+ ElectricityMapsInvalidTokenError
+ )
+ electricity_maps.latest_carbon_intensity_by_country_code.side_effect = (
+ ElectricityMapsInvalidTokenError
+ )
freezer.tick(timedelta(minutes=20))
async_fire_time_changed(hass)
diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr
index 23dab0902a9..034bfafc1f5 100644
--- a/tests/components/conversation/snapshots/test_init.ambr
+++ b/tests/components/conversation/snapshots/test_init.ambr
@@ -339,7 +339,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'An unexpected error occurred while handling the intent',
+ 'speech': 'An unexpected error occurred',
}),
}),
}),
@@ -379,7 +379,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'An unexpected error occurred while handling the intent',
+ 'speech': 'An unexpected error occurred',
}),
}),
}),
@@ -519,7 +519,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called late added alias',
+ 'speech': 'Sorry, I am not aware of any device called late added alias',
}),
}),
}),
@@ -539,7 +539,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
+ 'speech': 'Sorry, I am not aware of any device called kitchen light',
}),
}),
}),
@@ -679,7 +679,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called late added light',
+ 'speech': 'Sorry, I am not aware of any device called late added light',
}),
}),
}),
@@ -759,7 +759,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
+ 'speech': 'Sorry, I am not aware of any device called kitchen light',
}),
}),
}),
@@ -779,7 +779,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called my cool light',
+ 'speech': 'Sorry, I am not aware of any device called my cool light',
}),
}),
}),
@@ -919,7 +919,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
+ 'speech': 'Sorry, I am not aware of any device called kitchen light',
}),
}),
}),
@@ -969,7 +969,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any device or entity called renamed light',
+ 'speech': 'Sorry, I am not aware of any device called renamed light',
}),
}),
}),
@@ -1397,7 +1397,7 @@
'name': dict({
'name': 'name',
'text': 'my cool light',
- 'value': 'my cool light',
+ 'value': 'light.kitchen',
}),
}),
'intent': dict({
@@ -1422,7 +1422,7 @@
'name': dict({
'name': 'name',
'text': 'my cool light',
- 'value': 'my cool light',
+ 'value': 'light.kitchen',
}),
}),
'intent': dict({
@@ -1498,7 +1498,7 @@
'sentence_template': '[tell me] how many {on_off_domains:domain} (is|are) {on_off_states:state} [in ]',
'slots': dict({
'area': 'kitchen',
- 'domain': 'light',
+ 'domain': 'lights',
'state': 'on',
}),
'source': 'builtin',
@@ -1572,7 +1572,7 @@
'name': dict({
'name': 'name',
'text': 'test light',
- 'value': 'test light',
+ 'value': 'light.demo_1234',
}),
}),
'intent': dict({
@@ -1581,7 +1581,7 @@
'match': True,
'sentence_template': '[] brightness [to] ',
'slots': dict({
- 'brightness': 100,
+ 'brightness': '100%',
'name': 'test light',
}),
'source': 'builtin',
@@ -1604,7 +1604,7 @@
'name': dict({
'name': 'name',
'text': 'test light',
- 'value': 'test light',
+ 'value': 'light.demo_1234',
}),
}),
'intent': dict({
diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py
index b992b0086d7..0cf343a3e20 100644
--- a/tests/components/conversation/test_default_agent.py
+++ b/tests/components/conversation/test_default_agent.py
@@ -1,7 +1,9 @@
"""Test for the default agent."""
+
from collections import defaultdict
from unittest.mock import AsyncMock, patch
+from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult
import pytest
from homeassistant.components import conversation
@@ -84,8 +86,10 @@ async def test_exposed_areas(
entity_registry: er.EntityRegistry,
) -> None:
"""Test that all areas are exposed."""
- area_kitchen = area_registry.async_get_or_create("kitchen")
- area_bedroom = area_registry.async_get_or_create("bedroom")
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
+ area_bedroom = area_registry.async_get_or_create("bedroom_id")
+ area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
entry = MockConfigEntry()
entry.add_to_hass(hass)
@@ -121,6 +125,9 @@ async def test_exposed_areas(
# All is well for the exposed kitchen light
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
+ assert result.response.intent is not None
+ assert result.response.intent.slots["area"]["value"] == area_kitchen.id
+ assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
# Bedroom has no exposed entities
result = await conversation.async_converse(
@@ -194,7 +201,8 @@ async def test_unexposed_entities_skipped(
entity_registry: er.EntityRegistry,
) -> None:
"""Test that unexposed entities are skipped in exposed areas."""
- area_kitchen = area_registry.async_get_or_create("kitchen")
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
# Both lights are in the kitchen
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
@@ -223,6 +231,9 @@ async def test_unexposed_entities_skipped(
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
+ assert result.response.intent is not None
+ assert result.response.intent.slots["area"]["value"] == area_kitchen.id
+ assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
# Only one light should be returned
hass.states.async_set(exposed_light.entity_id, "on")
@@ -313,8 +324,10 @@ async def test_device_area_context(
turn_on_calls = async_mock_service(hass, "light", "turn_on")
turn_off_calls = async_mock_service(hass, "light", "turn_off")
- area_kitchen = area_registry.async_get_or_create("Kitchen")
- area_bedroom = area_registry.async_get_or_create("Bedroom")
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
+ area_bedroom = area_registry.async_get_or_create("bedroom_id")
+ area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
# Create 2 lights in each area
area_lights = defaultdict(list)
@@ -362,13 +375,14 @@ async def test_device_area_context(
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
+ assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
# Verify only kitchen lights were targeted
assert {s.entity_id for s in result.response.matched_states} == {
- e.entity_id for e in area_lights["kitchen"]
+ e.entity_id for e in area_lights[area_kitchen.id]
}
assert {c.data["entity_id"][0] for c in turn_on_calls} == {
- e.entity_id for e in area_lights["kitchen"]
+ e.entity_id for e in area_lights[area_kitchen.id]
}
turn_on_calls.clear()
@@ -385,13 +399,14 @@ async def test_device_area_context(
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
+ assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
# Verify only bedroom lights were targeted
assert {s.entity_id for s in result.response.matched_states} == {
- e.entity_id for e in area_lights["bedroom"]
+ e.entity_id for e in area_lights[area_bedroom.id]
}
assert {c.data["entity_id"][0] for c in turn_on_calls} == {
- e.entity_id for e in area_lights["bedroom"]
+ e.entity_id for e in area_lights[area_bedroom.id]
}
turn_on_calls.clear()
@@ -408,13 +423,14 @@ async def test_device_area_context(
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
+ assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
# Verify only bedroom lights were targeted
assert {s.entity_id for s in result.response.matched_states} == {
- e.entity_id for e in area_lights["bedroom"]
+ e.entity_id for e in area_lights[area_bedroom.id]
}
assert {c.data["entity_id"][0] for c in turn_off_calls} == {
- e.entity_id for e in area_lights["bedroom"]
+ e.entity_id for e in area_lights[area_bedroom.id]
}
turn_off_calls.clear()
@@ -430,8 +446,8 @@ async def test_device_area_context(
)
-async def test_error_missing_entity(hass: HomeAssistant, init_components) -> None:
- """Test error message when entity is missing."""
+async def test_error_no_device(hass: HomeAssistant, init_components) -> None:
+ """Test error message when device/entity is missing."""
result = await conversation.async_converse(
hass, "turn on missing entity", None, Context(), None
)
@@ -440,11 +456,11 @@ async def test_error_missing_entity(hass: HomeAssistant, init_components) -> Non
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
- == "Sorry, I am not aware of any device or entity called missing entity"
+ == "Sorry, I am not aware of any device called missing entity"
)
-async def test_error_missing_area(hass: HomeAssistant, init_components) -> None:
+async def test_error_no_area(hass: HomeAssistant, init_components) -> None:
"""Test error message when area is missing."""
result = await conversation.async_converse(
hass, "turn on the lights in missing area", None, Context(), None
@@ -458,11 +474,63 @@ async def test_error_missing_area(hass: HomeAssistant, init_components) -> None:
)
-async def test_error_no_exposed_for_domain(
+async def test_error_no_device_in_area(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
- """Test error message when no entities for a domain are exposed in an area."""
- area_registry.async_get_or_create("kitchen")
+ """Test error message when area is missing a device/entity."""
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
+ result = await conversation.async_converse(
+ hass, "turn on missing entity in the kitchen", None, Context(), None
+ )
+
+ assert result.response.response_type == intent.IntentResponseType.ERROR
+ assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
+ assert (
+ result.response.speech["plain"]["speech"]
+ == "Sorry, I am not aware of any device called missing entity in the kitchen area"
+ )
+
+
+async def test_error_no_domain(
+ hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
+) -> None:
+ """Test error message when no devices/entities exist for a domain."""
+
+ # We don't have a sentence for turning on all fans
+ fan_domain = MatchEntity(name="domain", value="fan", text="fans")
+ recognize_result = RecognizeResult(
+ intent=Intent("HassTurnOn"),
+ intent_data=IntentData([]),
+ entities={"domain": fan_domain},
+ entities_list=[fan_domain],
+ )
+
+ with patch(
+ "homeassistant.components.conversation.default_agent.recognize_all",
+ return_value=[recognize_result],
+ ):
+ result = await conversation.async_converse(
+ hass, "turn on the fans", None, Context(), None
+ )
+
+ assert result.response.response_type == intent.IntentResponseType.ERROR
+ assert (
+ result.response.error_code
+ == intent.IntentResponseErrorCode.NO_VALID_TARGETS
+ )
+ assert (
+ result.response.speech["plain"]["speech"]
+ == "Sorry, I am not aware of any fan"
+ )
+
+
+async def test_error_no_domain_in_area(
+ hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
+) -> None:
+ """Test error message when no devices/entities for a domain exist in an area."""
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
result = await conversation.async_converse(
hass, "turn on the lights in the kitchen", None, Context(), None
)
@@ -475,11 +543,43 @@ async def test_error_no_exposed_for_domain(
)
-async def test_error_no_exposed_for_device_class(
+async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None:
+ """Test error message when no entities of a device class exist."""
+
+ # We don't have a sentence for opening all windows
+ window_class = MatchEntity(name="device_class", value="window", text="windows")
+ recognize_result = RecognizeResult(
+ intent=Intent("HassTurnOn"),
+ intent_data=IntentData([]),
+ entities={"device_class": window_class},
+ entities_list=[window_class],
+ )
+
+ with patch(
+ "homeassistant.components.conversation.default_agent.recognize_all",
+ return_value=[recognize_result],
+ ):
+ result = await conversation.async_converse(
+ hass, "open the windows", None, Context(), None
+ )
+
+ assert result.response.response_type == intent.IntentResponseType.ERROR
+ assert (
+ result.response.error_code
+ == intent.IntentResponseErrorCode.NO_VALID_TARGETS
+ )
+ assert (
+ result.response.speech["plain"]["speech"]
+ == "Sorry, I am not aware of any window"
+ )
+
+
+async def test_error_no_device_class_in_area(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
- """Test error message when no entities of a device class are exposed in an area."""
- area_registry.async_get_or_create("bedroom")
+ """Test error message when no entities of a device class exist in an area."""
+ area_bedroom = area_registry.async_get_or_create("bedroom_id")
+ area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
result = await conversation.async_converse(
hass, "open bedroom windows", None, Context(), None
)
@@ -492,8 +592,8 @@ async def test_error_no_exposed_for_device_class(
)
-async def test_error_match_failure(hass: HomeAssistant, init_components) -> None:
- """Test response with complete match failure."""
+async def test_error_no_intent(hass: HomeAssistant, init_components) -> None:
+ """Test response with an intent match failure."""
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[],
@@ -506,13 +606,18 @@ async def test_error_match_failure(hass: HomeAssistant, init_components) -> None
assert (
result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
)
+ assert (
+ result.response.speech["plain"]["speech"]
+ == "Sorry, I couldn't understand that"
+ )
async def test_no_states_matched_default_error(
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
) -> None:
"""Test default response when no states match and slots are missing."""
- area_registry.async_get_or_create("kitchen")
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
with patch(
"homeassistant.components.conversation.default_agent.intent.async_handle",
@@ -541,9 +646,9 @@ async def test_empty_aliases(
entity_registry: er.EntityRegistry,
) -> None:
"""Test that empty aliases are not added to slot lists."""
- area_kitchen = area_registry.async_get_or_create("kitchen")
- assert area_kitchen.id is not None
- area_registry.async_update(area_kitchen.id, aliases={" "})
+ area_kitchen = area_registry.async_get_or_create("kitchen_id")
+ area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
+ area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "})
entry = MockConfigEntry()
entry.add_to_hass(hass)
@@ -555,11 +660,16 @@ async def test_empty_aliases(
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
- entity_registry.async_update_entity(
- kitchen_light.entity_id, device_id=kitchen_device.id, aliases={" "}
+ kitchen_light = entity_registry.async_update_entity(
+ kitchen_light.entity_id,
+ device_id=kitchen_device.id,
+ name="kitchen light",
+ aliases={" "},
)
hass.states.async_set(
- kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
+ kitchen_light.entity_id,
+ "on",
+ attributes={ATTR_FRIENDLY_NAME: kitchen_light.name},
)
with patch(
@@ -577,16 +687,16 @@ async def test_empty_aliases(
assert slot_lists.keys() == {"area", "name"}
areas = slot_lists["area"]
assert len(areas.values) == 1
- assert areas.values[0].value_out == "kitchen"
+ assert areas.values[0].value_out == area_kitchen.id
+ assert areas.values[0].text_in.text == area_kitchen.normalized_name
names = slot_lists["name"]
assert len(names.values) == 1
- assert names.values[0].value_out == "kitchen light"
+ assert names.values[0].value_out == kitchen_light.entity_id
+ assert names.values[0].text_in.text == kitchen_light.name
-async def test_all_domains_loaded(
- hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
-) -> None:
+async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None:
"""Test that sentences for all domains are always loaded."""
# light domain is not loaded
@@ -601,5 +711,5 @@ async def test_all_domains_loaded(
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
- == "Sorry, I am not aware of any device or entity called test light"
+ == "Sorry, I am not aware of any device called test light"
)
diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py
index 58e94d27aac..61712761250 100644
--- a/tests/components/conversation/test_init.py
+++ b/tests/components/conversation/test_init.py
@@ -733,7 +733,7 @@ async def test_ws_api(
assert await async_setup_component(hass, "conversation", {})
client = await hass_ws_client(hass)
- await client.send_json({"id": 5, "type": "conversation/process", **payload})
+ await client.send_json_auto_id({"type": "conversation/process", **payload})
msg = await client.receive_json()
@@ -757,18 +757,14 @@ async def test_ws_prepare(
client = await hass_ws_client(hass)
- msg = {
- "id": 5,
- "type": "conversation/prepare",
- }
+ msg = {"type": "conversation/prepare"}
if agent_id is not None:
msg["agent_id"] = agent_id
- await client.send_json(msg)
+ await client.send_json_auto_id(msg)
msg = await client.receive_json()
assert msg["success"]
- assert msg["id"] == 5
# Intents should now be load
assert agent._lang_intents.get(hass.config.language)
diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py
index e40c7554fdd..74df1b7f8a6 100644
--- a/tests/components/conversation/test_trigger.py
+++ b/tests/components/conversation/test_trigger.py
@@ -7,6 +7,7 @@ from homeassistant.helpers import trigger
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
+from tests.typing import WebSocketGenerator
@pytest.fixture
@@ -99,6 +100,62 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None:
assert service_response["response"]["speech"]["plain"]["speech"] == response
+async def test_subscribe_trigger_does_not_interfere_with_responses(
+ hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator
+) -> None:
+ """Test that subscribing to a trigger from the websocket API does not interfere with responses."""
+ websocket_client = await hass_ws_client()
+ await websocket_client.send_json_auto_id(
+ {
+ "type": "subscribe_trigger",
+ "trigger": {"platform": "conversation", "command": ["test sentence"]},
+ }
+ )
+
+ service_response = await hass.services.async_call(
+ "conversation",
+ "process",
+ {
+ "text": "test sentence",
+ },
+ blocking=True,
+ return_response=True,
+ )
+
+ # Default response, since no automations with responses are registered
+ assert service_response["response"]["speech"]["plain"]["speech"] == "Done"
+
+ # Now register a trigger with a response
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation test1": {
+ "trigger": {
+ "platform": "conversation",
+ "command": ["test sentence"],
+ },
+ "action": {
+ "set_conversation_response": "test response",
+ },
+ }
+ },
+ )
+
+ service_response = await hass.services.async_call(
+ "conversation",
+ "process",
+ {
+ "text": "test sentence",
+ },
+ blocking=True,
+ return_response=True,
+ )
+
+ # Response will now come through
+ assert service_response["response"]["speech"]["plain"]["speech"] == "test response"
+
+
async def test_same_trigger_multiple_sentences(
hass: HomeAssistant, calls, setup_comp
) -> None:
diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr
index ca61d16602a..45b7ef1cc51 100644
--- a/tests/components/ecovacs/snapshots/test_button.ambr
+++ b/tests/components/ecovacs/snapshots/test_button.ambr
@@ -42,49 +42,6 @@
'state': '2024-01-01T00:00:00+00:00',
})
# ---
-# name: test_buttons[yna5x1][button.ozmo_950_reset_brush_lifespan:entity-registry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'button',
- 'entity_category': ,
- 'entity_id': 'button.ozmo_950_reset_brush_lifespan',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Reset brush lifespan',
- 'platform': 'ecovacs',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'reset_lifespan_brush',
- 'unique_id': 'E1234567890000000001_reset_lifespan_brush',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_buttons[yna5x1][button.ozmo_950_reset_brush_lifespan:state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'Ozmo 950 Reset brush lifespan',
- }),
- 'context': ,
- 'entity_id': 'button.ozmo_950_reset_brush_lifespan',
- 'last_changed': ,
- 'last_updated': ,
- 'state': '2024-01-01T00:00:00+00:00',
- })
-# ---
# name: test_buttons[yna5x1][button.ozmo_950_reset_filter_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -128,7 +85,7 @@
'state': '2024-01-01T00:00:00+00:00',
})
# ---
-# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:entity-registry]
+# name: test_buttons[yna5x1][button.ozmo_950_reset_main_brush_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -140,7 +97,7 @@
'disabled_by': None,
'domain': 'button',
'entity_category': ,
- 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan',
+ 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -150,7 +107,50 @@
}),
'original_device_class': None,
'original_icon': None,
- 'original_name': 'Reset side brush lifespan',
+ 'original_name': 'Reset main brush lifespan',
+ 'platform': 'ecovacs',
+ 'previous_unique_id': None,
+ 'supported_features': 0,
+ 'translation_key': 'reset_lifespan_brush',
+ 'unique_id': 'E1234567890000000001_reset_lifespan_brush',
+ 'unit_of_measurement': None,
+ })
+# ---
+# name: test_buttons[yna5x1][button.ozmo_950_reset_main_brush_lifespan:state]
+ StateSnapshot({
+ 'attributes': ReadOnlyDict({
+ 'friendly_name': 'Ozmo 950 Reset main brush lifespan',
+ }),
+ 'context': ,
+ 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan',
+ 'last_changed': ,
+ 'last_updated': ,
+ 'state': '2024-01-01T00:00:00+00:00',
+ })
+# ---
+# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:entity-registry]
+ EntityRegistryEntrySnapshot({
+ 'aliases': set({
+ }),
+ 'area_id': None,
+ 'capabilities': None,
+ 'config_entry_id': ,
+ 'device_class': None,
+ 'device_id': ,
+ 'disabled_by': None,
+ 'domain': 'button',
+ 'entity_category': ,
+ 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan',
+ 'has_entity_name': True,
+ 'hidden_by': None,
+ 'icon': None,
+ 'id': ,
+ 'name': None,
+ 'options': dict({
+ }),
+ 'original_device_class': None,
+ 'original_icon': None,
+ 'original_name': 'Reset side brushes lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
@@ -159,13 +159,13 @@
'unit_of_measurement': None,
})
# ---
-# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:state]
+# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'friendly_name': 'Ozmo 950 Reset side brush lifespan',
+ 'friendly_name': 'Ozmo 950 Reset side brushes lifespan',
}),
'context': ,
- 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan',
+ 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan',
'last_changed': ,
'last_updated': ,
'state': '2024-01-01T00:00:00+00:00',
diff --git a/tests/components/ecovacs/snapshots/test_select.ambr b/tests/components/ecovacs/snapshots/test_select.ambr
index abf37a17256..4b01d448fd8 100644
--- a/tests/components/ecovacs/snapshots/test_select.ambr
+++ b/tests/components/ecovacs/snapshots/test_select.ambr
@@ -1,5 +1,5 @@
# serializer version: 1
-# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_amount:entity-registry]
+# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -18,7 +18,7 @@
'disabled_by': None,
'domain': 'select',
'entity_category': ,
- 'entity_id': 'select.ozmo_950_water_amount',
+ 'entity_id': 'select.ozmo_950_water_flow_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -28,7 +28,7 @@
}),
'original_device_class': None,
'original_icon': None,
- 'original_name': 'Water amount',
+ 'original_name': 'Water flow level',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
@@ -37,10 +37,10 @@
'unit_of_measurement': None,
})
# ---
-# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_amount:state]
+# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'friendly_name': 'Ozmo 950 Water amount',
+ 'friendly_name': 'Ozmo 950 Water flow level',
'options': list([
'low',
'medium',
@@ -49,7 +49,7 @@
]),
}),
'context': ,
- 'entity_id': 'select.ozmo_950_water_amount',
+ 'entity_id': 'select.ozmo_950_water_flow_level',
'last_changed': ,
'last_updated': ,
'state': 'ultrahigh',
diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr
index 3a59b3ba418..f07722afb53 100644
--- a/tests/components/ecovacs/snapshots/test_sensor.ambr
+++ b/tests/components/ecovacs/snapshots/test_sensor.ambr
@@ -88,7 +88,7 @@
'state': '100',
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_brush_lifespan:entity-registry]
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -99,37 +99,41 @@
'device_id': ,
'disabled_by': None,
'domain': 'sensor',
- 'entity_category': ,
- 'entity_id': 'sensor.ozmo_950_brush_lifespan',
+ 'entity_category': None,
+ 'entity_id': 'sensor.ozmo_950_cleaning_duration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': ,
'name': None,
'options': dict({
+ 'sensor.private': dict({
+ 'suggested_unit_of_measurement': ,
+ }),
}),
- 'original_device_class': None,
+ 'original_device_class': ,
'original_icon': None,
- 'original_name': 'Brush lifespan',
+ 'original_name': 'Cleaning duration',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'lifespan_brush',
- 'unique_id': 'E1234567890000000001_lifespan_brush',
- 'unit_of_measurement': '%',
+ 'translation_key': 'stats_time',
+ 'unique_id': 'E1234567890000000001_stats_time',
+ 'unit_of_measurement': ,
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_brush_lifespan:state]
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'friendly_name': 'Ozmo 950 Brush lifespan',
- 'unit_of_measurement': '%',
+ 'device_class': 'duration',
+ 'friendly_name': 'Ozmo 950 Cleaning duration',
+ 'unit_of_measurement': ,
}),
'context': ,
- 'entity_id': 'sensor.ozmo_950_brush_lifespan',
+ 'entity_id': 'sensor.ozmo_950_cleaning_duration',
'last_changed': ,
'last_updated': ,
- 'state': '80',
+ 'state': '5.0',
})
# ---
# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:entity-registry]
@@ -263,7 +267,7 @@
'state': '192.168.0.10',
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brush_lifespan:entity-registry]
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -275,7 +279,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': ,
- 'entity_id': 'sensor.ozmo_950_side_brush_lifespan',
+ 'entity_id': 'sensor.ozmo_950_main_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -285,29 +289,29 @@
}),
'original_device_class': None,
'original_icon': None,
- 'original_name': 'Side brush lifespan',
+ 'original_name': 'Main brush lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'lifespan_side_brush',
- 'unique_id': 'E1234567890000000001_lifespan_side_brush',
+ 'translation_key': 'lifespan_brush',
+ 'unique_id': 'E1234567890000000001_lifespan_brush',
'unit_of_measurement': '%',
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brush_lifespan:state]
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'friendly_name': 'Ozmo 950 Side brush lifespan',
+ 'friendly_name': 'Ozmo 950 Main brush lifespan',
'unit_of_measurement': '%',
}),
'context': ,
- 'entity_id': 'sensor.ozmo_950_side_brush_lifespan',
+ 'entity_id': 'sensor.ozmo_950_main_brush_lifespan',
'last_changed': ,
'last_updated': ,
- 'state': '40',
+ 'state': '80',
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_time_cleaned:entity-registry]
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -318,41 +322,37 @@
'device_id': ,
'disabled_by': None,
'domain': 'sensor',
- 'entity_category': None,
- 'entity_id': 'sensor.ozmo_950_time_cleaned',
+ 'entity_category': ,
+ 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': ,
'name': None,
'options': dict({
- 'sensor.private': dict({
- 'suggested_unit_of_measurement': ,
- }),
}),
- 'original_device_class': ,
+ 'original_device_class': None,
'original_icon': None,
- 'original_name': 'Time cleaned',
+ 'original_name': 'Side brushes lifespan',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'stats_time',
- 'unique_id': 'E1234567890000000001_stats_time',
- 'unit_of_measurement': ,
+ 'translation_key': 'lifespan_side_brush',
+ 'unique_id': 'E1234567890000000001_lifespan_side_brush',
+ 'unit_of_measurement': '%',
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_time_cleaned:state]
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'device_class': 'duration',
- 'friendly_name': 'Ozmo 950 Time cleaned',
- 'unit_of_measurement': ,
+ 'friendly_name': 'Ozmo 950 Side brushes lifespan',
+ 'unit_of_measurement': '%',
}),
'context': ,
- 'entity_id': 'sensor.ozmo_950_time_cleaned',
+ 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan',
'last_changed': ,
'last_updated': ,
- 'state': '5.0',
+ 'state': '40',
})
# ---
# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:entity-registry]
@@ -402,6 +402,57 @@
'state': '60',
})
# ---
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:entity-registry]
+ EntityRegistryEntrySnapshot({
+ 'aliases': set({
+ }),
+ 'area_id': None,
+ 'capabilities': dict({
+ 'state_class': ,
+ }),
+ 'config_entry_id': ,
+ 'device_class': None,
+ 'device_id': ,
+ 'disabled_by': None,
+ 'domain': 'sensor',
+ 'entity_category': None,
+ 'entity_id': 'sensor.ozmo_950_total_cleaning_duration',
+ 'has_entity_name': True,
+ 'hidden_by': None,
+ 'icon': None,
+ 'id': ,
+ 'name': None,
+ 'options': dict({
+ 'sensor.private': dict({
+ 'suggested_unit_of_measurement': ,
+ }),
+ }),
+ 'original_device_class': ,
+ 'original_icon': None,
+ 'original_name': 'Total cleaning duration',
+ 'platform': 'ecovacs',
+ 'previous_unique_id': None,
+ 'supported_features': 0,
+ 'translation_key': 'total_stats_time',
+ 'unique_id': 'E1234567890000000001_total_stats_time',
+ 'unit_of_measurement': ,
+ })
+# ---
+# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:state]
+ StateSnapshot({
+ 'attributes': ReadOnlyDict({
+ 'device_class': 'duration',
+ 'friendly_name': 'Ozmo 950 Total cleaning duration',
+ 'state_class': ,
+ 'unit_of_measurement': ,
+ }),
+ 'context': ,
+ 'entity_id': 'sensor.ozmo_950_total_cleaning_duration',
+ 'last_changed': ,
+ 'last_updated': ,
+ 'state': '40.000',
+ })
+# ---
# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -448,57 +499,6 @@
'state': '123',
})
# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_time_cleaned:entity-registry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': dict({
- 'state_class': ,
- }),
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'sensor',
- 'entity_category': None,
- 'entity_id': 'sensor.ozmo_950_total_time_cleaned',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'name': None,
- 'options': dict({
- 'sensor.private': dict({
- 'suggested_unit_of_measurement': ,
- }),
- }),
- 'original_device_class': ,
- 'original_icon': None,
- 'original_name': 'Total time cleaned',
- 'platform': 'ecovacs',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'total_stats_time',
- 'unique_id': 'E1234567890000000001_total_stats_time',
- 'unit_of_measurement': ,
- })
-# ---
-# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_time_cleaned:state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'device_class': 'duration',
- 'friendly_name': 'Ozmo 950 Total time cleaned',
- 'state_class': ,
- 'unit_of_measurement': ,
- }),
- 'context': ,
- 'entity_id': 'sensor.ozmo_950_total_time_cleaned',
- 'last_changed': ,
- 'last_updated': ,
- 'state': '40.000',
- })
-# ---
# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
diff --git a/tests/components/ecovacs/snapshots/test_switch.ambr b/tests/components/ecovacs/snapshots/test_switch.ambr
index 75441c4f918..c645502a831 100644
--- a/tests/components/ecovacs/snapshots/test_switch.ambr
+++ b/tests/components/ecovacs/snapshots/test_switch.ambr
@@ -42,7 +42,7 @@
'state': 'on',
})
# ---
-# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_fan_speed_boost:entity-registry]
+# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_boost_suction:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -54,7 +54,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': ,
- 'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost',
+ 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -64,7 +64,7 @@
}),
'original_device_class': None,
'original_icon': None,
- 'original_name': 'Carpet auto fan speed boost',
+ 'original_name': 'Carpet auto-boost suction',
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': 0,
@@ -73,13 +73,13 @@
'unit_of_measurement': None,
})
# ---
-# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_fan_speed_boost:state]
+# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_boost_suction:state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'friendly_name': 'Ozmo 950 Carpet auto fan speed boost',
+ 'friendly_name': 'Ozmo 950 Carpet auto-boost suction',
}),
'context': ,
- 'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost',
+ 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction',
'last_changed': ,
'last_updated': ,
'state': 'on',
diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py
index f804e813256..24c926b1f77 100644
--- a/tests/components/ecovacs/test_button.py
+++ b/tests/components/ecovacs/test_button.py
@@ -33,13 +33,16 @@ def platforms() -> Platform | list[Platform]:
"yna5x1",
[
("button.ozmo_950_relocate", SetRelocationState()),
- ("button.ozmo_950_reset_brush_lifespan", ResetLifeSpan(LifeSpan.BRUSH)),
+ (
+ "button.ozmo_950_reset_main_brush_lifespan",
+ ResetLifeSpan(LifeSpan.BRUSH),
+ ),
(
"button.ozmo_950_reset_filter_lifespan",
ResetLifeSpan(LifeSpan.FILTER),
),
(
- "button.ozmo_950_reset_side_brush_lifespan",
+ "button.ozmo_950_reset_side_brushes_lifespan",
ResetLifeSpan(LifeSpan.SIDE_BRUSH),
),
],
@@ -56,7 +59,7 @@ async def test_buttons(
entities: list[tuple[str, Command]],
) -> None:
"""Test that sensor entity snapshots match."""
- assert sorted(hass.states.async_entity_ids()) == [e[0] for e in entities]
+ assert hass.states.async_entity_ids() == [e[0] for e in entities]
device = controller.devices[0]
for entity_id, command in entities:
assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing"
@@ -89,9 +92,9 @@ async def test_buttons(
(
"yna5x1",
[
- "button.ozmo_950_reset_brush_lifespan",
+ "button.ozmo_950_reset_main_brush_lifespan",
"button.ozmo_950_reset_filter_lifespan",
- "button.ozmo_950_reset_side_brush_lifespan",
+ "button.ozmo_950_reset_side_brushes_lifespan",
],
),
],
diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py
index 8557ccb983c..e76001fbaeb 100644
--- a/tests/components/ecovacs/test_init.py
+++ b/tests/components/ecovacs/test_init.py
@@ -80,6 +80,7 @@ async def test_invalid_auth(
({}, 0),
({DOMAIN: IMPORT_DATA.copy()}, 1),
],
+ ids=["no_config", "import_config"],
)
async def test_async_setup_import(
hass: HomeAssistant,
diff --git a/tests/components/ecovacs/test_select.py b/tests/components/ecovacs/test_select.py
index cfe34c5a7a6..0d1a5d19116 100644
--- a/tests/components/ecovacs/test_select.py
+++ b/tests/components/ecovacs/test_select.py
@@ -44,7 +44,7 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
(
"yna5x1",
[
- "select.ozmo_950_water_amount",
+ "select.ozmo_950_water_flow_level",
],
),
],
@@ -58,7 +58,7 @@ async def test_selects(
entity_ids: list[str],
) -> None:
"""Test that select entity snapshots match."""
- assert entity_ids == sorted(hass.states.async_entity_ids())
+ assert entity_ids == hass.states.async_entity_ids()
for entity_id in entity_ids:
assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing"
assert state.state == STATE_UNKNOWN
@@ -83,7 +83,7 @@ async def test_selects(
[
(
"yna5x1",
- "select.ozmo_950_water_amount",
+ "select.ozmo_950_water_flow_level",
"ultrahigh",
"low",
SetWaterInfo(WaterAmount.LOW),
diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py
index 18d65349fa2..78755668f0f 100644
--- a/tests/components/ecovacs/test_sensor.py
+++ b/tests/components/ecovacs/test_sensor.py
@@ -54,18 +54,18 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
"yna5x1",
[
"sensor.ozmo_950_area_cleaned",
- "sensor.ozmo_950_battery",
- "sensor.ozmo_950_brush_lifespan",
- "sensor.ozmo_950_error",
- "sensor.ozmo_950_filter_lifespan",
- "sensor.ozmo_950_ip_address",
- "sensor.ozmo_950_side_brush_lifespan",
- "sensor.ozmo_950_time_cleaned",
+ "sensor.ozmo_950_cleaning_duration",
"sensor.ozmo_950_total_area_cleaned",
+ "sensor.ozmo_950_total_cleaning_duration",
"sensor.ozmo_950_total_cleanings",
- "sensor.ozmo_950_total_time_cleaned",
+ "sensor.ozmo_950_battery",
+ "sensor.ozmo_950_ip_address",
"sensor.ozmo_950_wi_fi_rssi",
"sensor.ozmo_950_wi_fi_ssid",
+ "sensor.ozmo_950_main_brush_lifespan",
+ "sensor.ozmo_950_filter_lifespan",
+ "sensor.ozmo_950_side_brushes_lifespan",
+ "sensor.ozmo_950_error",
],
),
],
@@ -79,7 +79,7 @@ async def test_sensors(
entity_ids: list[str],
) -> None:
"""Test that sensor entity snapshots match."""
- assert entity_ids == sorted(hass.states.async_entity_ids())
+ assert entity_ids == hass.states.async_entity_ids()
for entity_id in entity_ids:
assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing"
assert state.state == STATE_UNKNOWN
diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py
index 43c5d25e18f..35d2f487b95 100644
--- a/tests/components/ecovacs/test_switch.py
+++ b/tests/components/ecovacs/test_switch.py
@@ -69,7 +69,7 @@ class SwitchTestCase:
SetContinuousCleaning,
),
SwitchTestCase(
- "switch.ozmo_950_carpet_auto_fan_speed_boost",
+ "switch.ozmo_950_carpet_auto_boost_suction",
CarpetAutoFanBoostEvent(True),
SetCarpetAutoFanBoost,
),
@@ -90,9 +90,7 @@ async def test_switch_entities(
device = controller.devices[0]
event_bus = device.events
- assert sorted(hass.states.async_entity_ids()) == sorted(
- test.entity_id for test in tests
- )
+ assert hass.states.async_entity_ids() == [test.entity_id for test in tests]
for test_case in tests:
entity_id = test_case.entity_id
assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing"
@@ -139,7 +137,7 @@ async def test_switch_entities(
[
"switch.ozmo_950_advanced_mode",
"switch.ozmo_950_continuous_cleaning",
- "switch.ozmo_950_carpet_auto_fan_speed_boost",
+ "switch.ozmo_950_carpet_auto_boost_suction",
],
),
],
diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py
index 38a33bfdec2..f6665c4ad91 100644
--- a/tests/components/esphome/test_voice_assistant.py
+++ b/tests/components/esphome/test_voice_assistant.py
@@ -70,6 +70,19 @@ def voice_assistant_udp_server_v2(
return voice_assistant_udp_server(entry=mock_voice_assistant_v2_entry)
+@pytest.fixture
+def test_wav() -> bytes:
+ """Return one second of empty WAV audio."""
+ with io.BytesIO() as wav_io:
+ with wave.open(wav_io, "wb") as wav_file:
+ wav_file.setframerate(16000)
+ wav_file.setsampwidth(2)
+ wav_file.setnchannels(1)
+ wav_file.writeframes(bytes(_ONE_SECOND))
+
+ return wav_io.getvalue()
+
+
async def test_pipeline_events(
hass: HomeAssistant,
voice_assistant_udp_server_v1: VoiceAssistantUDPServer,
@@ -241,11 +254,13 @@ async def test_udp_server_multiple(
):
await voice_assistant_udp_server_v1.start_server()
- with patch(
- "homeassistant.components.esphome.voice_assistant.UDP_PORT",
- new=unused_udp_port_factory(),
- ), pytest.raises(RuntimeError):
- pass
+ with (
+ patch(
+ "homeassistant.components.esphome.voice_assistant.UDP_PORT",
+ new=unused_udp_port_factory(),
+ ),
+ pytest.raises(RuntimeError),
+ ):
await voice_assistant_udp_server_v1.start_server()
@@ -257,10 +272,13 @@ async def test_udp_server_after_stopped(
) -> None:
"""Test that the UDP server raises an error if started after stopped."""
voice_assistant_udp_server_v1.close()
- with patch(
- "homeassistant.components.esphome.voice_assistant.UDP_PORT",
- new=unused_udp_port_factory(),
- ), pytest.raises(RuntimeError):
+ with (
+ patch(
+ "homeassistant.components.esphome.voice_assistant.UDP_PORT",
+ new=unused_udp_port_factory(),
+ ),
+ pytest.raises(RuntimeError),
+ ):
await voice_assistant_udp_server_v1.start_server()
@@ -362,35 +380,33 @@ async def test_send_tts_not_called_when_empty(
async def test_send_tts(
hass: HomeAssistant,
voice_assistant_udp_server_v2: VoiceAssistantUDPServer,
+ test_wav,
) -> None:
"""Test the UDP server calls sendto to transmit audio data to device."""
- with io.BytesIO() as wav_io:
- with wave.open(wav_io, "wb") as wav_file:
- wav_file.setframerate(16000)
- wav_file.setsampwidth(2)
- wav_file.setnchannels(1)
- wav_file.writeframes(bytes(_ONE_SECOND))
-
- wav_bytes = wav_io.getvalue()
-
with patch(
"homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio",
- return_value=("wav", wav_bytes),
+ return_value=("wav", test_wav),
):
+ voice_assistant_udp_server_v2.started = True
voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport)
-
- voice_assistant_udp_server_v2._event_callback(
- PipelineEvent(
- type=PipelineEventType.TTS_END,
- data={
- "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL}
- },
+ with patch.object(
+ voice_assistant_udp_server_v2.transport, "is_closing", return_value=False
+ ):
+ voice_assistant_udp_server_v2._event_callback(
+ PipelineEvent(
+ type=PipelineEventType.TTS_END,
+ data={
+ "tts_output": {
+ "media_id": _TEST_MEDIA_ID,
+ "url": _TEST_OUTPUT_URL,
+ }
+ },
+ )
)
- )
- await voice_assistant_udp_server_v2._tts_done.wait()
+ await voice_assistant_udp_server_v2._tts_done.wait()
- voice_assistant_udp_server_v2.transport.sendto.assert_called()
+ voice_assistant_udp_server_v2.transport.sendto.assert_called()
async def test_send_tts_wrong_sample_rate(
@@ -400,17 +416,20 @@ async def test_send_tts_wrong_sample_rate(
"""Test the UDP server calls sendto to transmit audio data to device."""
with io.BytesIO() as wav_io:
with wave.open(wav_io, "wb") as wav_file:
- wav_file.setframerate(22050) # should be 16000
+ wav_file.setframerate(22050)
wav_file.setsampwidth(2)
wav_file.setnchannels(1)
wav_file.writeframes(bytes(_ONE_SECOND))
wav_bytes = wav_io.getvalue()
-
- with patch(
- "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio",
- return_value=("wav", wav_bytes),
- ), pytest.raises(ValueError):
+ with (
+ patch(
+ "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio",
+ return_value=("wav", wav_bytes),
+ ),
+ pytest.raises(ValueError),
+ ):
+ voice_assistant_udp_server_v2.started = True
voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport)
voice_assistant_udp_server_v2._event_callback(
@@ -431,10 +450,14 @@ async def test_send_tts_wrong_format(
voice_assistant_udp_server_v2: VoiceAssistantUDPServer,
) -> None:
"""Test that only WAV audio will be streamed."""
- with patch(
- "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio",
- return_value=("raw", bytes(1024)),
- ), pytest.raises(ValueError):
+ with (
+ patch(
+ "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio",
+ return_value=("raw", bytes(1024)),
+ ),
+ pytest.raises(ValueError),
+ ):
+ voice_assistant_udp_server_v2.started = True
voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport)
voice_assistant_udp_server_v2._event_callback(
@@ -450,6 +473,33 @@ async def test_send_tts_wrong_format(
await voice_assistant_udp_server_v2._tts_task # raises ValueError
+async def test_send_tts_not_started(
+ hass: HomeAssistant,
+ voice_assistant_udp_server_v2: VoiceAssistantUDPServer,
+ test_wav,
+) -> None:
+ """Test the UDP server does not call sendto when not started."""
+ with patch(
+ "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio",
+ return_value=("wav", test_wav),
+ ):
+ voice_assistant_udp_server_v2.started = False
+ voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport)
+
+ voice_assistant_udp_server_v2._event_callback(
+ PipelineEvent(
+ type=PipelineEventType.TTS_END,
+ data={
+ "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL}
+ },
+ )
+ )
+
+ await voice_assistant_udp_server_v2._tts_done.wait()
+
+ voice_assistant_udp_server_v2.transport.sendto.assert_not_called()
+
+
async def test_wake_word(
hass: HomeAssistant,
voice_assistant_udp_server_v2: VoiceAssistantUDPServer,
@@ -459,11 +509,12 @@ async def test_wake_word(
async def async_pipeline_from_audio_stream(*args, start_stage, **kwargs):
assert start_stage == PipelineStage.WAKE_WORD
- with patch(
- "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream",
- new=async_pipeline_from_audio_stream,
- ), patch(
- "asyncio.Event.wait" # TTS wait event
+ with (
+ patch(
+ "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream",
+ new=async_pipeline_from_audio_stream,
+ ),
+ patch("asyncio.Event.wait"), # TTS wait event
):
voice_assistant_udp_server_v2.transport = Mock()
@@ -515,10 +566,15 @@ async def test_wake_word_abort_exception(
async def async_pipeline_from_audio_stream(*args, **kwargs):
raise WakeWordDetectionAborted
- with patch(
- "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream",
- new=async_pipeline_from_audio_stream,
- ), patch.object(voice_assistant_udp_server_v2, "handle_event") as mock_handle_event:
+ with (
+ patch(
+ "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream",
+ new=async_pipeline_from_audio_stream,
+ ),
+ patch.object(
+ voice_assistant_udp_server_v2, "handle_event"
+ ) as mock_handle_event,
+ ):
voice_assistant_udp_server_v2.transport = Mock()
await voice_assistant_udp_server_v2.run_pipeline(
diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py
index aa7f8472cab..c6589555c3e 100644
--- a/tests/components/google_assistant/test_http.py
+++ b/tests/components/google_assistant/test_http.py
@@ -466,6 +466,6 @@ async def test_async_enable_local_sdk(
)
assert resp.status == HTTPStatus.OK
assert (
- "Cannot process request for webhook mock_webhook_id as no linked agent user is found:"
+ "Cannot process request for webhook **REDACTED** as no linked agent user is found:"
in caplog.text
)
diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py
index 7b83ed9eb0d..9db70ca80d1 100644
--- a/tests/components/group/test_config_flow.py
+++ b/tests/components/group/test_config_flow.py
@@ -479,7 +479,7 @@ LIGHT_ATTRS = [
"supported_color_modes": ["onoff"],
"supported_features": 0,
},
- {"color_mode": "onoff"},
+ {"color_mode": "unknown"},
]
LOCK_ATTRS = [{"supported_features": 1}, {}]
MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}]
diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py
index 3051ec502a0..63f21456066 100644
--- a/tests/components/group/test_light.py
+++ b/tests/components/group/test_light.py
@@ -28,9 +28,6 @@ from homeassistant.components.light import (
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
- SUPPORT_BRIGHTNESS,
- SUPPORT_COLOR,
- SUPPORT_COLOR_TEMP,
ColorMode,
)
from homeassistant.const import (
@@ -278,7 +275,8 @@ async def test_brightness(
entity0.brightness = 255
entity1 = platform.ENTITIES[1]
- entity1.supported_features = SUPPORT_BRIGHTNESS
+ entity1.supported_color_modes = {ColorMode.BRIGHTNESS}
+ entity1.color_mode = ColorMode.BRIGHTNESS
assert await async_setup_component(
hass,
@@ -349,7 +347,8 @@ async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) -
entity0.hs_color = (0, 100)
entity1 = platform.ENTITIES[1]
- entity1.supported_features = SUPPORT_COLOR
+ entity1.supported_color_modes = {ColorMode.HS}
+ entity1.color_mode = ColorMode.HS
assert await async_setup_component(
hass,
@@ -697,7 +696,8 @@ async def test_color_temp(
entity0.color_temp_kelvin = 2
entity1 = platform.ENTITIES[1]
- entity1.supported_features = SUPPORT_COLOR_TEMP
+ entity1.supported_color_modes = {ColorMode.COLOR_TEMP}
+ entity1.color_mode = ColorMode.COLOR_TEMP
assert await async_setup_component(
hass,
@@ -837,7 +837,8 @@ async def test_min_max_mireds(
entity0._attr_max_color_temp_kelvin = 5
entity1 = platform.ENTITIES[1]
- entity1.supported_features = SUPPORT_COLOR_TEMP
+ entity1.supported_color_modes = {ColorMode.COLOR_TEMP}
+ entity1.color_mode = ColorMode.COLOR_TEMP
entity1._attr_min_color_temp_kelvin = 1
entity1._attr_max_color_temp_kelvin = 1234567890
@@ -1009,12 +1010,15 @@ async def test_supported_color_modes(
entity0 = platform.ENTITIES[0]
entity0.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
+ entity0.color_mode = ColorMode.UNKNOWN
entity1 = platform.ENTITIES[1]
entity1.supported_color_modes = {ColorMode.RGBW, ColorMode.RGBWW}
+ entity1.color_mode = ColorMode.UNKNOWN
entity2 = platform.ENTITIES[2]
- entity2.supported_features = SUPPORT_BRIGHTNESS
+ entity2.supported_color_modes = {ColorMode.BRIGHTNESS}
+ entity2.color_mode = ColorMode.UNKNOWN
assert await async_setup_component(
hass,
@@ -1036,7 +1040,6 @@ async def test_supported_color_modes(
state = hass.states.get("light.light_group")
assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == {
- "brightness",
"color_temp",
"hs",
"rgbw",
@@ -1183,6 +1186,7 @@ async def test_color_mode2(
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
+ assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP]
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
await hass.services.async_call(
@@ -1193,7 +1197,8 @@ async def test_color_mode2(
)
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
- assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS
+ assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP]
+ assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
async def test_supported_features(hass: HomeAssistant) -> None:
diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py
index 876050586d2..5c5b6c0a44a 100644
--- a/tests/components/honeywell/conftest.py
+++ b/tests/components/honeywell/conftest.py
@@ -39,6 +39,15 @@ def config_data():
}
+@pytest.fixture
+def another_config_data():
+ """Provide configuration data for tests."""
+ return {
+ CONF_USERNAME: "user2",
+ CONF_PASSWORD: "fake2",
+ }
+
+
@pytest.fixture
def config_options():
"""Provide configuratio options for test."""
@@ -55,6 +64,16 @@ def config_entry(config_data, config_options):
)
+@pytest.fixture
+def config_entry2(another_config_data, config_options):
+ """Create a mock config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ data=another_config_data,
+ options=config_options,
+ )
+
+
@pytest.fixture
def device():
"""Mock a somecomfort.Device."""
diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py
index ccfc2c5d264..98578217af6 100644
--- a/tests/components/honeywell/test_init.py
+++ b/tests/components/honeywell/test_init.py
@@ -33,6 +33,22 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) -
) # 1 climate entity; 2 sensor entities
+@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0)
+async def test_setup_multiple_entry(
+ hass: HomeAssistant, config_entry: MockConfigEntry, config_entry2: MockConfigEntry
+) -> None:
+ """Initialize the config entry."""
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert config_entry.state is ConfigEntryState.LOADED
+
+ config_entry2.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry2.entry_id)
+ await hass.async_block_till_done()
+ assert config_entry2.state is ConfigEntryState.LOADED
+
+
async def test_setup_multiple_thermostats(
hass: HomeAssistant,
config_entry: MockConfigEntry,
diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py
index 69f6a841737..0e3bc1332cf 100644
--- a/tests/components/light/test_init.py
+++ b/tests/components/light/test_init.py
@@ -127,6 +127,9 @@ async def test_services(
| light.LightEntityFeature.EFFECT
| light.LightEntityFeature.TRANSITION
)
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ ent2.supported_color_modes = None
+ ent2.color_mode = None
ent3.supported_features = (
light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION
)
@@ -905,9 +908,15 @@ async def test_light_brightness_step(
platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON))
entity0 = platform.ENTITIES[0]
entity0.supported_features = light.SUPPORT_BRIGHTNESS
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity0.supported_color_modes = None
+ entity0.color_mode = None
entity0.brightness = 100
entity1 = platform.ENTITIES[1]
entity1.supported_features = light.SUPPORT_BRIGHTNESS
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity1.supported_color_modes = None
+ entity1.color_mode = None
entity1.brightness = 50
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
await hass.async_block_till_done()
@@ -967,6 +976,9 @@ async def test_light_brightness_pct_conversion(
platform.init()
entity = platform.ENTITIES[0]
entity.supported_features = light.SUPPORT_BRIGHTNESS
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity.supported_color_modes = None
+ entity.color_mode = None
entity.brightness = 100
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
await hass.async_block_till_done()
@@ -1133,17 +1145,29 @@ async def test_light_backwards_compatibility_supported_color_modes(
entity1 = platform.ENTITIES[1]
entity1.supported_features = light.SUPPORT_BRIGHTNESS
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity1.supported_color_modes = None
+ entity1.color_mode = None
entity2 = platform.ENTITIES[2]
entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity2.supported_color_modes = None
+ entity2.color_mode = None
entity3 = platform.ENTITIES[3]
entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity3.supported_color_modes = None
+ entity3.color_mode = None
entity4 = platform.ENTITIES[4]
entity4.supported_features = (
light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP
)
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity4.supported_color_modes = None
+ entity4.color_mode = None
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
await hass.async_block_till_done()
@@ -1204,20 +1228,32 @@ async def test_light_backwards_compatibility_color_mode(
entity1 = platform.ENTITIES[1]
entity1.supported_features = light.SUPPORT_BRIGHTNESS
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity1.supported_color_modes = None
+ entity1.color_mode = None
entity1.brightness = 100
entity2 = platform.ENTITIES[2]
entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity2.supported_color_modes = None
+ entity2.color_mode = None
entity2.color_temp_kelvin = 10000
entity3 = platform.ENTITIES[3]
entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity3.supported_color_modes = None
+ entity3.color_mode = None
entity3.hs_color = (240, 100)
entity4 = platform.ENTITIES[4]
entity4.supported_features = (
light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP
)
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity4.supported_color_modes = None
+ entity4.color_mode = None
entity4.hs_color = (240, 100)
entity4.color_temp_kelvin = 10000
@@ -1464,6 +1500,9 @@ async def test_light_service_call_color_conversion(
entity4 = platform.ENTITIES[4]
entity4.supported_features = light.SUPPORT_COLOR
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity4.supported_color_modes = None
+ entity4.color_mode = None
entity5 = platform.ENTITIES[5]
entity5.supported_color_modes = {light.ColorMode.RGBW}
@@ -1905,6 +1944,9 @@ async def test_light_service_call_color_conversion_named_tuple(
entity4 = platform.ENTITIES[4]
entity4.supported_features = light.SUPPORT_COLOR
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity4.supported_color_modes = None
+ entity4.color_mode = None
entity5 = platform.ENTITIES[5]
entity5.supported_color_modes = {light.ColorMode.RGBW}
@@ -2330,6 +2372,9 @@ async def test_light_state_color_conversion(
entity3 = platform.ENTITIES[3]
entity3.hs_color = (240, 100)
entity3.supported_features = light.SUPPORT_COLOR
+ # Set color modes to none to trigger backwards compatibility in LightEntity
+ entity3.supported_color_modes = None
+ entity3.color_mode = None
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
await hass.async_block_till_done()
diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py
index 4568eaf2e77..dc2ca4391f1 100644
--- a/tests/components/melissa/test_climate.py
+++ b/tests/components/melissa/test_climate.py
@@ -223,7 +223,10 @@ async def test_supported_features(hass: HomeAssistant) -> None:
device = (await api.async_fetch_devices())[_SERIAL]
thermostat = MelissaClimate(api, _SERIAL, device)
features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ | ClimateEntityFeature.FAN_MODE
+ | ClimateEntityFeature.TURN_OFF
+ | ClimateEntityFeature.TURN_ON
)
assert thermostat.supported_features == features
diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py
index 3c932a24afb..c5b12a112fd 100644
--- a/tests/components/modbus/test_init.py
+++ b/tests/components/modbus/test_init.py
@@ -79,9 +79,8 @@ from homeassistant.components.modbus.const import (
DataType,
)
from homeassistant.components.modbus.validators import (
- duplicate_entity_validator,
+ check_config,
duplicate_fan_mode_validator,
- duplicate_modbus_validator,
nan_validator,
register_int_list_validator,
struct_validator,
@@ -340,55 +339,46 @@ async def test_exception_struct_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
},
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST + " 2",
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
+ },
+ {
+ CONF_NAME: TEST_MODBUS_NAME + "2",
+ CONF_TYPE: TCP,
+ CONF_HOST: TEST_MODBUS_HOST,
+ CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
},
],
[
{
- CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
},
{
CONF_NAME: TEST_MODBUS_NAME + " 2",
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
},
],
],
)
-async def test_duplicate_modbus_validator(do_config) -> None:
+async def test_check_config(do_config) -> None:
"""Test duplicate modbus validator."""
- duplicate_modbus_validator(do_config)
+ check_config(do_config)
assert len(do_config) == 1
-@pytest.mark.parametrize(
- "do_config",
- [
- {
- CONF_ADDRESS: 11,
- CONF_FAN_MODE_VALUES: {
- CONF_FAN_MODE_ON: 7,
- CONF_FAN_MODE_OFF: 9,
- CONF_FAN_MODE_HIGH: 9,
- },
- }
- ],
-)
-async def test_duplicate_fan_mode_validator(do_config) -> None:
- """Test duplicate modbus validator."""
- duplicate_fan_mode_validator(do_config)
- assert len(do_config[CONF_FAN_MODE_VALUES]) == 2
-
-
@pytest.mark.parametrize(
"do_config",
[
@@ -398,6 +388,7 @@ async def test_duplicate_fan_mode_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
CONF_SENSORS: [
{
CONF_NAME: TEST_ENTITY_NAME,
@@ -418,6 +409,7 @@ async def test_duplicate_fan_mode_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
CONF_SENSORS: [
{
CONF_NAME: TEST_ENTITY_NAME,
@@ -432,35 +424,12 @@ async def test_duplicate_fan_mode_validator(do_config) -> None:
],
}
],
- [
- {
- CONF_NAME: TEST_MODBUS_NAME,
- CONF_TYPE: TCP,
- CONF_HOST: TEST_MODBUS_HOST,
- CONF_PORT: TEST_PORT_TCP,
- CONF_CLIMATES: [
- {
- CONF_NAME: TEST_ENTITY_NAME,
- CONF_ADDRESS: 117,
- CONF_SLAVE: 0,
- },
- {
- CONF_NAME: TEST_ENTITY_NAME + " 2",
- CONF_ADDRESS: 117,
- CONF_SLAVE: 0,
- },
- ],
- }
- ],
],
)
-async def test_duplicate_entity_validator(do_config) -> None:
+async def test_check_config_sensor(do_config) -> None:
"""Test duplicate entity validator."""
- duplicate_entity_validator(do_config)
- if CONF_SENSORS in do_config[0]:
- assert len(do_config[0][CONF_SENSORS]) == 1
- elif CONF_CLIMATES in do_config[0]:
- assert len(do_config[0][CONF_CLIMATES]) == 1
+ check_config(do_config)
+ assert len(do_config[0][CONF_SENSORS]) == 1
@pytest.mark.parametrize(
@@ -472,6 +441,28 @@ async def test_duplicate_entity_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
+ CONF_CLIMATES: [
+ {
+ CONF_NAME: TEST_ENTITY_NAME,
+ CONF_ADDRESS: 117,
+ CONF_SLAVE: 0,
+ },
+ {
+ CONF_NAME: TEST_ENTITY_NAME,
+ CONF_ADDRESS: 119,
+ CONF_SLAVE: 0,
+ },
+ ],
+ }
+ ],
+ [
+ {
+ CONF_NAME: TEST_MODBUS_NAME,
+ CONF_TYPE: TCP,
+ CONF_HOST: TEST_MODBUS_HOST,
+ CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
@@ -492,6 +483,7 @@ async def test_duplicate_entity_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
@@ -526,6 +518,7 @@ async def test_duplicate_entity_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
@@ -561,6 +554,7 @@ async def test_duplicate_entity_validator(do_config) -> None:
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
+ CONF_TIMEOUT: 3,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
@@ -592,12 +586,31 @@ async def test_duplicate_entity_validator(do_config) -> None:
],
],
)
-async def test_duplicate_entity_validator_with_climate(do_config) -> None:
+async def test_check_config_climate(do_config) -> None:
"""Test duplicate entity validator."""
- duplicate_entity_validator(do_config)
+ check_config(do_config)
assert len(do_config[0][CONF_CLIMATES]) == 1
+@pytest.mark.parametrize(
+ "do_config",
+ [
+ {
+ CONF_ADDRESS: 11,
+ CONF_FAN_MODE_VALUES: {
+ CONF_FAN_MODE_ON: 7,
+ CONF_FAN_MODE_OFF: 9,
+ CONF_FAN_MODE_HIGH: 9,
+ },
+ }
+ ],
+)
+async def test_duplicate_fan_mode_validator(do_config) -> None:
+ """Test duplicate modbus validator."""
+ duplicate_fan_mode_validator(do_config)
+ assert len(do_config[CONF_FAN_MODE_VALUES]) == 2
+
+
@pytest.mark.parametrize(
"do_config",
[
diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py
index 7c58290b143..97571041482 100644
--- a/tests/components/modbus/test_sensor.py
+++ b/tests/components/modbus/test_sensor.py
@@ -357,7 +357,7 @@ async def test_config_wrong_struct_sensor(
},
[7],
False,
- "34",
+ "34.0000",
),
(
{
@@ -379,7 +379,7 @@ async def test_config_wrong_struct_sensor(
},
[9],
False,
- "18",
+ "18.5",
),
(
{
@@ -390,7 +390,7 @@ async def test_config_wrong_struct_sensor(
},
[1],
False,
- "2",
+ "2.40",
),
(
{
@@ -401,7 +401,7 @@ async def test_config_wrong_struct_sensor(
},
[2],
False,
- "-8",
+ "-8.3",
),
(
{
@@ -676,7 +676,7 @@ async def test_config_wrong_struct_sensor(
},
[0x00AB, 0xCDEF],
False,
- "112594",
+ "112593.75",
),
(
{
diff --git a/tests/components/octoprint/test_servics.py b/tests/components/octoprint/test_servics.py
new file mode 100644
index 00000000000..70e983c4bb4
--- /dev/null
+++ b/tests/components/octoprint/test_servics.py
@@ -0,0 +1,66 @@
+"""Test the OctoPrint services."""
+from unittest.mock import patch
+
+from homeassistant.components.octoprint.const import (
+ CONF_BAUDRATE,
+ DOMAIN,
+ SERVICE_CONNECT,
+)
+from homeassistant.const import ATTR_DEVICE_ID, CONF_PORT, CONF_PROFILE_NAME
+from homeassistant.helpers.device_registry import (
+ async_entries_for_config_entry,
+ async_get as async_get_dev_reg,
+)
+
+from . import init_integration
+
+
+async def test_connect_default(hass) -> None:
+ """Test the connect to printer service."""
+ await init_integration(hass, "sensor")
+
+ dev_reg = async_get_dev_reg(hass)
+ device = async_entries_for_config_entry(dev_reg, "uuid")[0]
+
+ # Test pausing the printer when it is printing
+ with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CONNECT,
+ {
+ ATTR_DEVICE_ID: device.id,
+ },
+ blocking=True,
+ )
+
+ assert len(connect_command.mock_calls) == 1
+ connect_command.assert_called_with(
+ port=None, printer_profile=None, baud_rate=None
+ )
+
+
+async def test_connect_all_arguments(hass) -> None:
+ """Test the connect to printer service."""
+ await init_integration(hass, "sensor")
+
+ dev_reg = async_get_dev_reg(hass)
+ device = async_entries_for_config_entry(dev_reg, "uuid")[0]
+
+ # Test pausing the printer when it is printing
+ with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command:
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_CONNECT,
+ {
+ ATTR_DEVICE_ID: device.id,
+ CONF_PROFILE_NAME: "Test Profile",
+ CONF_PORT: "VIRTUAL",
+ CONF_BAUDRATE: 9600,
+ },
+ blocking=True,
+ )
+
+ assert len(connect_command.mock_calls) == 1
+ connect_command.assert_called_with(
+ port="VIRTUAL", printer_profile="Test Profile", baud_rate=9600
+ )
diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr
index 25d47b342c5..8ca1e476820 100644
--- a/tests/components/onewire/snapshots/test_binary_sensor.ambr
+++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr
@@ -144,7 +144,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_a',
+ 'translation_key': 'sensed_id',
'unique_id': '/12.111111111111/sensed.A',
'unit_of_measurement': None,
}),
@@ -173,7 +173,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_b',
+ 'translation_key': 'sensed_id',
'unique_id': '/12.111111111111/sensed.B',
'unit_of_measurement': None,
}),
@@ -556,7 +556,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_0',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.0',
'unit_of_measurement': None,
}),
@@ -585,7 +585,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_1',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.1',
'unit_of_measurement': None,
}),
@@ -614,7 +614,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_2',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.2',
'unit_of_measurement': None,
}),
@@ -643,7 +643,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_3',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.3',
'unit_of_measurement': None,
}),
@@ -672,7 +672,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_4',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.4',
'unit_of_measurement': None,
}),
@@ -701,7 +701,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_5',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.5',
'unit_of_measurement': None,
}),
@@ -730,7 +730,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_6',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.6',
'unit_of_measurement': None,
}),
@@ -759,7 +759,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_7',
+ 'translation_key': 'sensed_id',
'unique_id': '/29.111111111111/sensed.7',
'unit_of_measurement': None,
}),
@@ -960,7 +960,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_a',
+ 'translation_key': 'sensed_id',
'unique_id': '/3A.111111111111/sensed.A',
'unit_of_measurement': None,
}),
@@ -989,7 +989,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'sensed_b',
+ 'translation_key': 'sensed_id',
'unique_id': '/3A.111111111111/sensed.B',
'unit_of_measurement': None,
}),
@@ -1308,7 +1308,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_short_0',
+ 'translation_key': 'hub_short_id',
'unique_id': '/EF.111111111113/hub/short.0',
'unit_of_measurement': None,
}),
@@ -1337,7 +1337,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_short_1',
+ 'translation_key': 'hub_short_id',
'unique_id': '/EF.111111111113/hub/short.1',
'unit_of_measurement': None,
}),
@@ -1366,7 +1366,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_short_2',
+ 'translation_key': 'hub_short_id',
'unique_id': '/EF.111111111113/hub/short.2',
'unit_of_measurement': None,
}),
@@ -1395,7 +1395,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_short_3',
+ 'translation_key': 'hub_short_id',
'unique_id': '/EF.111111111113/hub/short.3',
'unit_of_measurement': None,
}),
diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr
index cbcf0d6234e..936018a48c4 100644
--- a/tests/components/onewire/snapshots/test_sensor.ambr
+++ b/tests/components/onewire/snapshots/test_sensor.ambr
@@ -322,7 +322,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'counter_a',
+ 'translation_key': 'counter_id',
'unique_id': '/1D.111111111111/counter.A',
'unit_of_measurement': 'count',
}),
@@ -353,7 +353,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'counter_b',
+ 'translation_key': 'counter_id',
'unique_id': '/1D.111111111111/counter.B',
'unit_of_measurement': 'count',
}),
@@ -476,7 +476,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'counter_a',
+ 'translation_key': 'counter_id',
'unique_id': '/1D.111111111111/counter.A',
'unit_of_measurement': 'count',
}),
@@ -507,7 +507,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'counter_b',
+ 'translation_key': 'counter_id',
'unique_id': '/1D.111111111111/counter.B',
'unit_of_measurement': 'count',
}),
@@ -2478,7 +2478,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'wetness_0',
+ 'translation_key': 'wetness_id',
'unique_id': '/EF.111111111112/moisture/sensor.0',
'unit_of_measurement': '%',
}),
@@ -2509,7 +2509,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'wetness_1',
+ 'translation_key': 'wetness_id',
'unique_id': '/EF.111111111112/moisture/sensor.1',
'unit_of_measurement': '%',
}),
@@ -2540,7 +2540,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'moisture_2',
+ 'translation_key': 'moisture_id',
'unique_id': '/EF.111111111112/moisture/sensor.2',
'unit_of_measurement': ,
}),
@@ -2571,7 +2571,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'moisture_3',
+ 'translation_key': 'moisture_id',
'unique_id': '/EF.111111111112/moisture/sensor.3',
'unit_of_measurement': ,
}),
diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr
index e4d081a409b..24c985a311e 100644
--- a/tests/components/onewire/snapshots/test_switch.ambr
+++ b/tests/components/onewire/snapshots/test_switch.ambr
@@ -185,7 +185,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_a',
+ 'translation_key': 'pio_id',
'unique_id': '/12.111111111111/PIO.A',
'unit_of_measurement': None,
}),
@@ -214,7 +214,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_b',
+ 'translation_key': 'pio_id',
'unique_id': '/12.111111111111/PIO.B',
'unit_of_measurement': None,
}),
@@ -243,7 +243,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_a',
+ 'translation_key': 'latch_id',
'unique_id': '/12.111111111111/latch.A',
'unit_of_measurement': None,
}),
@@ -272,7 +272,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_b',
+ 'translation_key': 'latch_id',
'unique_id': '/12.111111111111/latch.B',
'unit_of_measurement': None,
}),
@@ -720,7 +720,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_0',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.0',
'unit_of_measurement': None,
}),
@@ -749,7 +749,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_1',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.1',
'unit_of_measurement': None,
}),
@@ -778,7 +778,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_2',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.2',
'unit_of_measurement': None,
}),
@@ -807,7 +807,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_3',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.3',
'unit_of_measurement': None,
}),
@@ -836,7 +836,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_4',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.4',
'unit_of_measurement': None,
}),
@@ -865,7 +865,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_5',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.5',
'unit_of_measurement': None,
}),
@@ -894,7 +894,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_6',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.6',
'unit_of_measurement': None,
}),
@@ -923,7 +923,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_7',
+ 'translation_key': 'pio_id',
'unique_id': '/29.111111111111/PIO.7',
'unit_of_measurement': None,
}),
@@ -952,7 +952,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_0',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.0',
'unit_of_measurement': None,
}),
@@ -981,7 +981,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_1',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.1',
'unit_of_measurement': None,
}),
@@ -1010,7 +1010,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_2',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.2',
'unit_of_measurement': None,
}),
@@ -1039,7 +1039,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_3',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.3',
'unit_of_measurement': None,
}),
@@ -1068,7 +1068,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_4',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.4',
'unit_of_measurement': None,
}),
@@ -1097,7 +1097,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_5',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.5',
'unit_of_measurement': None,
}),
@@ -1126,7 +1126,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_6',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.6',
'unit_of_measurement': None,
}),
@@ -1155,7 +1155,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'latch_7',
+ 'translation_key': 'latch_id',
'unique_id': '/29.111111111111/latch.7',
'unit_of_measurement': None,
}),
@@ -1452,7 +1452,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_a',
+ 'translation_key': 'pio_id',
'unique_id': '/3A.111111111111/PIO.A',
'unit_of_measurement': None,
}),
@@ -1481,7 +1481,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'pio_b',
+ 'translation_key': 'pio_id',
'unique_id': '/3A.111111111111/PIO.B',
'unit_of_measurement': None,
}),
@@ -1762,7 +1762,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'leaf_sensor_0',
+ 'translation_key': 'leaf_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_leaf.0',
'unit_of_measurement': None,
}),
@@ -1791,7 +1791,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'leaf_sensor_1',
+ 'translation_key': 'leaf_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_leaf.1',
'unit_of_measurement': None,
}),
@@ -1820,7 +1820,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'leaf_sensor_2',
+ 'translation_key': 'leaf_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_leaf.2',
'unit_of_measurement': None,
}),
@@ -1849,7 +1849,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'leaf_sensor_3',
+ 'translation_key': 'leaf_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_leaf.3',
'unit_of_measurement': None,
}),
@@ -1878,7 +1878,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'moisture_sensor_0',
+ 'translation_key': 'moisture_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_moisture.0',
'unit_of_measurement': None,
}),
@@ -1907,7 +1907,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'moisture_sensor_1',
+ 'translation_key': 'moisture_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_moisture.1',
'unit_of_measurement': None,
}),
@@ -1936,7 +1936,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'moisture_sensor_2',
+ 'translation_key': 'moisture_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_moisture.2',
'unit_of_measurement': None,
}),
@@ -1965,7 +1965,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'moisture_sensor_3',
+ 'translation_key': 'moisture_sensor_id',
'unique_id': '/EF.111111111112/moisture/is_moisture.3',
'unit_of_measurement': None,
}),
@@ -2128,7 +2128,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_branch_0',
+ 'translation_key': 'hub_branch_id',
'unique_id': '/EF.111111111113/hub/branch.0',
'unit_of_measurement': None,
}),
@@ -2157,7 +2157,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_branch_1',
+ 'translation_key': 'hub_branch_id',
'unique_id': '/EF.111111111113/hub/branch.1',
'unit_of_measurement': None,
}),
@@ -2186,7 +2186,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_branch_2',
+ 'translation_key': 'hub_branch_id',
'unique_id': '/EF.111111111113/hub/branch.2',
'unit_of_measurement': None,
}),
@@ -2215,7 +2215,7 @@
'platform': 'onewire',
'previous_unique_id': None,
'supported_features': 0,
- 'translation_key': 'hub_branch_3',
+ 'translation_key': 'hub_branch_id',
'unique_id': '/EF.111111111113/hub/branch.3',
'unit_of_measurement': None,
}),
diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py
index 11b4f25e4a3..2de79a6a6dc 100644
--- a/tests/components/powerwall/test_sensor.py
+++ b/tests/components/powerwall/test_sensor.py
@@ -157,7 +157,6 @@ async def test_sensors(
float(hass.states.get("sensor.mysite_tg0123456789ab_battery_remaining").state)
== 14.715
)
- assert float(hass.states.get("sensor.mysite_tg0123456789ab_charge").state) == 100.0
assert (
str(hass.states.get("sensor.mysite_tg0123456789ab_grid_state").state)
== "grid_compliant"
@@ -187,7 +186,6 @@ async def test_sensors(
float(hass.states.get("sensor.mysite_tg9876543210ba_battery_remaining").state)
== 15.137
)
- assert float(hass.states.get("sensor.mysite_tg9876543210ba_charge").state) == 100.0
assert (
str(hass.states.get("sensor.mysite_tg9876543210ba_grid_state").state)
== "grid_compliant"
diff --git a/tests/components/proximity/snapshots/test_diagnostics.ambr b/tests/components/proximity/snapshots/test_diagnostics.ambr
new file mode 100644
index 00000000000..f8f7d9b014e
--- /dev/null
+++ b/tests/components/proximity/snapshots/test_diagnostics.ambr
@@ -0,0 +1,86 @@
+# serializer version: 1
+# name: test_entry_diagnostics
+ dict({
+ 'data': dict({
+ 'entities': dict({
+ 'device_tracker.test1': dict({
+ 'dir_of_travel': None,
+ 'dist_to_zone': 2218752,
+ 'is_in_ignored_zone': False,
+ 'name': 'test1',
+ }),
+ 'device_tracker.test2': dict({
+ 'dir_of_travel': None,
+ 'dist_to_zone': 4077309,
+ 'is_in_ignored_zone': False,
+ 'name': 'test2',
+ }),
+ }),
+ 'entity_mapping': dict({
+ 'device_tracker.test1': list([
+ 'sensor.home_test1_distance',
+ 'sensor.home_test1_direction_of_travel',
+ ]),
+ 'device_tracker.test2': list([
+ 'sensor.home_test2_distance',
+ 'sensor.home_test2_direction_of_travel',
+ ]),
+ 'device_tracker.test3': list([
+ 'sensor.home_test3_distance',
+ 'sensor.home_test3_direction_of_travel',
+ ]),
+ }),
+ 'proximity': dict({
+ 'dir_of_travel': 'unknown',
+ 'dist_to_zone': 2219,
+ 'nearest': 'test1',
+ }),
+ 'tracked_states': dict({
+ 'device_tracker.test1': dict({
+ 'attributes': dict({
+ 'friendly_name': 'test1',
+ 'latitude': '**REDACTED**',
+ 'longitude': '**REDACTED**',
+ }),
+ 'context': '**REDACTED**',
+ 'entity_id': 'device_tracker.test1',
+ 'state': 'not_home',
+ }),
+ 'device_tracker.test2': dict({
+ 'attributes': dict({
+ 'friendly_name': 'test2',
+ 'latitude': '**REDACTED**',
+ 'longitude': '**REDACTED**',
+ }),
+ 'context': '**REDACTED**',
+ 'entity_id': 'device_tracker.test2',
+ 'state': 'not_home',
+ }),
+ }),
+ }),
+ 'entry': dict({
+ 'data': dict({
+ 'ignored_zones': list([
+ ]),
+ 'tolerance': 1,
+ 'tracked_entities': list([
+ 'device_tracker.test1',
+ 'device_tracker.test2',
+ 'device_tracker.test3',
+ ]),
+ 'zone': 'zone.home',
+ }),
+ 'disabled_by': None,
+ 'domain': 'proximity',
+ 'minor_version': 1,
+ 'options': dict({
+ }),
+ 'pref_disable_new_entities': False,
+ 'pref_disable_polling': False,
+ 'source': 'user',
+ 'title': 'home',
+ 'unique_id': 'proximity_home',
+ 'version': 1,
+ }),
+ })
+# ---
diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py
new file mode 100644
index 00000000000..35ecd152a06
--- /dev/null
+++ b/tests/components/proximity/test_diagnostics.py
@@ -0,0 +1,62 @@
+"""Tests for proximity diagnostics platform."""
+from __future__ import annotations
+
+from syrupy.assertion import SnapshotAssertion
+from syrupy.filters import props
+
+from homeassistant.components.proximity.const import (
+ CONF_IGNORED_ZONES,
+ CONF_TOLERANCE,
+ CONF_TRACKED_ENTITIES,
+ DOMAIN,
+)
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.const import CONF_ZONE
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+from tests.components.diagnostics import get_diagnostics_for_config_entry
+from tests.typing import ClientSessionGenerator
+
+
+async def test_entry_diagnostics(
+ hass: HomeAssistant,
+ hass_client: ClientSessionGenerator,
+ snapshot: SnapshotAssertion,
+) -> None:
+ """Test config entry diagnostics."""
+ hass.states.async_set(
+ "device_tracker.test1",
+ "not_home",
+ {"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
+ )
+ hass.states.async_set(
+ "device_tracker.test2",
+ "not_home",
+ {"friendly_name": "test2", "latitude": 150.1, "longitude": 20.1},
+ )
+
+ mock_entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="home",
+ data={
+ CONF_ZONE: "zone.home",
+ CONF_TRACKED_ENTITIES: [
+ "device_tracker.test1",
+ "device_tracker.test2",
+ "device_tracker.test3",
+ ],
+ CONF_IGNORED_ZONES: [],
+ CONF_TOLERANCE: 1,
+ },
+ unique_id=f"{DOMAIN}_home",
+ )
+
+ mock_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ assert mock_entry.state == ConfigEntryState.LOADED
+
+ assert await get_diagnostics_for_config_entry(
+ hass, hass_client, mock_entry
+ ) == snapshot(exclude=props("entry_id", "last_changed", "last_updated"))
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index 1696c9018b4..2a9260a28a4 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -1424,6 +1424,18 @@ async def test_purge_entities(
)
assert states_sensor_kept.count() == 10
+ # sensor.keep should remain in the StatesMeta table
+ states_meta_remain = session.query(StatesMeta).filter(
+ StatesMeta.entity_id == "sensor.keep"
+ )
+ assert states_meta_remain.count() == 1
+
+ # sensor.purge_entity should be removed from the StatesMeta table
+ states_meta_remain = session.query(StatesMeta).filter(
+ StatesMeta.entity_id == "sensor.purge_entity"
+ )
+ assert states_meta_remain.count() == 0
+
_add_purge_records(hass)
# Confirm calling service without arguments matches all records (default filter behavior)
@@ -1437,6 +1449,10 @@ async def test_purge_entities(
states = session.query(States)
assert states.count() == 0
+ # The states_meta table should be empty
+ states_meta_remain = session.query(StatesMeta)
+ assert states_meta_remain.count() == 0
+
async def _add_test_states(hass: HomeAssistant, wait_recording_done: bool = True):
"""Add multiple states to the db for testing."""
diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py
index 69b7f9316f7..00ffdc21b81 100644
--- a/tests/components/recorder/test_statistics.py
+++ b/tests/components/recorder/test_statistics.py
@@ -785,9 +785,8 @@ async def test_import_statistics(
}
# Adjust the statistics in a different unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py
index 323b81211d7..e902dd49020 100644
--- a/tests/components/recorder/test_websocket_api.py
+++ b/tests/components/recorder/test_websocket_api.py
@@ -156,9 +156,8 @@ async def test_statistics_during_period(
await async_wait_recording_done(hass)
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": now.isoformat(),
@@ -170,9 +169,8 @@ async def test_statistics_during_period(
assert response["success"]
assert response["result"] == {}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -194,9 +192,8 @@ async def test_statistics_during_period(
]
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 3,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -226,13 +223,6 @@ async def test_statistic_during_period(
offset,
) -> None:
"""Test statistic_during_period."""
- id = 1
-
- def next_id():
- nonlocal id
- id += 1
- return id
-
now = dt_util.utcnow()
await async_recorder_block_till_done(hass)
@@ -313,9 +303,8 @@ async def test_statistic_during_period(
assert run_cache.get_latest_ids({metadata_id}) is not None
# No data for this period yet
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"fixed_period": {
"start_time": now.isoformat(),
@@ -334,9 +323,8 @@ async def test_statistic_during_period(
}
# This should include imported_statistics_5min[:]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
}
@@ -359,9 +347,8 @@ async def test_statistic_during_period(
dt_util.parse_datetime("2022-10-21T07:15:00+00:00")
+ timedelta(minutes=5 * offset)
).isoformat()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"fixed_period": {
@@ -388,9 +375,8 @@ async def test_statistic_during_period(
dt_util.parse_datetime("2022-10-21T08:20:00+00:00")
+ timedelta(minutes=5 * offset)
).isoformat()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"fixed_period": {
@@ -414,9 +400,8 @@ async def test_statistic_during_period(
+ timedelta(minutes=5 * offset)
).isoformat()
assert imported_stats_5min[26]["start"].isoformat() == start_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"fixed_period": {
"start_time": start_time,
@@ -438,9 +423,8 @@ async def test_statistic_during_period(
dt_util.parse_datetime("2022-10-21T06:09:00+00:00")
+ timedelta(minutes=5 * offset)
).isoformat()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"fixed_period": {
"start_time": start_time,
@@ -463,9 +447,8 @@ async def test_statistic_during_period(
+ timedelta(minutes=5 * offset)
).isoformat()
assert imported_stats_5min[26]["start"].isoformat() == end_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"fixed_period": {
"end_time": end_time,
@@ -493,9 +476,8 @@ async def test_statistic_during_period(
+ timedelta(minutes=5 * offset)
).isoformat()
assert imported_stats_5min[32]["start"].isoformat() == end_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"fixed_period": {
"start_time": start_time,
@@ -517,9 +499,8 @@ async def test_statistic_during_period(
start_time = "2022-10-21T06:00:00+00:00"
assert imported_stats_5min[24 - offset]["start"].isoformat() == start_time
assert imported_stats[2]["start"].isoformat() == start_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"fixed_period": {
"start_time": start_time,
@@ -538,9 +519,8 @@ async def test_statistic_during_period(
}
# This should also include imported_statistics[2:] + imported_statistics_5min[36:]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"rolling_window": {
"duration": {"hours": 1, "minutes": 25},
@@ -559,9 +539,8 @@ async def test_statistic_during_period(
}
# This should include imported_statistics[2:3]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"rolling_window": {
"duration": {"hours": 1},
@@ -585,9 +564,8 @@ async def test_statistic_during_period(
}
# Test we can get only selected types
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"types": ["max", "change"],
@@ -601,9 +579,8 @@ async def test_statistic_during_period(
}
# Test we can convert units
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"units": {"energy": "MWh"},
@@ -621,9 +598,8 @@ async def test_statistic_during_period(
# Test we can automatically convert units
hass.states.async_set("sensor.test", None, attributes=ENERGY_SENSOR_WH_ATTRIBUTES)
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
}
@@ -707,9 +683,8 @@ async def test_statistic_during_period_hole(
await async_wait_recording_done(hass)
# This should include imported_stats[:]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
}
@@ -728,9 +703,8 @@ async def test_statistic_during_period_hole(
end_time = "2022-10-21T05:00:00+00:00"
assert imported_stats[0]["start"].isoformat() == start_time
assert imported_stats[-1]["start"].isoformat() < end_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"fixed_period": {
@@ -751,9 +725,8 @@ async def test_statistic_during_period_hole(
# This should also include imported_stats[:]
start_time = "2022-10-20T13:00:00+00:00"
end_time = "2022-10-21T08:20:00+00:00"
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"fixed_period": {
@@ -776,9 +749,8 @@ async def test_statistic_during_period_hole(
end_time = "2022-10-20T23:00:00+00:00"
assert imported_stats[1]["start"].isoformat() == start_time
assert imported_stats[3]["start"].isoformat() < end_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"fixed_period": {
@@ -801,9 +773,8 @@ async def test_statistic_during_period_hole(
end_time = "2022-10-21T00:00:00+00:00"
assert imported_stats[1]["start"].isoformat() > start_time
assert imported_stats[3]["start"].isoformat() < end_time
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": next_id(),
"type": "recorder/statistic_during_period",
"statistic_id": "sensor.test",
"fixed_period": {
@@ -894,9 +865,8 @@ async def test_statistic_during_period_calendar(
"homeassistant.components.recorder.websocket_api.statistic_during_period",
return_value={},
) as statistic_during_period:
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistic_during_period",
"calendar": calendar_period,
"statistic_id": "sensor.test",
@@ -956,9 +926,8 @@ async def test_statistics_during_period_unit_conversion(
client = await hass_ws_client()
# Query in state unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -981,9 +950,8 @@ async def test_statistics_during_period_unit_conversion(
}
# Query in custom unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1044,9 +1012,8 @@ async def test_sum_statistics_during_period_unit_conversion(
client = await hass_ws_client()
# Query in state unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1069,9 +1036,8 @@ async def test_sum_statistics_during_period_unit_conversion(
}
# Query in custom unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1121,9 +1087,8 @@ async def test_statistics_during_period_invalid_unit_conversion(
client = await hass_ws_client()
# Query in state unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1135,9 +1100,8 @@ async def test_statistics_during_period_invalid_unit_conversion(
assert response["result"] == {}
# Query in custom unit
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1176,9 +1140,8 @@ async def test_statistics_during_period_in_the_past(
await async_wait_recording_done(hass)
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": now.isoformat(),
@@ -1190,9 +1153,8 @@ async def test_statistics_during_period_in_the_past(
assert response["success"]
assert response["result"] == {}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1204,9 +1166,8 @@ async def test_statistics_during_period_in_the_past(
assert response["result"] == {}
past = now - timedelta(days=3, hours=1)
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 3,
"type": "recorder/statistics_during_period",
"start_time": past.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1229,9 +1190,8 @@ async def test_statistics_during_period_in_the_past(
}
start_of_day = stats_top_of_hour.replace(hour=0, minute=0)
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 4,
"type": "recorder/statistics_during_period",
"start_time": stats_top_of_hour.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1253,9 +1213,8 @@ async def test_statistics_during_period_in_the_past(
]
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 5,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1272,9 +1231,8 @@ async def test_statistics_during_period_bad_start_time(
) -> None:
"""Test statistics_during_period."""
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": "cats",
"statistic_ids": ["sensor.test"],
@@ -1293,9 +1251,8 @@ async def test_statistics_during_period_bad_end_time(
now = dt_util.utcnow()
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": "dogs",
@@ -1315,9 +1272,8 @@ async def test_statistics_during_period_no_statistic_ids(
now = dt_util.utcnow()
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": (now + timedelta(seconds=1)).isoformat(),
@@ -1336,9 +1292,8 @@ async def test_statistics_during_period_empty_statistic_ids(
now = dt_util.utcnow()
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": [],
@@ -1428,7 +1383,7 @@ async def test_list_statistic_ids(
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
- await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
@@ -1436,7 +1391,7 @@ async def test_list_statistic_ids(
hass.states.async_set("sensor.test", 10, attributes=attributes)
await async_wait_recording_done(hass)
- await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1458,7 +1413,7 @@ async def test_list_statistic_ids(
hass.states.async_remove("sensor.test")
await hass.async_block_till_done()
- await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1474,14 +1429,14 @@ async def test_list_statistic_ids(
}
]
- await client.send_json(
- {"id": 4, "type": "recorder/list_statistic_ids", "statistic_type": "dogs"}
+ await client.send_json_auto_id(
+ {"type": "recorder/list_statistic_ids", "statistic_type": "dogs"}
)
response = await client.receive_json()
assert not response["success"]
- await client.send_json(
- {"id": 5, "type": "recorder/list_statistic_ids", "statistic_type": "mean"}
+ await client.send_json_auto_id(
+ {"type": "recorder/list_statistic_ids", "statistic_type": "mean"}
)
response = await client.receive_json()
assert response["success"]
@@ -1501,8 +1456,8 @@ async def test_list_statistic_ids(
else:
assert response["result"] == []
- await client.send_json(
- {"id": 6, "type": "recorder/list_statistic_ids", "statistic_type": "sum"}
+ await client.send_json_auto_id(
+ {"type": "recorder/list_statistic_ids", "statistic_type": "sum"}
)
response = await client.receive_json()
assert response["success"]
@@ -1591,7 +1546,7 @@ async def test_list_statistic_ids_unit_change(
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
- await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
@@ -1602,7 +1557,7 @@ async def test_list_statistic_ids_unit_change(
do_adhoc_statistics(hass, start=now)
await async_recorder_block_till_done(hass)
- await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1621,7 +1576,7 @@ async def test_list_statistic_ids_unit_change(
# Change the state unit
hass.states.async_set("sensor.test", 10, attributes=attributes2)
- await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1642,17 +1597,9 @@ async def test_validate_statistics(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test validate_statistics can be called."""
- id = 1
-
- def next_id():
- nonlocal id
- id += 1
- return id
async def assert_validation_result(client, expected_result):
- await client.send_json(
- {"id": next_id(), "type": "recorder/validate_statistics"}
- )
+ await client.send_json_auto_id({"type": "recorder/validate_statistics"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == expected_result
@@ -1685,9 +1632,8 @@ async def test_clear_statistics(
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"],
@@ -1730,9 +1676,8 @@ async def test_clear_statistics(
}
assert response["result"] == expected_response
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/clear_statistics",
"statistic_ids": ["sensor.test"],
}
@@ -1742,9 +1687,8 @@ async def test_clear_statistics(
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 3,
"type": "recorder/statistics_during_period",
"statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"],
"start_time": now.isoformat(),
@@ -1755,9 +1699,8 @@ async def test_clear_statistics(
assert response["success"]
assert response["result"] == expected_response
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 4,
"type": "recorder/clear_statistics",
"statistic_ids": ["sensor.test1", "sensor.test3"],
}
@@ -1767,9 +1710,8 @@ async def test_clear_statistics(
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 5,
"type": "recorder/statistics_during_period",
"statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"],
"start_time": now.isoformat(),
@@ -1811,7 +1753,7 @@ async def test_update_statistics_metadata(
client = await hass_ws_client()
- await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1827,9 +1769,8 @@ async def test_update_statistics_metadata(
}
]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/update_statistics_metadata",
"statistic_id": "sensor.test",
"unit_of_measurement": new_unit,
@@ -1839,7 +1780,7 @@ async def test_update_statistics_metadata(
assert response["success"]
await async_recorder_block_till_done(hass)
- await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1855,9 +1796,8 @@ async def test_update_statistics_metadata(
}
]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 5,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1902,7 +1842,7 @@ async def test_change_statistics_unit(
client = await hass_ws_client()
- await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1918,9 +1858,8 @@ async def test_change_statistics_unit(
}
]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1942,9 +1881,8 @@ async def test_change_statistics_unit(
],
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 3,
"type": "recorder/change_statistics_unit",
"statistic_id": "sensor.test",
"new_unit_of_measurement": "W",
@@ -1955,7 +1893,7 @@ async def test_change_statistics_unit(
assert response["success"]
await async_recorder_block_till_done(hass)
- await client.send_json({"id": 4, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -1971,9 +1909,8 @@ async def test_change_statistics_unit(
}
]
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 5,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -1997,9 +1934,8 @@ async def test_change_statistics_unit(
}
# Changing to the same unit is allowed but does nothing
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 6,
"type": "recorder/change_statistics_unit",
"statistic_id": "sensor.test",
"new_unit_of_measurement": "W",
@@ -2010,7 +1946,7 @@ async def test_change_statistics_unit(
assert response["success"]
await async_recorder_block_till_done(hass)
- await client.send_json({"id": 7, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
@@ -2035,7 +1971,6 @@ async def test_change_statistics_unit_errors(
) -> None:
"""Test change unit of recorded statistics."""
now = dt_util.utcnow()
- ws_id = 0
units = METRIC_SYSTEM
attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None}
@@ -2068,19 +2003,14 @@ async def test_change_statistics_unit_errors(
}
async def assert_statistic_ids(expected):
- nonlocal ws_id
- ws_id += 1
- await client.send_json({"id": ws_id, "type": "recorder/list_statistic_ids"})
+ await client.send_json_auto_id({"type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == expected
async def assert_statistics(expected):
- nonlocal ws_id
- ws_id += 1
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": ws_id,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
@@ -2106,10 +2036,8 @@ async def test_change_statistics_unit_errors(
await assert_statistics(expected_statistics)
# Try changing to an invalid unit
- ws_id += 1
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": ws_id,
"type": "recorder/change_statistics_unit",
"statistic_id": "sensor.test",
"old_unit_of_measurement": "kW",
@@ -2126,10 +2054,8 @@ async def test_change_statistics_unit_errors(
await assert_statistics(expected_statistics)
# Try changing from the wrong unit
- ws_id += 1
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": ws_id,
"type": "recorder/change_statistics_unit",
"statistic_id": "sensor.test",
"old_unit_of_measurement": "W",
@@ -2155,7 +2081,7 @@ async def test_recorder_info(
# Ensure there are no queued events
await async_wait_recording_done(hass)
- await client.send_json({"id": 1, "type": "recorder/info"})
+ await client.send_json_auto_id({"type": "recorder/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
@@ -2174,7 +2100,7 @@ async def test_recorder_info_no_recorder(
"""Test getting recorder status when recorder is not present."""
client = await hass_ws_client()
- await client.send_json({"id": 1, "type": "recorder/info"})
+ await client.send_json_auto_id({"type": "recorder/info"})
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "unknown_command"
@@ -2199,7 +2125,7 @@ async def test_recorder_info_bad_recorder_config(
# Wait for recorder to shut down
await hass.async_add_executor_job(recorder.get_instance(hass).join)
- await client.send_json({"id": 1, "type": "recorder/info"})
+ await client.send_json_auto_id({"type": "recorder/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"]["recording"] is False
@@ -2250,7 +2176,7 @@ async def test_recorder_info_migration_queue_exhausted(
client = await hass_ws_client()
# Check the status
- await client.send_json({"id": 1, "type": "recorder/info"})
+ await client.send_json_auto_id({"type": "recorder/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"]["migration_in_progress"] is True
@@ -2262,7 +2188,7 @@ async def test_recorder_info_migration_queue_exhausted(
await async_wait_recording_done(hass)
# Check the status after migration finished
- await client.send_json({"id": 2, "type": "recorder/info"})
+ await client.send_json_auto_id({"type": "recorder/info"})
response = await client.receive_json()
assert response["success"]
assert response["result"]["migration_in_progress"] is False
@@ -2278,7 +2204,7 @@ async def test_backup_start_no_recorder(
"""Test getting backup start when recorder is not present."""
client = await hass_ws_client(hass, hass_supervisor_access_token)
- await client.send_json({"id": 1, "type": "backup/start"})
+ await client.send_json_auto_id({"type": "backup/start"})
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "unknown_command"
@@ -2303,12 +2229,12 @@ async def test_backup_start_timeout(
with patch.object(recorder.core, "DB_LOCK_TIMEOUT", 0):
try:
- await client.send_json({"id": 1, "type": "backup/start"})
+ await client.send_json_auto_id({"type": "backup/start"})
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "timeout_error"
finally:
- await client.send_json({"id": 2, "type": "backup/end"})
+ await client.send_json_auto_id({"type": "backup/end"})
async def test_backup_end(
@@ -2323,11 +2249,11 @@ async def test_backup_end(
# Ensure there are no queued events
await async_wait_recording_done(hass)
- await client.send_json({"id": 1, "type": "backup/start"})
+ await client.send_json_auto_id({"type": "backup/start"})
response = await client.receive_json()
assert response["success"]
- await client.send_json({"id": 2, "type": "backup/end"})
+ await client.send_json_auto_id({"type": "backup/end"})
response = await client.receive_json()
assert response["success"]
@@ -2349,7 +2275,7 @@ async def test_backup_end_without_start(
# Ensure there are no queued events
await async_wait_recording_done(hass)
- await client.send_json({"id": 1, "type": "backup/end"})
+ await client.send_json_auto_id({"type": "backup/end"})
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "database_unlock_failed"
@@ -2393,7 +2319,7 @@ async def test_get_statistics_metadata(
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
- await client.send_json({"id": 1, "type": "recorder/get_statistics_metadata"})
+ await client.send_json_auto_id({"type": "recorder/get_statistics_metadata"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
@@ -2442,9 +2368,8 @@ async def test_get_statistics_metadata(
)
await async_wait_recording_done(hass)
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/get_statistics_metadata",
"statistic_ids": ["test:total_gas"],
}
@@ -2470,9 +2395,8 @@ async def test_get_statistics_metadata(
hass.states.async_set("sensor.test2", 10, attributes=attributes)
await async_wait_recording_done(hass)
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 3,
"type": "recorder/get_statistics_metadata",
"statistic_ids": ["sensor.test"],
}
@@ -2498,9 +2422,8 @@ async def test_get_statistics_metadata(
hass.states.async_remove("sensor.test")
await hass.async_block_till_done()
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 4,
"type": "recorder/get_statistics_metadata",
"statistic_ids": ["sensor.test"],
}
@@ -2568,9 +2491,8 @@ async def test_import_statistics(
"unit_of_measurement": "kWh",
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/import_statistics",
"metadata": imported_metadata,
"stats": [imported_statistics1, imported_statistics2],
@@ -2656,9 +2578,8 @@ async def test_import_statistics(
"sum": 6,
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 2,
"type": "recorder/import_statistics",
"metadata": imported_metadata,
"stats": [external_statistics],
@@ -2702,9 +2623,8 @@ async def test_import_statistics(
"sum": 5,
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 3,
"type": "recorder/import_statistics",
"metadata": imported_metadata,
"stats": [external_statistics],
@@ -2785,9 +2705,8 @@ async def test_adjust_sum_statistics_energy(
"unit_of_measurement": "kWh",
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/import_statistics",
"metadata": imported_metadata,
"stats": [imported_statistics1, imported_statistics2],
@@ -2852,9 +2771,8 @@ async def test_adjust_sum_statistics_energy(
}
# Adjust previously inserted statistics in kWh
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 4,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
@@ -2893,9 +2811,8 @@ async def test_adjust_sum_statistics_energy(
}
# Adjust previously inserted statistics in MWh
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 5,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
@@ -2981,9 +2898,8 @@ async def test_adjust_sum_statistics_gas(
"unit_of_measurement": "m³",
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/import_statistics",
"metadata": imported_metadata,
"stats": [imported_statistics1, imported_statistics2],
@@ -3048,9 +2964,8 @@ async def test_adjust_sum_statistics_gas(
}
# Adjust previously inserted statistics in m³
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 4,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
@@ -3089,9 +3004,8 @@ async def test_adjust_sum_statistics_gas(
}
# Adjust previously inserted statistics in ft³
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 5,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
@@ -3194,9 +3108,8 @@ async def test_adjust_sum_statistics_errors(
"unit_of_measurement": statistic_unit,
}
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": 1,
"type": "recorder/import_statistics",
"metadata": imported_metadata,
"stats": [imported_statistics1, imported_statistics2],
@@ -3262,10 +3175,8 @@ async def test_adjust_sum_statistics_errors(
}
# Try to adjust statistics
- msg_id = 2
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": msg_id,
"type": "recorder/adjust_sum_statistics",
"statistic_id": "sensor.does_not_exist",
"start_time": period2.isoformat(),
@@ -3282,10 +3193,8 @@ async def test_adjust_sum_statistics_errors(
assert stats == previous_stats
for unit in invalid_units:
- msg_id += 1
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": msg_id,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
@@ -3302,10 +3211,8 @@ async def test_adjust_sum_statistics_errors(
assert stats == previous_stats
for unit in valid_units:
- msg_id += 1
- await client.send_json(
+ await client.send_json_auto_id(
{
- "id": msg_id,
"type": "recorder/adjust_sum_statistics",
"statistic_id": statistic_id,
"start_time": period2.isoformat(),
diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py
index fa37b2210e7..ddf550dc376 100644
--- a/tests/components/sonos/test_media_player.py
+++ b/tests/components/sonos/test_media_player.py
@@ -1,26 +1,45 @@
"""Tests for the Sonos Media Player platform."""
from homeassistant.const import STATE_IDLE
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.device_registry import (
+ CONNECTION_NETWORK_MAC,
+ CONNECTION_UPNP,
+ DeviceRegistry,
+)
async def test_device_registry(
- hass: HomeAssistant, async_autosetup_sonos, soco
+ hass: HomeAssistant, device_registry: DeviceRegistry, async_autosetup_sonos, soco
) -> None:
"""Test sonos device registered in the device registry."""
- device_registry = dr.async_get(hass)
reg_device = device_registry.async_get_device(
identifiers={("sonos", "RINCON_test")}
)
+ assert reg_device is not None
assert reg_device.model == "Model Name"
assert reg_device.sw_version == "13.1"
assert reg_device.connections == {
- (dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55"),
- (dr.CONNECTION_UPNP, "uuid:RINCON_test"),
+ (CONNECTION_NETWORK_MAC, "00:11:22:33:44:55"),
+ (CONNECTION_UPNP, "uuid:RINCON_test"),
}
assert reg_device.manufacturer == "Sonos"
- assert reg_device.suggested_area == "Zone A"
assert reg_device.name == "Zone A"
+ # Default device provides battery info, area should not be suggested
+ assert reg_device.suggested_area is None
+
+
+async def test_device_registry_not_portable(
+ hass: HomeAssistant, device_registry: DeviceRegistry, async_setup_sonos, soco
+) -> None:
+ """Test non-portable sonos device registered in the device registry to ensure area suggested."""
+ soco.get_battery_info.return_value = {}
+ await async_setup_sonos()
+
+ reg_device = device_registry.async_get_device(
+ identifiers={("sonos", "RINCON_test")}
+ )
+ assert reg_device is not None
+ assert reg_device.suggested_area == "Zone A"
async def test_entity_basic(
diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py
index f2b4e41ed71..2c8e12e04bf 100644
--- a/tests/components/swiss_public_transport/test_init.py
+++ b/tests/components/swiss_public_transport/test_init.py
@@ -45,25 +45,26 @@ CONNECTIONS = [
]
-async def test_migration_1_to_2(
+async def test_migration_1_1_to_1_2(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test successful setup."""
+ config_entry_faulty = MockConfigEntry(
+ domain=DOMAIN,
+ data=MOCK_DATA_STEP,
+ title="MIGRATION_TEST",
+ version=1,
+ minor_version=1,
+ )
+ config_entry_faulty.add_to_hass(hass)
+
with patch(
"homeassistant.components.swiss_public_transport.OpendataTransport",
return_value=AsyncMock(),
) as mock:
mock().connections = CONNECTIONS
- config_entry_faulty = MockConfigEntry(
- domain=DOMAIN,
- data=MOCK_DATA_STEP,
- title="MIGRATION_TEST",
- minor_version=1,
- )
- config_entry_faulty.add_to_hass(hass)
-
# Setup the config entry
await hass.config_entries.async_setup(config_entry_faulty.entry_id)
await hass.async_block_till_done()
diff --git a/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr b/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr
index a27a210c46e..f52cb3a88a5 100644
--- a/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr
+++ b/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr
@@ -3,8 +3,10 @@
dict({
'data': dict({
'3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8': dict({
- '__type': "",
- 'repr': "PriceInfo(status=, e5=1.719, e10=1.659, diesel=1.659)",
+ 'diesel': 1.659,
+ 'e10': 1.659,
+ 'e5': 1.719,
+ 'status': 'open',
}),
}),
'entry': dict({
diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py
index 684d7de0e82..28b50ba72ea 100644
--- a/tests/components/tesla_wall_connector/test_sensor.py
+++ b/tests/components/tesla_wall_connector/test_sensor.py
@@ -47,7 +47,7 @@ async def test_sensors(hass: HomeAssistant) -> None:
"sensor.tesla_wall_connector_phase_c_voltage", "232.1", "230"
),
EntityAndExpectedValues(
- "sensor.tesla_wall_connector_session_energy", "1234.56", "112.2"
+ "sensor.tesla_wall_connector_session_energy", "1.23456", "0.1122"
),
]
diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensors.ambr
index 2fbd6764081..aacaad1d7e4 100644
--- a/tests/components/tessie/snapshots/test_binary_sensors.ambr
+++ b/tests/components/tessie/snapshots/test_binary_sensors.ambr
@@ -19,7 +19,7 @@
'name': None,
'options': dict({
}),
- 'original_device_class': ,
+ 'original_device_class': None,
'original_icon': None,
'original_name': 'Auto seat climate left',
'platform': 'tessie',
@@ -33,7 +33,6 @@
# name: test_binary_sensors[binary_sensor.test_auto_seat_climate_left-state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'device_class': 'heat',
'friendly_name': 'Test Auto seat climate left',
}),
'context': ,
@@ -63,7 +62,7 @@
'name': None,
'options': dict({
}),
- 'original_device_class': ,
+ 'original_device_class': None,
'original_icon': None,
'original_name': 'Auto seat climate right',
'platform': 'tessie',
@@ -77,7 +76,6 @@
# name: test_binary_sensors[binary_sensor.test_auto_seat_climate_right-state]
StateSnapshot({
'attributes': ReadOnlyDict({
- 'device_class': 'heat',
'friendly_name': 'Test Auto seat climate right',
}),
'context': ,
@@ -87,6 +85,49 @@
'state': 'on',
})
# ---
+# name: test_binary_sensors[binary_sensor.test_auto_steering_wheel_heater-entry]
+ EntityRegistryEntrySnapshot({
+ 'aliases': set({
+ }),
+ 'area_id': None,
+ 'capabilities': None,
+ 'config_entry_id': ,
+ 'device_class': None,
+ 'device_id': ,
+ 'disabled_by': None,
+ 'domain': 'binary_sensor',
+ 'entity_category': ,
+ 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater',
+ 'has_entity_name': True,
+ 'hidden_by': None,
+ 'icon': None,
+ 'id': ,
+ 'name': None,
+ 'options': dict({
+ }),
+ 'original_device_class': None,
+ 'original_icon': None,
+ 'original_name': 'Auto steering wheel heater',
+ 'platform': 'tessie',
+ 'previous_unique_id': None,
+ 'supported_features': 0,
+ 'translation_key': 'climate_state_auto_steering_wheel_heat',
+ 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat',
+ 'unit_of_measurement': None,
+ })
+# ---
+# name: test_binary_sensors[binary_sensor.test_auto_steering_wheel_heater-state]
+ StateSnapshot({
+ 'attributes': ReadOnlyDict({
+ 'friendly_name': 'Test Auto steering wheel heater',
+ }),
+ 'context': ,
+ 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater',
+ 'last_changed': ,
+ 'last_updated': ,
+ 'state': 'on',
+ })
+# ---
# name: test_binary_sensors[binary_sensor.test_battery_heater-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -527,50 +568,6 @@
'state': 'off',
})
# ---
-# name: test_binary_sensors[binary_sensor.test_heat-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'binary_sensor',
- 'entity_category': ,
- 'entity_id': 'binary_sensor.test_heat',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': ,
- 'original_icon': None,
- 'original_name': 'Heat',
- 'platform': 'tessie',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'climate_state_auto_steering_wheel_heat',
- 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_binary_sensors[binary_sensor.test_heat-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'device_class': 'heat',
- 'friendly_name': 'Test Heat',
- }),
- 'context': ,
- 'entity_id': 'binary_sensor.test_heat',
- 'last_changed': ,
- 'last_updated': ,
- 'state': 'on',
- })
-# ---
# name: test_binary_sensors[binary_sensor.test_preconditioning_enabled-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr
index 2f5e1e8ddb2..b9a423bfa9a 100644
--- a/tests/components/tessie/snapshots/test_sensor.ambr
+++ b/tests/components/tessie/snapshots/test_sensor.ambr
@@ -347,6 +347,68 @@
'state': '224',
})
# ---
+# name: test_sensors[sensor.test_charging-entry]
+ EntityRegistryEntrySnapshot({
+ 'aliases': set({
+ }),
+ 'area_id': None,
+ 'capabilities': dict({
+ 'options': list([
+ 'starting',
+ 'charging',
+ 'stopped',
+ 'complete',
+ 'disconnected',
+ 'no_power',
+ ]),
+ }),
+ 'config_entry_id': ,
+ 'device_class': None,
+ 'device_id': ,
+ 'disabled_by': None,
+ 'domain': 'sensor',
+ 'entity_category': None,
+ 'entity_id': 'sensor.test_charging',
+ 'has_entity_name': True,
+ 'hidden_by': None,
+ 'icon': None,
+ 'id': ,
+ 'name': None,
+ 'options': dict({
+ }),
+ 'original_device_class': ,
+ 'original_icon': 'mdi:ev-station',
+ 'original_name': 'Charging',
+ 'platform': 'tessie',
+ 'previous_unique_id': None,
+ 'supported_features': 0,
+ 'translation_key': 'charge_state_charging_state',
+ 'unique_id': 'VINVINVIN-charge_state_charging_state',
+ 'unit_of_measurement': None,
+ })
+# ---
+# name: test_sensors[sensor.test_charging-state]
+ StateSnapshot({
+ 'attributes': ReadOnlyDict({
+ 'device_class': 'enum',
+ 'friendly_name': 'Test Charging',
+ 'icon': 'mdi:ev-station',
+ 'options': list([
+ 'starting',
+ 'charging',
+ 'stopped',
+ 'complete',
+ 'disconnected',
+ 'no_power',
+ ]),
+ }),
+ 'context': ,
+ 'entity_id': 'sensor.test_charging',
+ 'last_changed': ,
+ 'last_updated': ,
+ 'state': 'charging',
+ })
+# ---
# name: test_sensors[sensor.test_destination-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -368,7 +430,7 @@
'options': dict({
}),
'original_device_class': None,
- 'original_icon': 'mdi:map-marker',
+ 'original_icon': None,
'original_name': 'Destination',
'platform': 'tessie',
'previous_unique_id': None,
@@ -382,7 +444,6 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Destination',
- 'icon': 'mdi:map-marker',
}),
'context': ,
'entity_id': 'sensor.test_destination',
@@ -776,7 +837,7 @@
'options': dict({
}),
'original_device_class': ,
- 'original_icon': 'mdi:car-shift-pattern',
+ 'original_icon': None,
'original_name': 'Shift state',
'platform': 'tessie',
'previous_unique_id': None,
@@ -791,7 +852,6 @@
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Test Shift state',
- 'icon': 'mdi:car-shift-pattern',
'options': list([
'p',
'd',
diff --git a/tests/components/tessie/test_binary_sensors.py b/tests/components/tessie/test_binary_sensors.py
index ca53a60d493..b6dccd9d3b1 100644
--- a/tests/components/tessie/test_binary_sensors.py
+++ b/tests/components/tessie/test_binary_sensors.py
@@ -1,4 +1,5 @@
"""Test the Tessie binary sensor platform."""
+import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
@@ -8,6 +9,7 @@ from homeassistant.helpers import entity_registry as er
from .common import assert_entities, setup_platform
+@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensors(
hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry
) -> None:
diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py
index 4141b28849c..10cc6192d38 100644
--- a/tests/components/traccar_server/conftest.py
+++ b/tests/components/traccar_server/conftest.py
@@ -3,12 +3,79 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
+from pytraccar import ApiClient
+
+from homeassistant.components.traccar_server.const import (
+ CONF_CUSTOM_ATTRIBUTES,
+ CONF_EVENTS,
+ CONF_MAX_ACCURACY,
+ CONF_SKIP_ACCURACY_FILTER_FOR,
+ DOMAIN,
+)
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+
+from tests.common import (
+ MockConfigEntry,
+ load_json_array_fixture,
+ load_json_object_fixture,
+)
@pytest.fixture
-def mock_setup_entry() -> Generator[AsyncMock, None, None]:
- """Override async_setup_entry."""
+def mock_traccar_api_client() -> Generator[AsyncMock, None, None]:
+ """Mock a Traccar ApiClient client."""
with patch(
- "homeassistant.components.traccar_server.async_setup_entry", return_value=True
- ) as mock_setup_entry:
- yield mock_setup_entry
+ "homeassistant.components.traccar_server.ApiClient",
+ autospec=True,
+ ) as mock_client, patch(
+ "homeassistant.components.traccar_server.config_flow.ApiClient",
+ new=mock_client,
+ ):
+ client: ApiClient = mock_client.return_value
+ client.get_devices.return_value = load_json_array_fixture(
+ "traccar_server/devices.json"
+ )
+ client.get_geofences.return_value = load_json_array_fixture(
+ "traccar_server/geofences.json"
+ )
+ client.get_positions.return_value = load_json_array_fixture(
+ "traccar_server/positions.json"
+ )
+ client.get_server.return_value = load_json_object_fixture(
+ "traccar_server/server.json"
+ )
+ client.get_reports_events.return_value = load_json_array_fixture(
+ "traccar_server/reports_events.json"
+ )
+
+ yield client
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+ """Mock a Traccar Server config entry."""
+ return MockConfigEntry(
+ domain=DOMAIN,
+ title="1.1.1.1:8082",
+ data={
+ CONF_HOST: "1.1.1.1",
+ CONF_PORT: "8082",
+ CONF_USERNAME: "test@example.org",
+ CONF_PASSWORD: "ThisIsNotThePasswordYouAreL00kingFor",
+ CONF_SSL: False,
+ CONF_VERIFY_SSL: True,
+ },
+ options={
+ CONF_CUSTOM_ATTRIBUTES: ["custom_attr_1"],
+ CONF_EVENTS: ["device_moving"],
+ CONF_MAX_ACCURACY: 5.0,
+ CONF_SKIP_ACCURACY_FILTER_FOR: [],
+ },
+ )
diff --git a/tests/components/traccar_server/fixtures/devices.json b/tests/components/traccar_server/fixtures/devices.json
new file mode 100644
index 00000000000..b04d53d9fdf
--- /dev/null
+++ b/tests/components/traccar_server/fixtures/devices.json
@@ -0,0 +1,17 @@
+[
+ {
+ "id": 0,
+ "name": "X-Wing",
+ "uniqueId": "abc123",
+ "status": "unknown",
+ "disabled": false,
+ "lastUpdate": "1970-01-01T00:00:00Z",
+ "positionId": 0,
+ "groupId": 0,
+ "phone": null,
+ "model": "1337",
+ "contact": null,
+ "category": "starfighter",
+ "attributes": {}
+ }
+]
diff --git a/tests/components/traccar_server/fixtures/geofences.json b/tests/components/traccar_server/fixtures/geofences.json
new file mode 100644
index 00000000000..5452c0485de
--- /dev/null
+++ b/tests/components/traccar_server/fixtures/geofences.json
@@ -0,0 +1,10 @@
+[
+ {
+ "id": 0,
+ "name": "Tatooine",
+ "description": "A harsh desert world orbiting twin suns in the galaxy's Outer Rim",
+ "area": "string",
+ "calendarId": 0,
+ "attributes": {}
+ }
+]
diff --git a/tests/components/traccar_server/fixtures/positions.json b/tests/components/traccar_server/fixtures/positions.json
new file mode 100644
index 00000000000..6b65116e804
--- /dev/null
+++ b/tests/components/traccar_server/fixtures/positions.json
@@ -0,0 +1,24 @@
+[
+ {
+ "id": 0,
+ "deviceId": 0,
+ "protocol": "C-3PO",
+ "deviceTime": "1970-01-01T00:00:00Z",
+ "fixTime": "1970-01-01T00:00:00Z",
+ "serverTime": "1970-01-01T00:00:00Z",
+ "outdated": true,
+ "valid": true,
+ "latitude": 52.0,
+ "longitude": 25.0,
+ "altitude": 546841384638,
+ "speed": 4568795,
+ "course": 360,
+ "address": "Mos Espa",
+ "accuracy": 3.5,
+ "network": {},
+ "geofenceIds": [0],
+ "attributes": {
+ "custom_attr_1": "custom_attr_1_value"
+ }
+ }
+]
diff --git a/tests/components/traccar_server/fixtures/reports_events.json b/tests/components/traccar_server/fixtures/reports_events.json
new file mode 100644
index 00000000000..e8280471d96
--- /dev/null
+++ b/tests/components/traccar_server/fixtures/reports_events.json
@@ -0,0 +1,12 @@
+[
+ {
+ "id": 0,
+ "type": "deviceMoving",
+ "eventTime": "2019-08-24T14:15:22Z",
+ "deviceId": 0,
+ "positionId": 0,
+ "geofenceId": 0,
+ "maintenanceId": 0,
+ "attributes": {}
+ }
+]
diff --git a/tests/components/traccar_server/fixtures/server.json b/tests/components/traccar_server/fixtures/server.json
new file mode 100644
index 00000000000..039b6bfa1f4
--- /dev/null
+++ b/tests/components/traccar_server/fixtures/server.json
@@ -0,0 +1,21 @@
+{
+ "id": 0,
+ "registration": true,
+ "readonly": true,
+ "deviceReadonly": true,
+ "limitCommands": true,
+ "map": null,
+ "bingKey": null,
+ "mapUrl": null,
+ "poiLayer": null,
+ "latitude": 0,
+ "longitude": 0,
+ "zoom": 0,
+ "twelveHourFormat": true,
+ "version": "99.99",
+ "forceSettings": true,
+ "coordinateFormat": null,
+ "openIdEnabled": true,
+ "openIdForce": true,
+ "attributes": {}
+}
diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py
index 028bc99cec5..00a987a4711 100644
--- a/tests/components/traccar_server/test_config_flow.py
+++ b/tests/components/traccar_server/test_config_flow.py
@@ -1,6 +1,7 @@
"""Test the Traccar Server config flow."""
+from collections.abc import Generator
from typing import Any
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock
import pytest
from pytraccar import TraccarException
@@ -29,7 +30,10 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
-async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
+async def test_form(
+ hass: HomeAssistant,
+ mock_traccar_api_client: Generator[AsyncMock, None, None],
+) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -37,19 +41,15 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
- with patch(
- "homeassistant.components.traccar_server.config_flow.ApiClient.get_server",
- return_value={"id": "1234"},
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_HOST: "1.1.1.1",
- CONF_USERNAME: "test-username",
- CONF_PASSWORD: "test-password",
- },
- )
- await hass.async_block_till_done()
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_HOST: "1.1.1.1",
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ },
+ )
+ await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "1.1.1.1:8082"
@@ -61,7 +61,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
CONF_SSL: False,
CONF_VERIFY_SSL: True,
}
- assert len(mock_setup_entry.mock_calls) == 1
+ assert result["result"].state == config_entries.ConfigEntryState.LOADED
@pytest.mark.parametrize(
@@ -73,44 +73,40 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
)
async def test_form_cannot_connect(
hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
side_effect: Exception,
error: str,
+ mock_traccar_api_client: Generator[AsyncMock, None, None],
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch(
- "homeassistant.components.traccar_server.config_flow.ApiClient.get_server",
- side_effect=side_effect,
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_HOST: "1.1.1.1",
- CONF_USERNAME: "test-username",
- CONF_PASSWORD: "test-password",
- },
- )
+ mock_traccar_api_client.get_server.side_effect = side_effect
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_HOST: "1.1.1.1",
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ },
+ )
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": error}
- with patch(
- "homeassistant.components.traccar_server.config_flow.ApiClient.get_server",
- return_value={"id": "1234"},
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_HOST: "1.1.1.1",
- CONF_USERNAME: "test-username",
- CONF_PASSWORD: "test-password",
- },
- )
- await hass.async_block_till_done()
+ mock_traccar_api_client.get_server.side_effect = None
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_HOST: "1.1.1.1",
+ CONF_USERNAME: "test-username",
+ CONF_PASSWORD: "test-password",
+ },
+ )
+ await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "1.1.1.1:8082"
@@ -122,27 +118,23 @@ async def test_form_cannot_connect(
CONF_SSL: False,
CONF_VERIFY_SSL: True,
}
- assert len(mock_setup_entry.mock_calls) == 1
+
+ assert result["result"].state == config_entries.ConfigEntryState.LOADED
async def test_options(
hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
+ mock_config_entry: MockConfigEntry,
+ mock_traccar_api_client: Generator[AsyncMock, None, None],
) -> None:
"""Test options flow."""
+ mock_config_entry.add_to_hass(hass)
- config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={},
- )
+ assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
- config_entry.add_to_hass(hass)
+ assert mock_config_entry.options.get(CONF_MAX_ACCURACY) == 5.0
- assert await hass.config_entries.async_setup(config_entry.entry_id)
-
- assert CONF_MAX_ACCURACY not in config_entry.options
-
- result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
@@ -151,7 +143,7 @@ async def test_options(
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
- assert config_entry.options == {
+ assert mock_config_entry.options == {
CONF_MAX_ACCURACY: 2.0,
CONF_EVENTS: [],
CONF_CUSTOM_ATTRIBUTES: [],
@@ -234,10 +226,10 @@ async def test_options(
)
async def test_import_from_yaml(
hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
imported: dict[str, Any],
data: dict[str, Any],
options: dict[str, Any],
+ mock_traccar_api_client: Generator[AsyncMock, None, None],
) -> None:
"""Test importing configuration from YAML."""
result = await hass.config_entries.flow.async_init(
@@ -249,12 +241,10 @@ async def test_import_from_yaml(
assert result["title"] == f"{data[CONF_HOST]}:{data[CONF_PORT]}"
assert result["data"] == data
assert result["options"] == options
+ assert result["result"].state == config_entries.ConfigEntryState.LOADED
-async def test_abort_import_already_configured(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
-) -> None:
+async def test_abort_import_already_configured(hass: HomeAssistant) -> None:
"""Test abort for existing server while importing."""
config_entry = MockConfigEntry(
@@ -284,18 +274,12 @@ async def test_abort_import_already_configured(
async def test_abort_already_configured(
hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
+ mock_config_entry: MockConfigEntry,
+ mock_traccar_api_client: Generator[AsyncMock, None, None],
) -> None:
"""Test abort for existing server."""
-
- config_entry = MockConfigEntry(
- domain=DOMAIN,
- data={CONF_HOST: "1.1.1.1", CONF_PORT: "8082"},
- )
-
- config_entry.add_to_hass(hass)
-
- assert await hass.config_entries.async_setup(config_entry.entry_id)
+ mock_config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
diff --git a/tests/helpers/test_redact.py b/tests/helpers/test_redact.py
new file mode 100644
index 00000000000..73461012907
--- /dev/null
+++ b/tests/helpers/test_redact.py
@@ -0,0 +1,94 @@
+"""Test the data redation helper."""
+from homeassistant.helpers.redact import REDACTED, async_redact_data, partial_redact
+
+
+def test_redact() -> None:
+ """Test the async_redact_data helper."""
+ data = {
+ "key1": "value1",
+ "key2": ["value2_a", "value2_b"],
+ "key3": [["value_3a", "value_3b"], ["value_3c", "value_3d"]],
+ "key4": {
+ "key4_1": "value4_1",
+ "key4_2": ["value4_2a", "value4_2b"],
+ "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]],
+ },
+ "key5": None,
+ "key6": "",
+ "key7": False,
+ }
+
+ to_redact = {
+ "key1",
+ "key3",
+ "key4_1",
+ "key5",
+ "key6",
+ "key7",
+ }
+
+ assert async_redact_data(data, to_redact) == {
+ "key1": REDACTED,
+ "key2": ["value2_a", "value2_b"],
+ "key3": REDACTED,
+ "key4": {
+ "key4_1": REDACTED,
+ "key4_2": ["value4_2a", "value4_2b"],
+ "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]],
+ },
+ "key5": None,
+ "key6": "",
+ "key7": REDACTED,
+ }
+
+
+def test_redact_custom_redact_function() -> None:
+ """Test the async_redact_data helper."""
+ data = {
+ "key1": "val1val1val1val1",
+ "key2": ["value2_a", "value2_b"],
+ "key3": [
+ ["val_3avalue_3avalue_3a", "value_3bvalue_3bvalue_3b"],
+ ["value_3cvalue_3cvalue_3c", "value_3dvalue_3dvalue_3d"],
+ ],
+ "key4": {
+ "key4_1": "val4_1val4_1val4_1val4_1",
+ "key4_2": ["value4_2a", "value4_2b"],
+ "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]],
+ },
+ "key5": None,
+ "key6": "",
+ "key7": False,
+ }
+
+ to_redact = {
+ "key1": partial_redact,
+ "key3": partial_redact, # Value is a list, will default to REDACTED
+ "key4_1": partial_redact,
+ "key5": partial_redact,
+ "key6": partial_redact,
+ "key7": partial_redact, # Value is False, will default to REDACTED
+ }
+
+ assert async_redact_data(data, to_redact) == {
+ "key1": "val1***val1",
+ "key2": ["value2_a", "value2_b"],
+ "key3": REDACTED,
+ "key4": {
+ "key4_1": "val4***l4_1",
+ "key4_2": ["value4_2a", "value4_2b"],
+ "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]],
+ },
+ "key5": None,
+ "key6": "",
+ "key7": REDACTED,
+ }
+
+
+def test_partial_redact() -> None:
+ """Test the partial_redact helper."""
+ assert partial_redact(None, 0, 0) == REDACTED
+ assert partial_redact("short_string") == REDACTED
+ assert partial_redact("long_enough_string") == "long***ring"
+ assert partial_redact("long_enough_string", 2, 2) == "lo***ng"
+ assert partial_redact("long_enough_string", 0, 0) == REDACTED
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index 07e68e081b3..90f9b65aaba 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -1,4 +1,5 @@
"""Test service helpers."""
+import asyncio
from collections.abc import Iterable
from copy import deepcopy
from typing import Any
@@ -782,6 +783,84 @@ async def test_async_get_all_descriptions_dynamically_created_services(
}
+async def test_async_get_all_descriptions_new_service_added_while_loading(
+ hass: HomeAssistant,
+) -> None:
+ """Test async_get_all_descriptions when a new service is added while loading translations."""
+ group = hass.components.group
+ group_config = {group.DOMAIN: {}}
+ await async_setup_component(hass, group.DOMAIN, group_config)
+ descriptions = await service.async_get_all_descriptions(hass)
+
+ assert len(descriptions) == 1
+
+ assert "description" in descriptions["group"]["reload"]
+ assert "fields" in descriptions["group"]["reload"]
+
+ logger = hass.components.logger
+ logger_domain = logger.DOMAIN
+ logger_config = {logger_domain: {}}
+
+ translations_called = asyncio.Event()
+ translations_wait = asyncio.Event()
+
+ async def async_get_translations(
+ hass: HomeAssistant,
+ language: str,
+ category: str,
+ integrations: Iterable[str] | None = None,
+ config_flow: bool | None = None,
+ ) -> dict[str, Any]:
+ """Return all backend translations."""
+ translations_called.set()
+ await translations_wait.wait()
+ translation_key_prefix = f"component.{logger_domain}.services.set_default_level"
+ return {
+ f"{translation_key_prefix}.name": "Translated name",
+ f"{translation_key_prefix}.description": "Translated description",
+ f"{translation_key_prefix}.fields.level.name": "Field name",
+ f"{translation_key_prefix}.fields.level.description": "Field description",
+ f"{translation_key_prefix}.fields.level.example": "Field example",
+ }
+
+ with patch(
+ "homeassistant.helpers.service.translation.async_get_translations",
+ side_effect=async_get_translations,
+ ):
+ await async_setup_component(hass, logger_domain, logger_config)
+ task = asyncio.create_task(service.async_get_all_descriptions(hass))
+ await translations_called.wait()
+ # Now register a new service while translations are being loaded
+ hass.services.async_register(logger_domain, "new_service", lambda x: None, None)
+ service.async_set_service_schema(
+ hass, logger_domain, "new_service", {"description": "new service"}
+ )
+ translations_wait.set()
+ descriptions = await task
+
+ # Two domains should be present
+ assert len(descriptions) == 2
+
+ logger_descriptions = descriptions[logger_domain]
+
+ # The new service was loaded after the translations were loaded
+ # so it should not appear until the next time we fetch
+ assert "new_service" not in logger_descriptions
+
+ set_default_level = logger_descriptions["set_default_level"]
+
+ assert set_default_level["name"] == "Translated name"
+ assert set_default_level["description"] == "Translated description"
+ set_default_level_fields = set_default_level["fields"]
+ assert set_default_level_fields["level"]["name"] == "Field name"
+ assert set_default_level_fields["level"]["description"] == "Field description"
+ assert set_default_level_fields["level"]["example"] == "Field example"
+
+ descriptions = await service.async_get_all_descriptions(hass)
+ assert "description" in descriptions[logger_domain]["new_service"]
+ assert descriptions[logger_domain]["new_service"]["description"] == "new service"
+
+
async def test_register_with_mixed_case(hass: HomeAssistant) -> None:
"""Test registering a service with mixed case.
diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py
index e84e8cbe390..e22aca289a8 100644
--- a/tests/testing_config/custom_components/test/light.py
+++ b/tests/testing_config/custom_components/test/light.py
@@ -2,7 +2,7 @@
Call init before using it in your tests to ensure clean test data.
"""
-from homeassistant.components.light import LightEntity
+from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.const import STATE_OFF, STATE_ON
from tests.common import MockToggleEntity
@@ -32,13 +32,21 @@ async def async_setup_platform(
async_add_entities_callback(ENTITIES)
+TURN_ON_ARG_TO_COLOR_MODE = {
+ "hs_color": ColorMode.HS,
+ "xy_color": ColorMode.XY,
+ "rgb_color": ColorMode.RGB,
+ "rgbw_color": ColorMode.RGBW,
+ "rgbww_color": ColorMode.RGBWW,
+ "color_temp_kelvin": ColorMode.COLOR_TEMP,
+}
+
+
class MockLight(MockToggleEntity, LightEntity):
"""Mock light class."""
- color_mode = None
_attr_max_color_temp_kelvin = 6500
_attr_min_color_temp_kelvin = 2000
- supported_color_modes = None
supported_features = 0
brightness = None
@@ -49,6 +57,23 @@ class MockLight(MockToggleEntity, LightEntity):
rgbww_color = None
xy_color = None
+ def __init__(
+ self,
+ name,
+ state,
+ unique_id=None,
+ supported_color_modes: set[ColorMode] | None = None,
+ ):
+ """Initialize the mock light."""
+ super().__init__(name, state, unique_id)
+ if supported_color_modes is None:
+ supported_color_modes = {ColorMode.ONOFF}
+ self._attr_supported_color_modes = supported_color_modes
+ color_mode = ColorMode.UNKNOWN
+ if len(supported_color_modes) == 1:
+ color_mode = next(iter(supported_color_modes))
+ self._attr_color_mode = color_mode
+
def turn_on(self, **kwargs):
"""Turn the entity on."""
super().turn_on(**kwargs)
@@ -65,3 +90,5 @@ class MockLight(MockToggleEntity, LightEntity):
setattr(self, key, value)
if key == "white":
setattr(self, "brightness", value)
+ if key in TURN_ON_ARG_TO_COLOR_MODE:
+ self._attr_color_mode = TURN_ON_ARG_TO_COLOR_MODE[key]
diff --git a/tests/util/test_file.py b/tests/util/test_file.py
index 0b87985fe13..dc09ff83e9e 100644
--- a/tests/util/test_file.py
+++ b/tests/util/test_file.py
@@ -25,6 +25,11 @@ def test_write_utf8_file_atomic_private(tmpdir: py.path.local, func) -> None:
assert fh.read() == '{"some":"data"}'
assert os.stat(test_file).st_mode & 0o777 == 0o600
+ func(test_file, b'{"some":"data"}', True, mode="wb")
+ with open(test_file) as fh:
+ assert fh.read() == '{"some":"data"}'
+ assert os.stat(test_file).st_mode & 0o777 == 0o600
+
def test_write_utf8_file_fails_at_creation(tmpdir: py.path.local) -> None:
"""Test that failed creation of the temp file does not create an empty file."""