Compare commits

...

12 Commits

Author SHA1 Message Date
farmio
0f633f6765 Update knx-frontend to 2026.3.2.183756 2026-03-02 20:15:57 +01:00
Bram Kragten
cb016b014b Update frontend to 20260302.0 (#164612) 2026-03-02 18:53:01 +01:00
Michael Hansen
afb4523f63 Add device_id and satellite_id to conversation HTTP/websocket APIs (#164414) 2026-03-02 17:01:51 +01:00
Alex Brown
05ad4986ac Fix Matter clear lock user (#164493) 2026-03-02 16:28:49 +01:00
epenet
42dbd5f98f Migrate moat to runtime_data (#164605) 2026-03-02 16:14:25 +01:00
epenet
f58a514ce7 Migrate monzo to runtime_data (#164603) 2026-03-02 16:14:10 +01:00
Artur Pragacz
8fb384a5e1 Raise on vacuum area mapping not configured (#164595) 2026-03-02 15:36:48 +01:00
Samuel Xiao
c24302b5ce Switchbot Cloud: Fixed Smart Radiator Thermostat off line (#162714)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-02 14:44:34 +01:00
Jan-Philipp Benecke
999ad9b642 Bump aiotankerkoenig to 0.5.1 (#164590) 2026-03-02 14:44:29 +01:00
Pierre Sassoulas
36d6b4dafe Use clearer number notation for very small and very large literals (#164521) 2026-03-02 14:06:19 +01:00
Norbert Rittel
06870a2e25 Replace "the lock" with "a lock" in matter action descriptions (#164585) 2026-03-02 12:56:45 +01:00
willemstuursma
85eba2bb15 Bump DSMR parser to 1.5.0 (#164484) 2026-03-02 12:52:37 +01:00
29 changed files with 229 additions and 359 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,17 @@
"name": "Previous"
}
},
"climate": {
"smart_radiator_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"schedule": "Schedule"
}
}
}
}
},
"fan": {
"air_purifier": {
"state_attributes": {

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiotankerkoenig"],
"quality_scale": "platinum",
"requirements": ["aiotankerkoenig==0.4.2"]
"requirements": ["aiotankerkoenig==0.5.1"]
}

View File

@@ -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] = {}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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