Compare commits

...

38 Commits

Author SHA1 Message Date
Franck Nijhof
f18f161efa Bump version to 2024.2.0b3 2024-02-02 17:36:51 +01:00
Bram Kragten
3c90e3d83f Update frontend to 20240202.0 (#109388) 2024-02-02 17:36:43 +01:00
Robert Resch
57a43ef151 Improve Ecovacs naming (#109372) 2024-02-02 17:36:40 +01:00
Franck Nijhof
6afc6ca126 Remove suggested area from Verisure (#109364) 2024-02-02 17:36:37 +01:00
Joakim Sørensen
92a3edc536 Specify end_time when importing Elvia data to deal with drift (#109361) 2024-02-02 17:36:34 +01:00
Robert Resch
e91e67b400 Bump deebot_client to 5.1.0 (#109360) 2024-02-02 17:36:31 +01:00
Franck Nijhof
7a7bcf1a92 Update cryptography to 42.0.2 (#109359) 2024-02-02 17:36:28 +01:00
karwosts
1383a0c13a Missing template helper translation keys (#109347) 2024-02-02 17:36:24 +01:00
J. Nick Koston
12e3077895 Ensure the purge entities service cleans up the states_meta table (#109344) 2024-02-02 17:36:21 +01:00
G Johansson
05c0973937 Add Adax migrated ClimateEntityFeatures (#109341) 2024-02-02 17:36:18 +01:00
G Johansson
7467d588c8 Add sensibo migrated ClimateEntityFeatures (#109340)
Adds sensibo migrated ClimateEntityFeatures
2024-02-02 17:36:15 +01:00
jan iversen
0c82e0a618 Correct modbus commit validation, too strict on integers (#109338) 2024-02-02 17:36:12 +01:00
J. Nick Koston
5a4f88349a Fix stale camera error message in img_util (#109325) 2024-02-02 17:36:09 +01:00
G Johansson
e720e82fd6 Add migrated ClimateEntityFeature for Nibe Heat Pump (#109140) 2024-02-02 17:36:05 +01:00
G Johansson
8ed0af2fb7 Add TURN_ON/OFF ClimateEntityFeature for KNX (#109138) 2024-02-02 17:36:02 +01:00
G Johansson
550c0bf3c3 Add migrated ClimateEntityFeature for SwitchBot Cloud (#109136) 2024-02-02 17:35:59 +01:00
G Johansson
8bb98b4146 Add TURN_ON/OFF ClimateEntityFeature for Modbus (#109133) 2024-02-02 17:35:56 +01:00
G Johansson
bd5bc6b83d Add TURN_ON/OFF ClimateEntityFeature for Matter (#108974)
* Add TURN_ON/OFF ClimateEntityFeature for Matter

* Adjust matter
2024-02-02 17:35:53 +01:00
G Johansson
a2b6b0a0bc Add TURN_ON/OFF ClimateEntityFeature for Fibaro (#108963) 2024-02-02 17:35:50 +01:00
G Johansson
41ad3d8987 Add migrated ClimateEntityFeature for Atag (#108961) 2024-02-02 17:35:45 +01:00
mkmer
3e7dc3588d Add independent session in honeywell (#108435) 2024-02-02 17:35:42 +01:00
Cyrill Raccaud
66d802b5e5 Follow up swiss_public_transport migration fix of unique ids (#107873)
improve migration fix of unique ids
- follow up to #107087
2024-02-02 17:35:38 +01:00
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
72 changed files with 1318 additions and 794 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

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

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

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

View File

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

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

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

View File

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

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

View File

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

View File

@@ -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"]
@@ -62,8 +69,10 @@ class ElviaImporter:
if not last_stats:
# First time we insert 1 years of data (if available)
until = dt_util.utcnow()
hourly_data = await self._fetch_hourly_data(
since=dt_util.now() - timedelta(days=365)
since=until - timedelta(days=365),
until=until,
)
if hourly_data is None or len(hourly_data) == 0:
return
@@ -71,7 +80,8 @@ class ElviaImporter:
_sum = 0.0
else:
hourly_data = await self._fetch_hourly_data(
since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"])
since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]),
until=dt_util.utcnow(),
)
if (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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 = "0b3"
__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

@@ -19,7 +19,7 @@ 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
@@ -28,8 +28,8 @@ habluetooth==2.4.0
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-frontend==20240202.0
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.0b3"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -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",

View File

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

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

@@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.ozmo_950_reset_brush_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'button.ozmo_950_reset_brush_lifespan',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <EntityCategory.CONFIG: 'config'>,
'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': <ANY>,
'entity_id': 'button.ozmo_950_reset_main_brush_lifespan',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'button.ozmo_950_reset_side_brush_lifespan',
'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '2024-01-01T00:00:00+00:00',

View File

@@ -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': <EntityCategory.CONFIG: 'config'>,
'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': <ANY>,
'entity_id': 'select.ozmo_950_water_amount',
'entity_id': 'select.ozmo_950_water_flow_level',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'ultrahigh',

View File

@@ -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': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'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': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'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': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# 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': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ozmo_950_brush_lifespan',
'entity_id': 'sensor.ozmo_950_cleaning_duration',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'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': <ANY>,
'entity_id': 'sensor.ozmo_950_side_brush_lifespan',
'entity_id': 'sensor.ozmo_950_main_brush_lifespan',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.ozmo_950_time_cleaned',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.ozmo_950_side_brushes_lifespan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'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': <UnitOfTime.MINUTES: 'min'>,
'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': <UnitOfTime.MINUTES: 'min'>,
'friendly_name': 'Ozmo 950 Side brushes lifespan',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.ozmo_950_time_cleaned',
'entity_id': 'sensor.ozmo_950_side_brushes_lifespan',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'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': <UnitOfTime.HOURS: 'h'>,
})
# ---
# 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': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ozmo_950_total_cleaning_duration',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'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': <UnitOfTime.HOURS: 'h'>,
})
# ---
# 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': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ozmo_950_total_time_cleaned',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '40.000',
})
# ---
# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -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': <EntityCategory.CONFIG: 'config'>,
'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': <ANY>,
'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost',
'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'on',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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.