Compare commits

...

30 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
70668bf6f0 Add device_type restriction to SetpointChangeSource and SetpointChangeSourceTimestamp sensors
Co-authored-by: lboue <938089+lboue@users.noreply.github.com>
2025-11-30 20:41:33 +00:00
copilot-swe-agent[bot]
4b8202b828 Initial plan 2025-11-30 20:39:31 +00:00
Ludovic BOUÉ
0af8c8fd8c Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-30 21:37:01 +01:00
Ludovic BOUÉ
b9d6c3b9fe Update homeassistant/components/matter/strings.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-30 21:36:08 +01:00
Ludovic BOUÉ
b700940bb9 Merge branch 'dev' into setpoint_change_source 2025-11-30 21:31:19 +01:00
Ludovic BOUÉ
3b73f6d37e Update thermostat setpoint change timestamp to January 1, 2025 2025-11-30 20:21:45 +00:00
Ludovic BOUÉ
2812bb21da Add offset for Matter 2000 epoch in timestamp conversion 2025-11-30 20:20:13 +00:00
Ludovic BOUÉ
5d474675e8 Update mock thermostat state timestamp to January 1, 2025 2025-11-30 20:15:31 +00:00
Ludovic BOUÉ
ea7bcf6cda Update mock thermostat JSON to correct timestamp for attribute 1/513/50 2025-11-30 20:11:19 +00:00
Raphael Hehl
43ba10eebd Add missing translations for UniFi Protect integration (#157570) 2025-11-30 17:05:05 +01:00
Sanjay Govind
64bed19805 Bump bosch-alarm-mode2 to v0.4.10 (#157564) 2025-11-30 16:02:43 +01:00
Shay Levy
6357067f0f Rename Shelly SENSORS to BLOCK_SENSORS to match naming in other platforms (#157553) 2025-11-30 12:48:35 +02:00
Thomas55555
e328ba4045 Bump google air quality api to 1.1.3 (#157555) 2025-11-30 07:17:36 +01:00
Allen Porter
332dbddce6 Bump google-nest-sdm to 9.1.1 (#157562) 2025-11-29 23:19:44 -05:00
J. Nick Koston
82d935a819 Bump aioesphomeapi to 42.9.0 (#157558) 2025-11-29 18:04:55 -06:00
Raphael Hehl
4b84998c0c Fix UFPConfigEntry type consistency in unifiprotect (#157548) 2025-11-29 17:07:44 -06:00
Ludovic BOUÉ
725bd3d671 Add mock thermostat entity and state snapshots for temperature display mode 2025-11-21 12:38:04 +00:00
Ludovic BOUÉ
cfc4fa6342 Merge branch 'dev' into setpoint_change_source 2025-11-21 13:35:31 +01:00
Ludovic BOUÉ
b650e71660 Update mock thermostat snapshots with new attributes and state values 2025-11-18 18:05:29 +00:00
Ludovic BOUÉ
9ddf15e348 Update mock thermostat JSON with additional attributes and values 2025-11-18 18:03:46 +00:00
Ludovic BOUÉ
15082f9111 Merge branch 'dev' into setpoint_change_source 2025-11-18 16:45:05 +01:00
Ludovic BOUÉ
12f16611ff Rename mock thermostat entity IDs and friendly names in snapshots for consistency 2025-11-18 15:30:39 +00:00
Ludovic BOUÉ
8041be3d08 Merge branch 'dev' into setpoint_change_source 2025-11-18 14:08:38 +01:00
Ludovic BOUÉ
40b021e755 Add tests for Thermostat SetpointChangeSource, Timestamp, and Amount sensors 2025-11-18 13:02:44 +00:00
Ludovic BOUÉ
aab57eda96 Update mock thermostat product name to "Mock Thermostat" 2025-11-18 13:00:26 +00:00
Ludovic BOUÉ
f0dd37caa5 Add mock thermostat sensors and states for testing 2025-11-18 12:49:32 +00:00
Ludovic BOUÉ
662b178495 Remove unused attribute from thermostat fixture 2025-11-18 12:48:56 +00:00
Ludovic BOUÉ
cb3d30884a Add mock thermostat fixture for integration tests 2025-11-18 12:48:21 +00:00
Ludovic BOUÉ
49e6f20372 Add Setpoint Change Source timestamp and amount sensors with localization strings 2025-11-18 12:39:28 +00:00
Ludovic BOUÉ
75d02661eb Add Setpoint Change Source sensor and localization strings 2025-11-14 17:19:28 +00:00
32 changed files with 1380 additions and 70 deletions

View File

@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
model=panel.model,
model=panel.model.name,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -83,7 +83,7 @@ async def try_connect(
finally:
await panel.disconnect()
return (panel.model, panel.serial_number)
return (panel.model.name, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -20,7 +20,8 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model,
"model": entry.runtime_data.model.name,
"family": entry.runtime_data.model.family.name,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,

View File

@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
)

View File

@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["bosch-alarm-mode2==0.4.6"]
"requirements": ["bosch-alarm-mode2==0.4.10"]
}

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==42.8.0",
"aioesphomeapi==42.9.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==1.1.2"]
"requirements": ["google_air_quality_api==1.1.3"]
}

View File

@@ -183,6 +183,16 @@ PUMP_CONTROL_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
}
SETPOINT_CHANGE_SOURCE_MAP = {
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kManual: "manual",
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kSchedule: "schedule",
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kExternal: "external",
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kUnknownEnumValue: None,
}
MATTER_2000_TO_UNIX_EPOCH_OFFSET = (
946684800 # Seconds from Matter 2000 epoch to Unix epoch
)
HUMIDITY_SCALING_FACTOR = 100
TEMPERATURE_SCALING_FACTOR = 100
@@ -1488,4 +1498,54 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterSensor,
required_attributes=(clusters.ServiceArea.Attributes.EstimatedEndTime,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SetpointChangeSource",
translation_key="setpoint_change_source",
device_class=SensorDeviceClass.ENUM,
state_class=None,
options=[x for x in SETPOINT_CHANGE_SOURCE_MAP.values() if x is not None],
device_to_ha=lambda x: SETPOINT_CHANGE_SOURCE_MAP[x],
),
entity_class=MatterSensor,
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeSource,),
device_type=(device_types.Thermostat,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SetpointChangeSourceTimestamp",
translation_key="setpoint_change_timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=None,
device_to_ha=(
lambda x: (
dt_util.utc_from_timestamp(x + MATTER_2000_TO_UNIX_EPOCH_OFFSET)
if x > 0
else None
)
),
),
entity_class=MatterSensor,
required_attributes=(
clusters.Thermostat.Attributes.SetpointChangeSourceTimestamp,
),
device_type=(device_types.Thermostat,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ThermostatSetpointChangeAmount",
translation_key="setpoint_change_amount",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=1,
device_class=SensorDeviceClass.TEMPERATURE,
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeAmount,),
device_type=(device_types.Thermostat,),
),
]

View File

@@ -528,6 +528,20 @@
"rms_voltage": {
"name": "Effective voltage"
},
"setpoint_change_amount": {
"name": "Last change amount"
},
"setpoint_change_source": {
"name": "Last change source",
"state": {
"external": "External",
"manual": "Manual",
"schedule": "Schedule"
}
},
"setpoint_change_timestamp": {
"name": "Last change"
},
"switch_current_position": {
"name": "Current switch position"
},

View File

@@ -19,5 +19,5 @@
"documentation": "https://www.home-assistant.io/integrations/nest",
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"requirements": ["google-nest-sdm==9.1.0"]
"requirements": ["google-nest-sdm==9.1.1"]
}

View File

@@ -127,7 +127,7 @@ class RpcBluTrvBinarySensor(RpcBinarySensor):
)
SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
BLOCK_SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
("device", "overtemp"): BlockBinarySensorDescription(
key="device|overtemp",
translation_key="overheating",
@@ -376,7 +376,7 @@ def _async_setup_block_entry(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSleepingBinarySensor,
)
else:
@@ -384,7 +384,7 @@ def _async_setup_block_entry(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockBinarySensor,
)
async_setup_entry_rest(

View File

@@ -198,7 +198,7 @@ class RpcBluTrvSensor(RpcSensor):
)
SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("device", "battery"): BlockSensorDescription(
key="device|battery",
native_unit_of_measurement=PERCENTAGE,
@@ -1740,7 +1740,7 @@ def _async_setup_block_entry(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSleepingSensor,
)
else:
@@ -1748,7 +1748,7 @@ def _async_setup_block_entry(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSensor,
)
async_setup_entry_rest(

View File

@@ -15,7 +15,7 @@ from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized
# diagnostics module will not be imported in the executor.
from uiprotect.test_util.anonymize import anonymize_data # noqa: F401
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
@@ -208,7 +208,7 @@ async def async_remove_config_entry_device(
return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_migrate_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating configuration from version %s", entry.version)

View File

@@ -16,7 +16,6 @@ import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_IGNORE,
ConfigEntry,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
@@ -55,7 +54,7 @@ from .const import (
MIN_REQUIRED_PROTECT_V,
OUTDATED_LOG_MESSAGE,
)
from .data import async_last_update_was_successful
from .data import UFPConfigEntry, async_last_update_was_successful
from .discovery import async_start_discovery
from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
@@ -80,7 +79,7 @@ def _host_is_direct_connect(host: str) -> bool:
async def _async_console_is_offline(
hass: HomeAssistant,
entry: ConfigEntry,
entry: UFPConfigEntry,
) -> bool:
"""Check if a console is offline.
@@ -224,7 +223,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: UFPConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
return OptionsFlowHandler()

View File

@@ -23,6 +23,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .data import ProtectDeviceType, UFPConfigEntry
from .entity import ProtectDeviceEntity
@@ -123,7 +124,10 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
media_id = async_process_play_media_url(self.hass, play_item.url)
if media_type != MediaType.MUSIC:
raise HomeAssistantError("Only music media type is supported")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="only_music_supported",
)
_LOGGER.debug(
"Playing Media %s for %s Speaker", media_id, self.device.display_name
@@ -132,7 +136,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
try:
await self.device.play_audio(media_id, blocking=False)
except StreamError as err:
raise HomeAssistantError(err) from err
_LOGGER.debug("Error playing audio: %s", err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="stream_error",
) from err
# update state after starting player
self._async_updated_event(self.device)

View File

@@ -10,7 +10,6 @@ import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
@@ -165,7 +164,7 @@ class RTSPRepair(ProtectRepair):
@callback
def _async_get_or_create_api_client(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: UFPConfigEntry
) -> ProtectApiClient:
"""Get or create an API client."""
if data := async_get_data_for_entry_id(hass, entry.entry_id):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import logging
from typing import Any, cast
from pydantic import ValidationError
@@ -45,6 +46,8 @@ from .const import (
)
from .data import async_ufp_instance_for_config_entry_ids
_LOGGER = logging.getLogger(__name__)
SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
SERVICE_SET_PRIVACY_ZONE = "set_privacy_zone"
@@ -92,7 +95,11 @@ GET_USER_KEYRING_INFO_SCHEMA = vol.Schema(
def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient:
device_registry = dr.async_get(hass)
if not (device_entry := device_registry.async_get(device_id)):
raise HomeAssistantError(f"No device found for device id: {device_id}")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device_id": device_id},
)
if device_entry.via_device_id is not None:
return _async_get_ufp_instance(hass, device_entry.via_device_id)
@@ -101,7 +108,11 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl
if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids):
return ufp_instance
raise HomeAssistantError(f"No device found for device id: {device_id}")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device_id": device_id},
)
@callback
@@ -141,7 +152,11 @@ async def _async_service_call_nvr(
*(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for i in instances)
)
except (ClientError, ValidationError) as err:
raise HomeAssistantError(str(err)) from err
_LOGGER.debug("Error calling UniFi Protect service: %s", err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_error",
) from err
async def add_doorbell_text(call: ServiceCall) -> None:
@@ -170,7 +185,12 @@ async def remove_privacy_zone(call: ServiceCall) -> None:
if remove_index is None:
raise ServiceValidationError(
f"Could not find privacy zone with name {name} on camera {camera.display_name}."
translation_domain=DOMAIN,
translation_key="privacy_zone_not_found",
translation_placeholders={
"zone_name": name,
"camera_name": camera.display_name,
},
)
def remove_zone() -> None:
@@ -230,7 +250,10 @@ async def get_user_keyring_info(call: ServiceCall) -> ServiceResponse:
camera = _async_get_ufp_camera(call)
ulp_users = camera.api.bootstrap.ulp_users.as_list()
if not ulp_users:
raise HomeAssistantError("No users found, please check Protect permissions.")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="no_users_found",
)
user_keyrings: list[JsonValueType] = [
{

View File

@@ -20,7 +20,9 @@
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"api_key": "API key for your local user account."
"api_key": "[%key:component::unifiprotect::config::step::user::data_description::api_key%]",
"password": "[%key:component::unifiprotect::config::step::user::data_description::password%]",
"username": "[%key:component::unifiprotect::config::step::user::data_description::username%]"
},
"description": "Do you want to set up {name} ({ip_address})? You will need a local user created in your UniFi OS Console to log in with. Ubiquiti Cloud users will not work. For more information: {local_user_documentation_url}",
"title": "UniFi Protect discovered"
@@ -34,8 +36,11 @@
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"api_key": "API key for your local user account.",
"username": "Username for your local (not cloud) user account."
"api_key": "[%key:component::unifiprotect::config::step::user::data_description::api_key%]",
"host": "[%key:component::unifiprotect::config::step::user::data_description::host%]",
"password": "[%key:component::unifiprotect::config::step::user::data_description::password%]",
"port": "[%key:component::unifiprotect::config::step::user::data_description::port%]",
"username": "[%key:component::unifiprotect::config::step::user::data_description::username%]"
},
"description": "Your credentials or API key seem to be missing or invalid. For instructions on how to create a local user or generate a new API key, please refer to the documentation: {local_user_documentation_url}",
"title": "UniFi Protect reauth"
@@ -51,7 +56,11 @@
},
"data_description": {
"api_key": "API key for your local user account.",
"host": "Hostname or IP address of your UniFi Protect device."
"host": "Hostname or IP address of your UniFi Protect device.",
"password": "Password for your local user account.",
"port": "Port of your UniFi Protect device.",
"username": "Username for your local (not cloud) user account.",
"verify_ssl": "Verify SSL certificate of the UniFi Protect device."
},
"description": "You will need a local user created in your UniFi OS Console to log in with. Ubiquiti Cloud users will not work. For more information: {local_user_documentation_url}",
"title": "UniFi Protect setup"
@@ -567,8 +576,26 @@
"api_key_required": {
"message": "API key is required. Please reauthenticate this integration to provide an API key."
},
"device_not_found": {
"message": "No device found for device id: {device_id}"
},
"no_users_found": {
"message": "No users found, please check Protect permissions"
},
"only_music_supported": {
"message": "Only music media type is supported"
},
"privacy_zone_not_found": {
"message": "Could not find privacy zone with name {zone_name} on camera {camera_name}"
},
"protect_version": {
"message": "Your UniFi Protect version ({current_version}) is too old. Minimum required: {min_version}."
"message": "Your UniFi Protect version ({current_version}) is too old. Minimum required: {min_version}"
},
"service_error": {
"message": "Error calling UniFi Protect service, check the logs for more details"
},
"stream_error": {
"message": "Error playing audio, check the logs for more details"
}
},
"issues": {
@@ -627,6 +654,12 @@
"max_media": "Max number of event to load for Media Browser (increases RAM usage)",
"override_connection_host": "Override connection host"
},
"data_description": {
"all_updates": "Enable realtime metrics updates. Only use if you have enabled diagnostic sensors and want them updated in realtime.",
"disable_rtsp": "Disable the RTSP stream for all cameras. Use this if you don't need live video feeds.",
"max_media": "Maximum number of events to load in the Media Browser. Higher values use more RAM.",
"override_connection_host": "Override the connection host for the UniFi Protect device."
},
"description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.",
"title": "UniFi Protect options"
}

8
requirements_all.txt generated
View File

@@ -252,7 +252,7 @@ aioelectricitymaps==1.1.1
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==42.8.0
aioesphomeapi==42.9.0
# homeassistant.components.matrix
# homeassistant.components.slack
@@ -675,7 +675,7 @@ bluetooth-data-tools==1.28.4
bond-async==0.2.1
# homeassistant.components.bosch_alarm
bosch-alarm-mode2==0.4.6
bosch-alarm-mode2==0.4.10
# homeassistant.components.bosch_shc
boschshcpy==0.2.107
@@ -1087,13 +1087,13 @@ google-genai==1.38.0
google-maps-routing==0.6.15
# homeassistant.components.nest
google-nest-sdm==9.1.0
google-nest-sdm==9.1.1
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==1.1.2
google_air_quality_api==1.1.3
# homeassistant.components.slide
# homeassistant.components.slide_local

View File

@@ -243,7 +243,7 @@ aioelectricitymaps==1.1.1
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==42.8.0
aioesphomeapi==42.9.0
# homeassistant.components.matrix
# homeassistant.components.slack
@@ -609,7 +609,7 @@ bluetooth-data-tools==1.28.4
bond-async==0.2.1
# homeassistant.components.bosch_alarm
bosch-alarm-mode2==0.4.6
bosch-alarm-mode2==0.4.10
# homeassistant.components.bosch_shc
boschshcpy==0.2.107
@@ -963,13 +963,13 @@ google-genai==1.38.0
google-maps-routing==0.6.15
# homeassistant.components.nest
google-nest-sdm==9.1.0
google-nest-sdm==9.1.1
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==1.1.2
google_air_quality_api==1.1.3
# homeassistant.components.slide
# homeassistant.components.slide_local

View File

@@ -4,6 +4,7 @@ from collections.abc import Generator
from typing import Any
from unittest.mock import AsyncMock, patch
from bosch_alarm_mode2.const import PANEL_FAMILY, PanelModel
from bosch_alarm_mode2.panel import Area, Door, Output, Point
from bosch_alarm_mode2.utils import Observable
import pytest
@@ -39,10 +40,10 @@ def model(request: pytest.FixtureRequest) -> Generator[str]:
@pytest.fixture
def extra_config_entry_data(
model: str, model_name: str, config_flow_data: dict[str, Any]
model: str, panel_model: PanelModel, config_flow_data: dict[str, Any]
) -> dict[str, Any]:
"""Return extra config entry data."""
return {CONF_MODEL: model_name} | config_flow_data
return {CONF_MODEL: panel_model.name} | config_flow_data
@pytest.fixture(params=[None])
@@ -64,12 +65,12 @@ def config_flow_data(model: str) -> dict[str, Any]:
@pytest.fixture
def model_name(model: str) -> str | None:
def panel_model(model: str) -> PanelModel | None:
"""Return extra config entry data."""
return {
"solution_3000": "Solution 3000",
"amax_3000": "AMAX 3000",
"b5512": "B5512 (US1B)",
"solution_3000": PanelModel("Solution 3000", PANEL_FAMILY.SOLUTION),
"amax_3000": PanelModel("AMAX 3000", PANEL_FAMILY.AMAX),
"b5512": PanelModel("B5512 (US1B)", PANEL_FAMILY.BG_SERIES),
}.get(model)
@@ -166,7 +167,7 @@ def mock_panel(
door: AsyncMock,
output: AsyncMock,
points: dict[int, AsyncMock],
model_name: str,
panel_model: str,
serial_number: str | None,
) -> Generator[AsyncMock]:
"""Define a fixture to set up Bosch Alarm."""
@@ -181,7 +182,7 @@ def mock_panel(
client.doors = {1: door}
client.outputs = {1: output}
client.points = points
client.model = model_name
client.model = panel_model
client.faults = []
client.events = []
client.panel_faults_ids = []

View File

@@ -28,6 +28,7 @@
'open': False,
}),
]),
'family': 'AMAX',
'firmware_version': '1.0.0',
'history_events': list([
]),
@@ -124,6 +125,7 @@
'open': False,
}),
]),
'family': 'BG_SERIES',
'firmware_version': '1.0.0',
'history_events': list([
]),
@@ -219,6 +221,7 @@
'open': False,
}),
]),
'family': 'SOLUTION',
'firmware_version': '1.0.0',
'history_events': list([
]),

View File

@@ -4,6 +4,7 @@ import asyncio
from typing import Any
from unittest.mock import AsyncMock
from bosch_alarm_mode2.const import PANEL_FAMILY, PanelModel
import pytest
from homeassistant.components.bosch_alarm.const import DOMAIN
@@ -22,7 +23,7 @@ async def test_form_user(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -45,13 +46,13 @@ async def test_form_user(
config_flow_data,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Bosch {model_name}"
assert result["title"] == f"Bosch {panel_model.name}"
assert (
result["data"]
== {
CONF_HOST: "1.1.1.1",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
}
| config_flow_data
)
@@ -211,7 +212,7 @@ async def test_dhcp_can_finish(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -237,12 +238,12 @@ async def test_dhcp_can_finish(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Bosch {model_name}"
assert result["title"] == f"Bosch {panel_model.name}"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_MAC: "34:ea:34:b4:3b:5a",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
**config_flow_data,
}
@@ -258,7 +259,7 @@ async def test_dhcp_exceptions(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
exception: Exception,
@@ -316,7 +317,7 @@ async def test_dhcp_discovery_if_panel_setup_config_flow(
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
serial_number: str,
model_name: str,
panel_model: PanelModel,
config_flow_data: dict[str, Any],
) -> None:
"""Test DHCP discovery doesn't fail if a different panel was set up via config flow."""
@@ -346,12 +347,12 @@ async def test_dhcp_discovery_if_panel_setup_config_flow(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Bosch {model_name}"
assert result["title"] == f"Bosch {panel_model.name}"
assert result["data"] == {
CONF_HOST: "4.5.6.7",
CONF_MAC: "34:ea:34:b4:3b:5a",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
**config_flow_data,
}
assert mock_config_entry.unique_id == serial_number
@@ -395,7 +396,7 @@ async def test_dhcp_updates_mac(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -424,7 +425,7 @@ async def test_reauth_flow_success(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -459,7 +460,7 @@ async def test_reauth_flow_error(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
exception: Exception,
@@ -494,7 +495,7 @@ async def test_reconfig_flow(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -529,7 +530,7 @@ async def test_reconfig_flow(
assert mock_config_entry.data == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
**config_flow_data,
}
@@ -540,7 +541,7 @@ async def test_reconfig_flow_incorrect_model(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -556,7 +557,7 @@ async def test_reconfig_flow_incorrect_model(
},
)
mock_panel.model = "Solution 3000"
mock_panel.model = PanelModel("Solution 3000", family=PANEL_FAMILY.SOLUTION)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"

View File

@@ -3,6 +3,7 @@
from typing import Any
from unittest.mock import AsyncMock
from bosch_alarm_mode2.const import PanelModel
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
@@ -19,7 +20,7 @@ async def test_diagnostics(
hass_client: ClientSessionGenerator,
mock_panel: AsyncMock,
area: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,

View File

@@ -113,6 +113,7 @@ async def integration_fixture(
"light_sensor",
"microwave_oven",
"mock_lock",
"mock_thermostat",
"mounted_dimmable_load_control_fixture",
"multi_endpoint_light",
"occupancy_sensor",

View File

@@ -0,0 +1,526 @@
{
"node_id": 150,
"date_commissioned": "2025-11-18T06:53:08.679289",
"last_interview": "2025-11-18T06:53:08.679325",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/49/0": 1,
"0/49/1": [
{
"0": "ZW5zMzM=",
"1": true
}
],
"0/49/4": true,
"0/49/5": 0,
"0/49/6": "ZW5zMzM=",
"0/49/7": null,
"0/49/65532": 4,
"0/49/65533": 2,
"0/49/65528": [],
"0/49/65529": [],
"0/49/65531": [0, 1, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531],
"0/65/0": [],
"0/65/65532": 0,
"0/65/65533": 1,
"0/65/65528": [],
"0/65/65529": [],
"0/65/65531": [0, 65532, 65533, 65528, 65529, 65531],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 4,
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 2,
"0/63/65528": [5, 2],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/62/0": [
{
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRlhgkBwEkCAEwCUEE2p7AKvoklmZUFHB0JFUiCsv5FCm0dmeH35yXz4UUH4HAWUwpbeU+R7hMGbAITM3T1R/mVWYthssdVcPNsfIVcjcKNQEoARgkAgE2AwQCBAEYMAQUQbZ3toX8hpE/FmJz7M6xHTbh6RMwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0DughBITJJHW/pS7o0J6o6FYTe1ufe0vCpaCj3qYeWb/QxLUydUaJQbce5Z3lUcFeHybUa/M9HID+0PRp2Ker3/GA==",
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY",
"254": 1
}
],
"0/62/1": [
{
"1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=",
"2": 4939,
"3": 2,
"4": 150,
"5": "ha",
"254": 1
}
],
"0/62/2": 16,
"0/62/3": 1,
"0/62/4": [
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEGZf4rAtHkcaVU1u+UL507wm/18+2EeJN8alTOLYkkANflrPobEEAWfTZAofAtxkfKH6WH19p/qt/fz+c9gXv8zcKNQEpARgkAmAwBBT0+qfdyShnG+4Pq01pwOnrxdhHRjAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQPVrsFnfFplsQGV5m5EUua+rmo9hAr+OP1bvaifdLqiEIn3uXLTLoKmVUkPImRL2Fb+xcMEAqR2p7RM6ZlFCR20Y"
],
"0/62/5": 1,
"0/62/65532": 0,
"0/62/65533": 2,
"0/62/65528": [1, 3, 5, 8, 14],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11, 12, 13],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65532, 65533, 65528, 65529, 65531],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 2],
"0/60/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531],
"0/55/2": 425,
"0/55/3": 61,
"0/55/4": 0,
"0/55/5": 0,
"0/55/6": 0,
"0/55/7": null,
"0/55/1": true,
"0/55/0": 2,
"0/55/8": 16,
"0/55/65532": 3,
"0/55/65533": 1,
"0/55/65528": [],
"0/55/65529": [0],
"0/55/65531": [
2, 3, 4, 5, 6, 7, 1, 0, 8, 65532, 65533, 65528, 65529, 65531
],
"0/54/0": null,
"0/54/1": null,
"0/54/2": 3,
"0/54/3": null,
"0/54/4": null,
"0/54/5": null,
"0/54/12": null,
"0/54/6": null,
"0/54/7": null,
"0/54/8": null,
"0/54/9": null,
"0/54/10": null,
"0/54/11": null,
"0/54/65532": 3,
"0/54/65533": 1,
"0/54/65528": [],
"0/54/65529": [0],
"0/54/65531": [
0, 1, 2, 3, 4, 5, 12, 6, 7, 8, 9, 10, 11, 65532, 65533, 65528, 65529,
65531
],
"0/52/0": [
{
"0": 6163,
"1": "6163"
},
{
"0": 6162,
"1": "6162"
},
{
"0": 6161,
"1": "6161"
},
{
"0": 6160,
"1": "6160"
},
{
"0": 6159,
"1": "6159"
}
],
"0/52/1": 545392,
"0/52/2": 650640,
"0/52/3": 650640,
"0/52/65532": 1,
"0/52/65533": 1,
"0/52/65528": [],
"0/52/65529": [0],
"0/52/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/51/0": [
{
"0": "docker0",
"1": false,
"2": null,
"3": null,
"4": "8mJ0KirG",
"5": ["rBEAAQ=="],
"6": [],
"7": 0
},
{
"0": "ens33",
"1": true,
"2": null,
"3": null,
"4": "AAwpaqXN",
"5": ["wKgBxA=="],
"6": [
"KgEOCgKzOZAcmuLd4EsaUA==",
"KgEOCgKzOZA2wMm9YG06Ag==",
"/oAAAAAAAACluAo+qvkuxw=="
],
"7": 2
},
{
"0": "lo",
"1": true,
"2": null,
"3": null,
"4": "AAAAAAAA",
"5": ["fwAAAQ=="],
"6": ["AAAAAAAAAAAAAAAAAAAAAQ=="],
"7": 0
}
],
"0/51/1": 1,
"0/51/8": false,
"0/51/3": 0,
"0/51/4": 0,
"0/51/2": 16,
"0/51/65532": 0,
"0/51/65533": 2,
"0/51/65528": [2],
"0/51/65529": [0, 1],
"0/51/65531": [0, 1, 8, 3, 4, 2, 65532, 65533, 65528, 65529, 65531],
"0/50/65532": 0,
"0/50/65533": 1,
"0/50/65528": [1],
"0/50/65529": [0],
"0/50/65531": [65532, 65533, 65528, 65529, 65531],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 2,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 2,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531],
"0/43/0": "en-US",
"0/43/1": ["en-US"],
"0/43/65532": 0,
"0/43/65533": 1,
"0/43/65528": [],
"0/43/65529": [],
"0/43/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
"0/40/0": 19,
"0/40/1": "TEST_VENDOR",
"0/40/2": 65521,
"0/40/3": "Mock Thermostat",
"0/40/4": 32769,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 0,
"0/40/8": "TEST_VERSION",
"0/40/9": 1,
"0/40/10": "1.0",
"0/40/19": {
"0": 3,
"1": 65535
},
"0/40/21": 17104896,
"0/40/22": 1,
"0/40/24": 1,
"0/40/11": "20200101",
"0/40/12": "",
"0/40/13": "",
"0/40/14": "",
"0/40/15": "TEST_SN",
"0/40/16": false,
"0/40/18": "29DB8B9DB518F05F",
"0/40/65532": 0,
"0/40/65533": 5,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 19, 21, 22, 24, 11, 12, 13, 14, 15, 16,
18, 65532, 65533, 65528, 65529, 65531
],
"0/31/0": [
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 1
}
],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65532": 0,
"0/31/65533": 3,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 2, 3, 4, 65532, 65533, 65528, 65529, 65531],
"0/30/0": [],
"0/30/65532": 0,
"0/30/65533": 1,
"0/30/65528": [],
"0/30/65529": [],
"0/30/65531": [0, 65532, 65533, 65528, 65529, 65531],
"0/29/0": [
{
"0": 18,
"1": 1
},
{
"0": 22,
"1": 3
}
],
"0/29/1": [
49, 65, 63, 62, 60, 55, 54, 52, 51, 50, 48, 43, 40, 31, 30, 29, 3, 42, 45,
53
],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 3,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/3/0": 0,
"0/3/1": 2,
"0/3/65532": 0,
"0/3/65533": 6,
"0/3/65528": [],
"0/3/65529": [0, 64],
"0/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
"0/42/0": [],
"0/42/1": true,
"0/42/2": 0,
"0/42/3": 0,
"0/42/65532": 0,
"0/42/65533": 1,
"0/42/65528": [],
"0/42/65529": [0],
"0/42/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/45/0": 1,
"0/45/65532": 1,
"0/45/65533": 2,
"0/45/65528": [],
"0/45/65529": [],
"0/45/65531": [0, 65532, 65533, 65528, 65529, 65531],
"0/53/0": null,
"0/53/1": null,
"0/53/2": null,
"0/53/3": null,
"0/53/4": null,
"0/53/5": null,
"0/53/6": 0,
"0/53/7": [],
"0/53/8": [],
"0/53/9": null,
"0/53/10": null,
"0/53/11": null,
"0/53/12": null,
"0/53/13": null,
"0/53/14": 0,
"0/53/15": 0,
"0/53/16": 0,
"0/53/17": 0,
"0/53/18": 0,
"0/53/19": 0,
"0/53/20": 0,
"0/53/21": 0,
"0/53/22": 0,
"0/53/23": 0,
"0/53/24": 0,
"0/53/25": 0,
"0/53/26": 0,
"0/53/27": 0,
"0/53/28": 0,
"0/53/29": 0,
"0/53/30": 0,
"0/53/31": 0,
"0/53/32": 0,
"0/53/33": 0,
"0/53/34": 0,
"0/53/35": 0,
"0/53/36": 0,
"0/53/37": 0,
"0/53/38": 0,
"0/53/39": 0,
"0/53/40": 0,
"0/53/41": 0,
"0/53/42": 0,
"0/53/43": 0,
"0/53/44": 0,
"0/53/45": 0,
"0/53/46": 0,
"0/53/47": 0,
"0/53/48": 0,
"0/53/49": 0,
"0/53/50": 0,
"0/53/51": 0,
"0/53/52": 0,
"0/53/53": 0,
"0/53/54": 0,
"0/53/55": 0,
"0/53/56": null,
"0/53/57": null,
"0/53/58": null,
"0/53/59": null,
"0/53/60": null,
"0/53/61": null,
"0/53/62": [],
"0/53/65532": 15,
"0/53/65533": 3,
"0/53/65528": [],
"0/53/65529": [0],
"0/53/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 65532, 65533, 65528, 65529, 65531
],
"1/29/0": [
{
"0": 769,
"1": 4
}
],
"1/29/1": [29, 3, 4, 513, 516],
"1/29/2": [3],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 3,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 6,
"1/3/65528": [],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
"1/4/0": 128,
"1/4/65532": 1,
"1/4/65533": 4,
"1/4/65528": [0, 1, 2, 3],
"1/4/65529": [0, 1, 2, 3, 4, 5],
"1/4/65531": [0, 65532, 65533, 65528, 65529, 65531],
"1/513/0": 1800,
"1/513/1": 500,
"1/513/3": 700,
"1/513/4": 3000,
"1/513/5": 1600,
"1/513/6": 3200,
"1/513/7": 0,
"1/513/8": 25,
"1/513/16": 0,
"1/513/17": 2600,
"1/513/18": 2000,
"1/513/21": 700,
"1/513/22": 3000,
"1/513/23": 1600,
"1/513/24": 3200,
"1/513/25": 25,
"1/513/26": 0,
"1/513/27": 4,
"1/513/28": 1,
"1/513/30": 4,
"1/513/35": 0,
"1/513/36": 0,
"1/513/37": 0,
"1/513/41": 1,
"1/513/48": 0,
"1/513/49": 150,
"1/513/50": 789004800,
"1/513/72": [
{
"0": 1,
"1": 1,
"2": 1
},
{
"0": 2,
"1": 1,
"2": 1
},
{
"0": 3,
"1": 1,
"2": 2
},
{
"0": 4,
"1": 1,
"2": 2
},
{
"0": 5,
"1": 1,
"2": 2
},
{
"0": 254,
"1": 1,
"2": 2
}
],
"1/513/73": [
{
"0": 4,
"1": 1,
"2": 2
},
{
"0": 3,
"1": 1,
"2": 2
}
],
"1/513/74": 5,
"1/513/78": null,
"1/513/80": [
{
"0": "AQ==",
"1": 1,
"3": 2500,
"4": 2100,
"5": true
},
{
"0": "Ag==",
"1": 2,
"3": 2600,
"4": 2000,
"5": true
}
],
"1/513/82": 0,
"1/513/83": 5,
"1/513/84": [],
"1/513/85": null,
"1/513/86": null,
"1/513/65532": 419,
"1/513/65533": 9,
"1/513/65528": [2, 253],
"1/513/65529": [0, 6, 7, 8, 254],
"1/513/65531": [
0, 1, 3, 4, 5, 6, 7, 8, 16, 17, 18, 21, 22, 23, 24, 25, 26, 27, 28, 30,
35, 36, 37, 41, 48, 49, 50, 72, 73, 74, 78, 80, 82, 83, 84, 85, 86, 65532,
65533, 65528, 65529, 65531
],
"1/516/0": 0,
"1/516/1": 0,
"1/516/65532": 0,
"1/516/65533": 2,
"1/516/65528": [],
"1/516/65529": [],
"1/516/65531": [0, 1, 65532, 65533, 65528, 65529, 65531]
},
"attribute_subscriptions": []
}

View File

@@ -2290,6 +2290,104 @@
'state': 'unknown',
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_0-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.mock_thermostat_identify_0',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (0)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-0-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_0-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Mock Thermostat Identify (0)',
}),
'context': <ANY>,
'entity_id': 'button.mock_thermostat_identify_0',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.mock_thermostat_identify_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Mock Thermostat Identify (1)',
}),
'context': <ANY>,
'entity_id': 'button.mock_thermostat_identify_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -325,6 +325,77 @@
'state': 'cool',
})
# ---
# name: test_climates[mock_thermostat][climate.mock_thermostat-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 32.0,
'min_temp': 7.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.mock_thermostat',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 387>,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-MatterThermostat-513-0',
'unit_of_measurement': None,
})
# ---
# name: test_climates[mock_thermostat][climate.mock_thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 18.0,
'friendly_name': 'Mock Thermostat',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 32.0,
'min_temp': 7.0,
'supported_features': <ClimateEntityFeature: 387>,
'target_temp_high': 26.0,
'target_temp_low': 20.0,
'temperature': None,
}),
'context': <ANY>,
'entity_id': 'climate.mock_thermostat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat_cool',
})
# ---
# name: test_climates[room_airconditioner][climate.room_airconditioner-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -2570,6 +2570,63 @@
'state': 'silent',
})
# ---
# name: test_selects[mock_thermostat][select.mock_thermostat_temperature_display_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Celsius',
'Fahrenheit',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.mock_thermostat_temperature_display_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Temperature display mode',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_display_mode',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0',
'unit_of_measurement': None,
})
# ---
# name: test_selects[mock_thermostat][select.mock_thermostat_temperature_display_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Temperature display mode',
'options': list([
'Celsius',
'Fahrenheit',
]),
}),
'context': <ANY>,
'entity_id': 'select.mock_thermostat_temperature_display_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Celsius',
})
# ---
# name: test_selects[mounted_dimmable_load_control_fixture][select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -7259,6 +7259,332 @@
'state': 'stopped',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_heating_demand-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_thermostat_heating_demand',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Heating demand',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pi_heating_demand',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatPIHeatingDemand-513-8',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_heating_demand-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Heating demand',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_heating_demand',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '25',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_last_change',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Last change',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'setpoint_change_timestamp',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-SetpointChangeSourceTimestamp-513-50',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Mock Thermostat Last change',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_last_change',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-01-01T00:00:00+00:00',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_amount-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_last_change_amount',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Last change amount',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'setpoint_change_amount',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatSetpointChangeAmount-513-49',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_amount-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Thermostat Last change amount',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_last_change_amount',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_source-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'manual',
'schedule',
'external',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_last_change_source',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Last change source',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'setpoint_change_source',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-SetpointChangeSource-513-48',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_source-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Mock Thermostat Last change source',
'options': list([
'manual',
'schedule',
'external',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_last_change_source',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'manual',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_outdoor_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_outdoor_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Outdoor temperature',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'outdoor_temperature',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatOutdoorTemperature-513-1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_outdoor_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Thermostat Outdoor temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_outdoor_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5.0',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatLocalTemperature-513-0',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Thermostat Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '18.0',
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_current_switch_position_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -697,3 +697,91 @@ async def test_vacuum_operational_error_sensor(
state = hass.states.get("sensor.mock_vacuum_operational_error")
assert state
assert state.state == "unknown"
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
async def test_thermostat_setpoint_change_source(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test Thermostat SetpointChangeSource sensor."""
# Thermostat Cluster / SetpointChangeSource attribute (1/513/48)
state = hass.states.get("sensor.mock_thermostat_last_change_source")
assert state
assert state.state == "manual"
assert state.attributes["options"] == ["manual", "schedule", "external"]
# Test schedule source
set_node_attribute(matter_node, 1, 513, 48, 1)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_thermostat_last_change_source")
assert state
assert state.state == "schedule"
# Test external source
set_node_attribute(matter_node, 1, 513, 48, 2)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_thermostat_last_change_source")
assert state
assert state.state == "external"
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
async def test_thermostat_setpoint_change_timestamp(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test Thermostat SetpointChangeSourceTimestamp sensor."""
# Thermostat Cluster / SetpointChangeSourceTimestamp attribute (1/513/50)
state = hass.states.get("sensor.mock_thermostat_last_change")
assert state
assert state.state == "2025-01-01T00:00:00+00:00"
# Update to a new timestamp (2024-01-01 00:00:00+00:00 UTC)
set_node_attribute(matter_node, 1, 513, 50, 757382400)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_thermostat_last_change")
assert state
assert state.state == "2024-01-01T00:00:00+00:00"
# Test zero value (should be None/unknown)
set_node_attribute(matter_node, 1, 513, 50, 0)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_thermostat_last_change")
assert state
assert state.state == "unknown"
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
async def test_thermostat_setpoint_change_amount(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test Thermostat SetpointChangeAmount sensor."""
# Thermostat Cluster / SetpointChangeAmount attribute (1/513/49)
state = hass.states.get("sensor.mock_thermostat_last_change_amount")
assert state
assert state.state == "1.5"
# Update to 2.0°C (200 in Matter units)
set_node_attribute(matter_node, 1, 513, 49, 200)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_thermostat_last_change_amount")
assert state
assert state.state == "2.0"
# Update to -0.5°C (-50 in Matter units)
set_node_attribute(matter_node, 1, 513, 49, -50)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_thermostat_last_change_amount")
assert state
assert state.state == "-0.5"

View File

@@ -333,7 +333,7 @@ async def test_get_user_keyring_info_no_users(
camera_entry = entity_registry.async_get("binary_sensor.test_camera_doorbell")
with pytest.raises(
HomeAssistantError, match="No users found, please check Protect permissions."
HomeAssistantError, match="No users found, please check Protect permissions"
):
await hass.services.async_call(
DOMAIN,