Compare commits

...

16 Commits

Author SHA1 Message Date
Franck Nijhof
f77bd13cc0 Bump version to 2024.2.0b2 2024-02-01 22:29:58 +01:00
Paul Bottein
fe4ad30ade Add device class to tesla wall connector session energy (#109333) 2024-02-01 22:29:49 +01:00
Joakim Sørensen
15a1a4bfdf Fix custom attribute lookup in Traccar Server (#109331) 2024-02-01 22:29:46 +01:00
Franck Nijhof
3d80c4f7f6 Update Home Assistant base image to 2024.02.0 (#109329) 2024-02-01 22:29:42 +01:00
Michael Hansen
0015af0b3c Move default response out of sentence trigger registration and into agent (#109317)
* Move default response out of trigger and into agent

* Add test
2024-02-01 22:29:39 +01:00
J. Nick Koston
a535bda821 Fix race in loading service descriptions (#109316) 2024-02-01 22:29:36 +01:00
Maciej Bieniek
ca539630a6 Do not use a battery device class for Shelly analog input sensor (#109311)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-02-01 22:29:32 +01:00
Josef Zweck
faf2a90cd1 Bump pytedee_async to 0.2.13 (#109307)
bump
2024-02-01 22:29:30 +01:00
Robert Resch
6aba79d7b9 Verify Ecovacs mqtt config (#109306) 2024-02-01 22:29:26 +01:00
Ståle Storø Hauknes
b464e77112 Bump airthings-ble to 0.6.1 (#109302)
Bump airthings-ble
2024-02-01 22:29:23 +01:00
Josh Pettersen
a8b39ce332 Remove battery charge sensor from powerwall (#109271) 2024-02-01 22:29:19 +01:00
Michael Hansen
77b25553e3 Migrate to new intent error response keys (#109269) 2024-02-01 22:29:16 +01:00
G Johansson
c31dfd6d00 Don't log warning for core integrations on new feature flags in Climate (#109250)
* Don't log for core integration on Climate new feature flags

* Add test

* Fix test
2024-02-01 22:29:12 +01:00
Brett Adams
647ac10dd9 Add climate turn on/off feature to Teslemetry (#109241) 2024-02-01 22:29:06 +01:00
Brett Adams
50dfe4dec0 Add climate on/off feature to Tessie (#109239) 2024-02-01 22:29:03 +01:00
Raman Gupta
52a8216150 Add translations for zwave_js entities and services (#109188) 2024-02-01 22:28:59 +01:00
32 changed files with 976 additions and 550 deletions

View File

@@ -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

View File

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==0.6.0"]
"requirements": ["airthings-ble==0.6.1"]
}

View File

@@ -339,6 +339,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def _report_turn_on_off(feature: str, method: str) -> None:
"""Log warning not implemented turn on/off feature."""
module = type(self).__module__
if module and "custom_components" not in module:
return
report_issue = self._suggest_report_issue()
if feature.startswith("TURN"):
message = (

View File

@@ -12,22 +12,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 +231,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 +242,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 +255,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,
)
@@ -273,9 +269,7 @@ class DefaultAgent(AbstractConversationAgent):
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,
@@ -325,7 +319,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 +327,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,
)
@@ -795,7 +789,7 @@ class DefaultAgent(AbstractConversationAgent):
def _get_error_text(
self,
response_type: ResponseType,
error_key: ErrorKey,
lang_intents: LanguageIntents | None,
**response_args,
) -> str:
@@ -803,7 +797,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 +910,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:

View File

@@ -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.0", "home-assistant-intents==2024.2.1"]
}

View File

@@ -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)

View File

@@ -74,11 +74,16 @@ class EcovacsController:
async def initialize(self) -> None:
"""Init controller."""
mqtt_config_verfied = False
try:
devices = await self._api_client.get_devices()
credentials = await self._authenticator.authenticate()
for device_config in devices:
if isinstance(device_config, DeviceInfo):
# MQTT device
if not mqtt_config_verfied:
await self._mqtt.verify_config()
mqtt_config_verfied = True
device = Device(device_config, self._authenticator)
await device.initialize(self._mqtt)
self.devices.append(device)

View File

@@ -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",

View File

@@ -958,7 +958,6 @@ RPC_SENSORS: Final = {
sub_key="percent",
name="Analog input",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
),
}

View File

@@ -6,5 +6,5 @@
"dependencies": ["http", "webhook"],
"documentation": "https://www.home-assistant.io/integrations/tedee",
"iot_class": "local_push",
"requirements": ["pytedee-async==0.2.12"]
"requirements": ["pytedee-async==0.2.13"]
}

View File

@@ -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(

View File

@@ -39,7 +39,10 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
)
_attr_preset_modes = ["off", "keep", "dog", "camp"]

View File

@@ -45,7 +45,10 @@ class TessieClimateEntity(TessieEntity, ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
)
_attr_preset_modes: list = [
TessieClimateKeeper.OFF,

View File

@@ -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"],

View File

@@ -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"],

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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: (

View File

@@ -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)"
}
}
}

View File

@@ -16,7 +16,7 @@ from .helpers.deprecation import (
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0b1"
PATCH_VERSION: Final = "0b2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@@ -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)

View File

@@ -29,7 +29,7 @@ hass-nabucasa==0.76.0
hassil==1.6.0
home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240131.0
home-assistant-intents==2024.1.29
home-assistant-intents==2024.2.1
httpx==0.26.0
ifaddr==0.2.0
janus==1.0.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.2.0b1"
version = "2024.2.0b2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@@ -410,7 +410,7 @@ aioymaps==1.2.2
airly==1.1.0
# homeassistant.components.airthings_ble
airthings-ble==0.6.0
airthings-ble==0.6.1
# homeassistant.components.airthings
airthings-cloud==0.2.0
@@ -1062,7 +1062,7 @@ holidays==0.41
home-assistant-frontend==20240131.0
# homeassistant.components.conversation
home-assistant-intents==2024.1.29
home-assistant-intents==2024.2.1
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -2163,7 +2163,7 @@ pyswitchbee==1.8.0
pytautulli==23.1.1
# homeassistant.components.tedee
pytedee-async==0.2.12
pytedee-async==0.2.13
# homeassistant.components.tfiac
pytfiac==0.4

View File

@@ -383,7 +383,7 @@ aioymaps==1.2.2
airly==1.1.0
# homeassistant.components.airthings_ble
airthings-ble==0.6.0
airthings-ble==0.6.1
# homeassistant.components.airthings
airthings-cloud==0.2.0
@@ -858,7 +858,7 @@ holidays==0.41
home-assistant-frontend==20240131.0
# homeassistant.components.conversation
home-assistant-intents==2024.1.29
home-assistant-intents==2024.2.1
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -1675,7 +1675,7 @@ pyswitchbee==1.8.0
pytautulli==23.1.1
# homeassistant.components.tedee
pytedee-async==0.2.12
pytedee-async==0.2.13
# homeassistant.components.motionmount
python-MotionMount==0.3.1

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from enum import Enum
from types import ModuleType
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
import voluptuous as vol
@@ -415,23 +415,26 @@ async def test_warning_not_implemented_turn_on_off_feature(
MockPlatform(async_setup_entry=async_setup_entry_climate_platform),
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch.object(
MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init"
):
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state is not None
assert (
"Entity climate.test (<class 'tests.components.climate.test_init."
"Entity climate.test (<class 'tests.custom_components.climate.test_init."
"test_warning_not_implemented_turn_on_off_feature.<locals>.MockClimateEntityTest'>)"
" does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method."
" Please report it to the author of the 'test' custom integration"
in caplog.text
)
assert (
"Entity climate.test (<class 'tests.components.climate.test_init."
"Entity climate.test (<class 'tests.custom_components.climate.test_init."
"test_warning_not_implemented_turn_on_off_feature.<locals>.MockClimateEntityTest'>)"
" does not set ClimateEntityFeature.TURN_ON but implements the turn_on method."
" Please report it to the author of the 'test' custom integration"
@@ -520,16 +523,19 @@ async def test_implicit_warning_not_implemented_turn_on_off_feature(
MockPlatform(async_setup_entry=async_setup_entry_climate_platform),
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch.object(
MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init"
):
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state is not None
assert (
"Entity climate.test (<class 'tests.components.climate.test_init."
"Entity climate.test (<class 'tests.custom_components.climate.test_init."
"test_implicit_warning_not_implemented_turn_on_off_feature.<locals>.MockClimateEntityTest'>)"
" implements HVACMode(s): off, heat and therefore implicitly supports the turn_on/turn_off"
" methods without setting the proper ClimateEntityFeature. Please report it to the author"
@@ -584,10 +590,13 @@ async def test_no_warning_implemented_turn_on_off_feature(
MockPlatform(async_setup_entry=async_setup_entry_climate_platform),
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch.object(
MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init"
):
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state is not None
@@ -652,10 +661,13 @@ async def test_no_warning_integration_has_migrated(
MockPlatform(async_setup_entry=async_setup_entry_climate_platform),
)
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch.object(
MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init"
):
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state is not None
@@ -672,3 +684,65 @@ async def test_no_warning_integration_has_migrated(
" implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods"
not in caplog.text
)
async def test_no_warning_on_core_integrations_for_on_off_feature_flags(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None
) -> None:
"""Test we don't warn on core integration on new turn_on/off feature flags."""
class MockClimateEntityTest(MockClimateEntity):
"""Mock Climate device."""
def turn_on(self) -> None:
"""Turn on."""
def turn_off(self) -> None:
"""Turn off."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True
async def async_setup_entry_climate_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up test climate platform via config entry."""
async_add_entities(
[MockClimateEntityTest(name="test", entity_id="climate.test")]
)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=async_setup_entry_init,
),
built_in=False,
)
mock_platform(
hass,
"test.climate",
MockPlatform(async_setup_entry=async_setup_entry_climate_platform),
)
with patch.object(
MockClimateEntityTest, "__module__", "homeassistant.components.test.climate"
):
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state is not None
assert (
"does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method."
not in caplog.text
)

View File

@@ -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',
}),
}),
}),

View File

@@ -2,6 +2,7 @@
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
@@ -430,8 +431,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 +441,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,10 +459,60 @@ 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."""
"""Test error message when area is missing a device/entity."""
area_registry.async_get_or_create("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="")
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_registry.async_get_or_create("kitchen")
result = await conversation.async_converse(
hass, "turn on the lights in the kitchen", None, Context(), None
@@ -475,10 +526,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, area_registry: ar.AreaRegistry
) -> None:
"""Test error message when no entities of a device class are exposed in an area."""
"""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="")
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 exist in an area."""
area_registry.async_get_or_create("bedroom")
result = await conversation.async_converse(
hass, "open bedroom windows", None, Context(), None
@@ -492,8 +576,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,6 +590,10 @@ 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(
@@ -601,5 +689,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"
)

View File

@@ -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,63 @@ 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(
{
"id": 5,
"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:

View File

@@ -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"

View File

@@ -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"
),
]

View File

@@ -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.