mirror of
https://github.com/home-assistant/core.git
synced 2026-03-03 06:17:01 +01:00
Compare commits
12 Commits
python-3.1
...
knx-fronte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f633f6765 | ||
|
|
cb016b014b | ||
|
|
afb4523f63 | ||
|
|
05ad4986ac | ||
|
|
42dbd5f98f | ||
|
|
f58a514ce7 | ||
|
|
8fb384a5e1 | ||
|
|
c24302b5ce | ||
|
|
999ad9b642 | ||
|
|
36d6b4dafe | ||
|
|
06870a2e25 | ||
|
|
85eba2bb15 |
@@ -190,7 +190,7 @@ class BitcoinSensor(SensorEntity):
|
||||
elif sensor_type == "miners_revenue_usd":
|
||||
self._attr_native_value = f"{stats.miners_revenue_usd:.0f}"
|
||||
elif sensor_type == "btc_mined":
|
||||
self._attr_native_value = str(stats.btc_mined * 0.00000001)
|
||||
self._attr_native_value = str(stats.btc_mined * 1e-8)
|
||||
elif sensor_type == "trade_volume_usd":
|
||||
self._attr_native_value = f"{stats.trade_volume_usd:.1f}"
|
||||
elif sensor_type == "difficulty":
|
||||
@@ -208,13 +208,13 @@ class BitcoinSensor(SensorEntity):
|
||||
elif sensor_type == "blocks_size":
|
||||
self._attr_native_value = f"{stats.blocks_size:.1f}"
|
||||
elif sensor_type == "total_fees_btc":
|
||||
self._attr_native_value = f"{stats.total_fees_btc * 0.00000001:.2f}"
|
||||
self._attr_native_value = f"{stats.total_fees_btc * 1e-8:.2f}"
|
||||
elif sensor_type == "total_btc_sent":
|
||||
self._attr_native_value = f"{stats.total_btc_sent * 0.00000001:.2f}"
|
||||
self._attr_native_value = f"{stats.total_btc_sent * 1e-8:.2f}"
|
||||
elif sensor_type == "estimated_btc_sent":
|
||||
self._attr_native_value = f"{stats.estimated_btc_sent * 0.00000001:.2f}"
|
||||
self._attr_native_value = f"{stats.estimated_btc_sent * 1e-8:.2f}"
|
||||
elif sensor_type == "total_btc":
|
||||
self._attr_native_value = f"{stats.total_btc * 0.00000001:.2f}"
|
||||
self._attr_native_value = f"{stats.total_btc * 1e-8:.2f}"
|
||||
elif sensor_type == "total_blocks":
|
||||
self._attr_native_value = f"{stats.total_blocks:.0f}"
|
||||
elif sensor_type == "next_retarget":
|
||||
@@ -222,7 +222,7 @@ class BitcoinSensor(SensorEntity):
|
||||
elif sensor_type == "estimated_transaction_volume_usd":
|
||||
self._attr_native_value = f"{stats.estimated_transaction_volume_usd:.2f}"
|
||||
elif sensor_type == "miners_revenue_btc":
|
||||
self._attr_native_value = f"{stats.miners_revenue_btc * 0.00000001:.1f}"
|
||||
self._attr_native_value = f"{stats.miners_revenue_btc * 1e-8:.1f}"
|
||||
elif sensor_type == "market_price_usd":
|
||||
self._attr_native_value = f"{stats.market_price_usd:.2f}"
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
vol.Optional("conversation_id"): vol.Any(str, None),
|
||||
vol.Optional("language"): str,
|
||||
vol.Optional("agent_id"): agent_id_validator,
|
||||
vol.Optional("device_id"): vol.Any(str, None),
|
||||
vol.Optional("satellite_id"): vol.Any(str, None),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -64,6 +66,8 @@ async def websocket_process(
|
||||
context=connection.context(msg),
|
||||
language=msg.get("language"),
|
||||
agent_id=msg.get("agent_id"),
|
||||
device_id=msg.get("device_id"),
|
||||
satellite_id=msg.get("satellite_id"),
|
||||
)
|
||||
connection.send_result(msg["id"], result.as_dict())
|
||||
|
||||
@@ -248,6 +252,8 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
vol.Optional("conversation_id"): str,
|
||||
vol.Optional("language"): str,
|
||||
vol.Optional("agent_id"): agent_id_validator,
|
||||
vol.Optional("device_id"): vol.Any(str, None),
|
||||
vol.Optional("satellite_id"): vol.Any(str, None),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -262,6 +268,8 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
context=self.context(request),
|
||||
language=data.get("language"),
|
||||
agent_id=data.get("agent_id"),
|
||||
device_id=data.get("device_id"),
|
||||
satellite_id=data.get("satellite_id"),
|
||||
)
|
||||
|
||||
return self.json(result.as_dict())
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["dsmr_parser"],
|
||||
"requirements": ["dsmr-parser==1.4.3"]
|
||||
"requirements": ["dsmr-parser==1.5.0"]
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
if (color_temp_k := kwargs.get(ATTR_COLOR_TEMP_KELVIN)) is not None:
|
||||
# Do not use kelvin_to_mired here to prevent precision loss
|
||||
data["color_temperature"] = 1000000.0 / color_temp_k
|
||||
data["color_temperature"] = 1_000_000.0 / color_temp_k
|
||||
if color_temp_modes := _filter_color_modes(
|
||||
color_modes, LightColorCapability.COLOR_TEMPERATURE
|
||||
):
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260226.0"]
|
||||
"requirements": ["home-assistant-frontend==20260302.0"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.15.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2026.2.25.165736"
|
||||
"knx-frontend==2026.3.2.183756"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ from chip.clusters.Types import NullValue
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from .const import (
|
||||
CLEAR_ALL_INDEX,
|
||||
CRED_TYPE_FACE,
|
||||
CRED_TYPE_FINGER_VEIN,
|
||||
CRED_TYPE_FINGERPRINT,
|
||||
@@ -222,42 +221,6 @@ def _format_user_response(user_data: Any) -> LockUserData | None:
|
||||
# --- Credential management helpers ---
|
||||
|
||||
|
||||
async def _clear_user_credentials(
|
||||
matter_client: MatterClient,
|
||||
node_id: int,
|
||||
endpoint_id: int,
|
||||
user_index: int,
|
||||
) -> None:
|
||||
"""Clear all credentials for a specific user.
|
||||
|
||||
Fetches the user to get credential list, then clears each credential.
|
||||
"""
|
||||
get_user_response = await matter_client.send_device_command(
|
||||
node_id=node_id,
|
||||
endpoint_id=endpoint_id,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=user_index),
|
||||
)
|
||||
|
||||
creds = _get_attr(get_user_response, "credentials")
|
||||
if not creds:
|
||||
return
|
||||
|
||||
for cred in creds:
|
||||
cred_type = _get_attr(cred, "credentialType")
|
||||
cred_index = _get_attr(cred, "credentialIndex")
|
||||
await matter_client.send_device_command(
|
||||
node_id=node_id,
|
||||
endpoint_id=endpoint_id,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=cred_type,
|
||||
credentialIndex=cred_index,
|
||||
),
|
||||
),
|
||||
timed_request_timeout_ms=LOCK_TIMED_REQUEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
|
||||
class LockEndpointNotFoundError(HomeAssistantError):
|
||||
"""Lock endpoint not found on node."""
|
||||
|
||||
@@ -557,33 +520,16 @@ async def clear_lock_user(
|
||||
node: MatterNode,
|
||||
user_index: int,
|
||||
) -> None:
|
||||
"""Clear a user from the lock, cleaning up credentials first.
|
||||
"""Clear a user from the lock.
|
||||
|
||||
Per the Matter spec, ClearUser also clears all associated credentials
|
||||
and schedules for the user.
|
||||
Use index 0xFFFE (CLEAR_ALL_INDEX) to clear all users.
|
||||
Raises HomeAssistantError on failure.
|
||||
"""
|
||||
lock_endpoint = _get_lock_endpoint_or_raise(node)
|
||||
_ensure_usr_support(lock_endpoint)
|
||||
|
||||
if user_index == CLEAR_ALL_INDEX:
|
||||
# Clear all: clear all credentials first, then all users
|
||||
await matter_client.send_device_command(
|
||||
node_id=node.node_id,
|
||||
endpoint_id=lock_endpoint.endpoint_id,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=None,
|
||||
),
|
||||
timed_request_timeout_ms=LOCK_TIMED_REQUEST_TIMEOUT_MS,
|
||||
)
|
||||
else:
|
||||
# Clear credentials for this specific user before deleting them
|
||||
await _clear_user_credentials(
|
||||
matter_client,
|
||||
node.node_id,
|
||||
lock_endpoint.endpoint_id,
|
||||
user_index,
|
||||
)
|
||||
|
||||
await matter_client.send_device_command(
|
||||
node_id=node.node_id,
|
||||
endpoint_id=lock_endpoint.endpoint_id,
|
||||
|
||||
@@ -642,7 +642,7 @@
|
||||
},
|
||||
"services": {
|
||||
"clear_lock_credential": {
|
||||
"description": "Removes a credential from the lock.",
|
||||
"description": "Removes a credential from a lock.",
|
||||
"fields": {
|
||||
"credential_index": {
|
||||
"description": "The credential slot index to clear.",
|
||||
@@ -666,7 +666,7 @@
|
||||
"name": "Clear lock user"
|
||||
},
|
||||
"get_lock_credential_status": {
|
||||
"description": "Returns the status of a credential slot on the lock.",
|
||||
"description": "Returns the status of a credential slot on a lock.",
|
||||
"fields": {
|
||||
"credential_index": {
|
||||
"description": "The credential slot index to query.",
|
||||
@@ -684,7 +684,7 @@
|
||||
"name": "Get lock info"
|
||||
},
|
||||
"get_lock_users": {
|
||||
"description": "Returns all users configured on the lock with their credentials.",
|
||||
"description": "Returns all users configured on a lock with their credentials.",
|
||||
"name": "Get lock users"
|
||||
},
|
||||
"open_commissioning_window": {
|
||||
@@ -698,7 +698,7 @@
|
||||
"name": "Open commissioning window"
|
||||
},
|
||||
"set_lock_credential": {
|
||||
"description": "Adds or updates a credential on the lock.",
|
||||
"description": "Adds or updates a credential on a lock.",
|
||||
"fields": {
|
||||
"credential_data": {
|
||||
"description": "The credential data. For PIN: digits only. For RFID: hexadecimal string.",
|
||||
|
||||
@@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MoatConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MoatConfigEntry) -> bool:
|
||||
"""Set up Moat BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = MoatBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
@@ -42,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MoatConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -4,12 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -28,7 +26,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import MoatConfigEntry
|
||||
|
||||
SENSOR_DESCRIPTIONS = {
|
||||
(DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription(
|
||||
@@ -104,13 +102,11 @@ def sensor_update_to_bluetooth_data_update(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: MoatConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Moat BLE sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -14,15 +13,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from .api import AuthenticatedMonzoAPI
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MonzoCoordinator
|
||||
from .coordinator import MonzoConfigEntry, MonzoCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
|
||||
|
||||
@@ -39,7 +37,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Set up Monzo from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
|
||||
@@ -51,15 +49,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The Monzo integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -18,6 +20,8 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MonzoConfigEntry = ConfigEntry[MonzoCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonzoData:
|
||||
@@ -30,10 +34,13 @@ class MonzoData:
|
||||
class MonzoCoordinator(DataUpdateCoordinator[MonzoData]):
|
||||
"""Class to manage fetching Monzo data from the API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MonzoConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, api: AuthenticatedMonzoAPI
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MonzoConfigEntry,
|
||||
api: AuthenticatedMonzoAPI,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
|
||||
@@ -11,14 +11,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import MonzoCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MonzoData
|
||||
from .coordinator import MonzoConfigEntry, MonzoCoordinator, MonzoData
|
||||
from .entity import MonzoBaseEntity
|
||||
|
||||
|
||||
@@ -64,11 +61,11 @@ MODEL_POT = "Pot"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MonzoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Defer sensor setup to the shared sensor module."""
|
||||
coordinator: MonzoCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
accounts = [
|
||||
MonzoSensor(
|
||||
|
||||
@@ -218,7 +218,7 @@ def fix_coordinates(user_input: dict) -> dict:
|
||||
# Ensure coordinates have acceptable length for the Netatmo API
|
||||
for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW):
|
||||
if len(str(user_input[coordinate]).split(".")[1]) < 7:
|
||||
user_input[coordinate] = user_input[coordinate] + 0.0000001
|
||||
user_input[coordinate] = user_input[coordinate] + 1e-7
|
||||
|
||||
# Swap coordinates if entered in wrong order
|
||||
if user_input[CONF_LAT_NE] < user_input[CONF_LAT_SW]:
|
||||
|
||||
@@ -17,13 +17,11 @@ from homeassistant.components import climate as FanState
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
PRESET_SLEEP,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
@@ -40,7 +38,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import DOMAIN, SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH
|
||||
from .const import (
|
||||
CLIMATE_PRESET_SCHEDULE,
|
||||
DOMAIN,
|
||||
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH,
|
||||
)
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
@@ -206,6 +208,7 @@ RADIATOR_PRESET_MODE_MAP: dict[str, SmartRadiatorThermostatMode] = {
|
||||
PRESET_BOOST: SmartRadiatorThermostatMode.FAST_HEATING,
|
||||
PRESET_COMFORT: SmartRadiatorThermostatMode.COMFORT,
|
||||
PRESET_HOME: SmartRadiatorThermostatMode.MANUAL,
|
||||
CLIMATE_PRESET_SCHEDULE: SmartRadiatorThermostatMode.SCHEDULE,
|
||||
}
|
||||
|
||||
RADIATOR_HA_PRESET_MODE_MAP = {
|
||||
@@ -227,15 +230,10 @@ class SwitchBotCloudSmartRadiatorThermostat(SwitchBotCloudEntity, ClimateEntity)
|
||||
_attr_target_temperature_step = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
_attr_preset_modes = [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_HOME,
|
||||
PRESET_SLEEP,
|
||||
]
|
||||
_attr_preset_modes = list(RADIATOR_PRESET_MODE_MAP)
|
||||
|
||||
_attr_translation_key = "smart_radiator_thermostat"
|
||||
|
||||
_attr_preset_mode = PRESET_HOME
|
||||
|
||||
_attr_hvac_modes = [
|
||||
@@ -300,7 +298,7 @@ class SwitchBotCloudSmartRadiatorThermostat(SwitchBotCloudEntity, ClimateEntity)
|
||||
SmartRadiatorThermostatMode(mode)
|
||||
]
|
||||
|
||||
if self.preset_mode in [PRESET_NONE, PRESET_AWAY]:
|
||||
if self.preset_mode == PRESET_NONE:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
|
||||
@@ -17,6 +17,9 @@ VACUUM_FAN_SPEED_STANDARD = "standard"
|
||||
VACUUM_FAN_SPEED_STRONG = "strong"
|
||||
VACUUM_FAN_SPEED_MAX = "max"
|
||||
|
||||
|
||||
CLIMATE_PRESET_SCHEDULE = "schedule"
|
||||
|
||||
AFTER_COMMAND_REFRESH = 5
|
||||
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
|
||||
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH = 30
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
"default": "mdi:chevron-left-box"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"smart_radiator_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"schedule": "mdi:clock-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"default": "mdi:air-purifier",
|
||||
|
||||
@@ -26,6 +26,17 @@
|
||||
"name": "Previous"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"smart_radiator_thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"state_attributes": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiotankerkoenig"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotankerkoenig==0.4.2"]
|
||||
"requirements": ["aiotankerkoenig==0.5.1"]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -63,7 +64,6 @@ SERVICE_STOP = "stop"
|
||||
DEFAULT_NAME = "Vacuum cleaner robot"
|
||||
|
||||
ISSUE_SEGMENTS_CHANGED = "segments_changed"
|
||||
ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED = "segments_mapping_not_configured"
|
||||
|
||||
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",)
|
||||
|
||||
@@ -438,7 +438,14 @@ class StateVacuumEntity(
|
||||
)
|
||||
|
||||
options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {})
|
||||
area_mapping: dict[str, list[str]] = options.get("area_mapping", {})
|
||||
area_mapping: dict[str, list[str]] | None = options.get("area_mapping")
|
||||
|
||||
if area_mapping is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="area_mapping_not_configured",
|
||||
translation_placeholders={"entity_id": self.entity_id},
|
||||
)
|
||||
|
||||
# We use a dict to preserve the order of segments.
|
||||
segment_ids: dict[str, None] = {}
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"area_mapping_not_configured": {
|
||||
"message": "Area mapping is not configured for `{entity_id}`. Configure the segment-to-area mapping before using this action."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"segments_changed": {
|
||||
"description": "",
|
||||
|
||||
@@ -40,7 +40,7 @@ habluetooth==5.8.0
|
||||
hass-nabucasa==1.15.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260226.0
|
||||
home-assistant-frontend==20260302.0
|
||||
home-assistant-intents==2026.2.13
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -283,7 +283,7 @@ def color_xy_brightness_to_RGB(
|
||||
Y = brightness
|
||||
|
||||
if vY == 0.0:
|
||||
vY += 0.00000000001
|
||||
vY += 1e-11
|
||||
|
||||
X = (Y / vY) * vX
|
||||
Z = (Y / vY) * (1 - vX - vY)
|
||||
|
||||
@@ -477,7 +477,7 @@ class MassVolumeConcentrationConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "concentration"
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1000000.0, # 1000 µg/m³ = 1 mg/m³
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1_000_000.0, # 1000 µg/m³ = 1 mg/m³
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1000.0, # 1000 mg/m³ = 1 g/m³
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: 1.0,
|
||||
}
|
||||
|
||||
8
requirements_all.txt
generated
8
requirements_all.txt
generated
@@ -416,7 +416,7 @@ aioswitcher==6.1.0
|
||||
aiosyncthing==0.7.1
|
||||
|
||||
# homeassistant.components.tankerkoenig
|
||||
aiotankerkoenig==0.4.2
|
||||
aiotankerkoenig==0.5.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
aiotedee==0.2.25
|
||||
@@ -828,7 +828,7 @@ dremel3dpy==2.1.1
|
||||
dropmqttapi==1.0.3
|
||||
|
||||
# homeassistant.components.dsmr
|
||||
dsmr-parser==1.4.3
|
||||
dsmr-parser==1.5.0
|
||||
|
||||
# homeassistant.components.dwd_weather_warnings
|
||||
dwdwfsapi==1.0.7
|
||||
@@ -1223,7 +1223,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260226.0
|
||||
home-assistant-frontend==20260302.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.2.13
|
||||
@@ -1374,7 +1374,7 @@ kiwiki-client==0.1.1
|
||||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2026.2.25.165736
|
||||
knx-frontend==2026.3.2.183756
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
|
||||
8
requirements_test_all.txt
generated
8
requirements_test_all.txt
generated
@@ -401,7 +401,7 @@ aioswitcher==6.1.0
|
||||
aiosyncthing==0.7.1
|
||||
|
||||
# homeassistant.components.tankerkoenig
|
||||
aiotankerkoenig==0.4.2
|
||||
aiotankerkoenig==0.5.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
aiotedee==0.2.25
|
||||
@@ -734,7 +734,7 @@ dremel3dpy==2.1.1
|
||||
dropmqttapi==1.0.3
|
||||
|
||||
# homeassistant.components.dsmr
|
||||
dsmr-parser==1.4.3
|
||||
dsmr-parser==1.5.0
|
||||
|
||||
# homeassistant.components.dwd_weather_warnings
|
||||
dwdwfsapi==1.0.7
|
||||
@@ -1084,7 +1084,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260226.0
|
||||
home-assistant-frontend==20260302.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.2.13
|
||||
@@ -1211,7 +1211,7 @@ kegtron-ble==1.0.2
|
||||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2026.2.25.165736
|
||||
knx-frontend==2026.3.2.183756
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.conversation import (
|
||||
async_get_chat_log,
|
||||
)
|
||||
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
|
||||
from homeassistant.components.conversation.models import ConversationResult
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -173,6 +174,36 @@ async def test_http_api_wrong_data(
|
||||
assert resp.status == HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
||||
async def test_http_processing_intent_with_device_satellite_ids(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test processing intent via HTTP API with both device_id and satellite_id."""
|
||||
client = await hass_client()
|
||||
mock_result = intent.IntentResponse(language=hass.config.language)
|
||||
mock_result.async_set_speech("test")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.http.async_converse",
|
||||
return_value=ConversationResult(response=mock_result),
|
||||
) as mock_converse:
|
||||
resp = await client.post(
|
||||
"/api/conversation/process",
|
||||
json={
|
||||
"text": "test",
|
||||
"device_id": "test-device-id",
|
||||
"satellite_id": "test-satellite-id",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
mock_converse.assert_called_once()
|
||||
call_kwargs = mock_converse.call_args[1]
|
||||
assert call_kwargs["device_id"] == "test-device-id"
|
||||
assert call_kwargs["satellite_id"] == "test-satellite-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
@@ -221,6 +252,38 @@ async def test_ws_api(
|
||||
assert msg["result"]["response"]["data"]["code"] == "no_intent_match"
|
||||
|
||||
|
||||
async def test_ws_api_with_device_satellite_ids(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the Websocket conversation API with both device_id and satellite_id."""
|
||||
client = await hass_ws_client(hass)
|
||||
mock_result = intent.IntentResponse(language=hass.config.language)
|
||||
mock_result.async_set_speech("test")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.http.async_converse",
|
||||
return_value=ConversationResult(response=mock_result),
|
||||
) as mock_converse:
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "conversation/process",
|
||||
"text": "test",
|
||||
"device_id": "test-device-id",
|
||||
"satellite_id": "test-satellite-id",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
mock_converse.assert_called_once()
|
||||
call_kwargs = mock_converse.call_args[1]
|
||||
assert call_kwargs["device_id"] == "test-device-id"
|
||||
assert call_kwargs["satellite_id"] == "test-satellite-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
|
||||
async def test_ws_prepare(
|
||||
hass: HomeAssistant, init_components, hass_ws_client: WebSocketGenerator, agent_id
|
||||
|
||||
@@ -485,13 +485,7 @@ async def test_clear_lock_user_service(
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test clear_lock_user entity service."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# clear_user_credentials: GetUser returns user with no creds
|
||||
{"userStatus": 1, "credentials": None},
|
||||
None, # ClearUser
|
||||
]
|
||||
)
|
||||
matter_client.send_device_command = AsyncMock(return_value=None)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -503,127 +497,9 @@ async def test_clear_lock_user_service(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 2
|
||||
# Verify GetUser was called to check credentials
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=1),
|
||||
)
|
||||
# Verify ClearUser was called
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearUser(userIndex=1),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
async def test_clear_lock_user_credentials_nullvalue(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test clear_lock_user handles NullValue credentials from Matter SDK."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# GetUser returns NullValue for credentials (truthy but not iterable)
|
||||
{"userStatus": 1, "credentials": NullValue},
|
||||
None, # ClearUser
|
||||
]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"clear_lock_user",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_USER_INDEX: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# GetUser + ClearUser (no ClearCredential since NullValue means no credentials)
|
||||
assert matter_client.send_device_command.call_count == 2
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=1),
|
||||
)
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearUser(userIndex=1),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
|
||||
async def test_clear_lock_user_clears_credentials_first(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test clear_lock_user clears credentials before clearing user."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# clear_user_credentials: GetUser returns user with credentials
|
||||
{
|
||||
"userStatus": 1,
|
||||
"credentials": [
|
||||
{"credentialType": 1, "credentialIndex": 1},
|
||||
{"credentialType": 1, "credentialIndex": 2},
|
||||
],
|
||||
},
|
||||
None, # ClearCredential for first
|
||||
None, # ClearCredential for second
|
||||
None, # ClearUser
|
||||
]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"clear_lock_user",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_USER_INDEX: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# GetUser + 2 ClearCredential + ClearUser
|
||||
assert matter_client.send_device_command.call_count == 4
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.GetUser(userIndex=1),
|
||||
)
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=1,
|
||||
credentialIndex=1,
|
||||
),
|
||||
),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
assert matter_client.send_device_command.call_args_list[2] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=1,
|
||||
credentialIndex=2,
|
||||
),
|
||||
),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
assert matter_client.send_device_command.call_args_list[3] == call(
|
||||
# ClearUser handles credential cleanup per the Matter spec
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearUser(userIndex=1),
|
||||
@@ -2169,13 +2045,8 @@ async def test_clear_lock_user_clear_all(
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test clear_lock_user with CLEAR_ALL_INDEX clears all credentials then users."""
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
None, # ClearCredential(None) - clear all credentials
|
||||
None, # ClearUser(0xFFFE) - clear all users
|
||||
]
|
||||
)
|
||||
"""Test clear_lock_user with CLEAR_ALL_INDEX clears all users."""
|
||||
matter_client.send_device_command = AsyncMock(return_value=None)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -2187,16 +2058,9 @@ async def test_clear_lock_user_clear_all(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 2
|
||||
# First: ClearCredential with None (clear all)
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(credential=None),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
# Second: ClearUser with CLEAR_ALL_INDEX
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
# ClearUser handles credential cleanup per the Matter spec
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearUser(userIndex=CLEAR_ALL_INDEX),
|
||||
@@ -2702,69 +2566,3 @@ async def test_set_lock_user_update_with_explicit_type_and_rule(
|
||||
),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
|
||||
|
||||
# --- clear_lock_user with mixed credential types ---
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN_RFID}])
|
||||
async def test_clear_lock_user_mixed_credential_types(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test clear_lock_user clears mixed PIN and RFID credentials."""
|
||||
pin_type = clusters.DoorLock.Enums.CredentialTypeEnum.kPin
|
||||
rfid_type = clusters.DoorLock.Enums.CredentialTypeEnum.kRfid
|
||||
matter_client.send_device_command = AsyncMock(
|
||||
side_effect=[
|
||||
# GetUser returns user with PIN and RFID credentials
|
||||
{
|
||||
"userStatus": 1,
|
||||
"credentials": [
|
||||
{"credentialType": pin_type, "credentialIndex": 1},
|
||||
{"credentialType": rfid_type, "credentialIndex": 2},
|
||||
],
|
||||
},
|
||||
None, # ClearCredential for PIN
|
||||
None, # ClearCredential for RFID
|
||||
None, # ClearUser
|
||||
]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"clear_lock_user",
|
||||
{
|
||||
ATTR_ENTITY_ID: "lock.mock_door_lock",
|
||||
ATTR_USER_INDEX: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 4
|
||||
# Verify PIN credential was cleared
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=pin_type,
|
||||
credentialIndex=1,
|
||||
),
|
||||
),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
# Verify RFID credential was cleared
|
||||
assert matter_client.send_device_command.call_args_list[2] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.DoorLock.Commands.ClearCredential(
|
||||
credential=clusters.DoorLock.Structs.CredentialStruct(
|
||||
credentialType=rfid_type,
|
||||
credentialIndex=2,
|
||||
),
|
||||
),
|
||||
timed_request_timeout_ms=10000,
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.components.vacuum import (
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
|
||||
from . import (
|
||||
@@ -276,6 +277,41 @@ async def test_clean_area_service(
|
||||
assert mock_vacuum.clean_segments_calls[0][0] == targeted_segments
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("config_flow_fixture")
|
||||
async def test_clean_area_not_configured(hass: HomeAssistant) -> None:
|
||||
"""Test clean_area raises when area mapping is not configured."""
|
||||
mock_vacuum = MockVacuumWithCleanArea(name="Testing", entity_id="vacuum.testing")
|
||||
|
||||
config_entry = MockConfigEntry(domain="test")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
"test",
|
||||
async_setup_entry=help_async_setup_entry_init,
|
||||
async_unload_entry=help_async_unload_entry,
|
||||
),
|
||||
)
|
||||
setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAN_AREA,
|
||||
{"entity_id": mock_vacuum.entity_id, "cleaning_area_id": ["area_1"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == "area_mapping_not_configured"
|
||||
assert exc_info.value.translation_placeholders == {
|
||||
"entity_id": mock_vacuum.entity_id
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("config_flow_fixture")
|
||||
@pytest.mark.parametrize(
|
||||
("area_mapping", "targeted_areas"),
|
||||
@@ -308,13 +344,6 @@ async def test_clean_area_no_segments(
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAN_AREA,
|
||||
{"entity_id": mock_vacuum.entity_id, "cleaning_area_id": targeted_areas},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_vacuum.entity_id,
|
||||
DOMAIN,
|
||||
|
||||
Reference in New Issue
Block a user