Compare commits

...

41 Commits

Author SHA1 Message Date
Ludovic BOUÉ
58f533feb6 Add device_type attribute for Thermostat sensors 2025-11-30 21:43:43 +01: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
Raphael Hehl
e10c1ebcf6 Fix UniFi Protect RTSP repair warnings when globally disabled (#157516) 2025-11-29 22:53:34 +02:00
Raphael Hehl
0174bad182 Add PARALLEL_UPDATES to UniFi Protect platforms (#157504) 2025-11-29 19:48:43 +01:00
Allen Porter
d5be623684 Bump python-roborock to 3.8.4 (#157538) 2025-11-29 20:34:27 +02:00
Raphael Hehl
d006b044c8 Bump uiprotect to 7.31.0 (#157543) 2025-11-29 20:33:09 +02:00
Jan Bouwhuis
fdd9571623 Fix MQTT entity cannot be renamed (#157540) 2025-11-29 19:29:54 +01:00
Shay Levy
4f4c5152b9 Refactor Shelly setup to use async_setup_entry_block for block entities (#157517) 2025-11-29 18:08:12 +02:00
Denis Shulyaka
b031a082cd Bump anthropic to 0.75.0 (#157491) 2025-11-29 14:35:30 +01:00
Shay Levy
a1132195fd Refactor Shelly RPC event platform to use base class (#157499) 2025-11-29 13:09:32 +02:00
Jordan Harvey
708b3dc8b2 Disable cookie quotes for Anglian Water (#157518) 2025-11-29 11:52:55 +01:00
J. Nick Koston
8ae0216135 Bump ESPHome stable BLE version to 2025.11.0 (#157511) 2025-11-29 03:40:22 -06:00
David Woodhouse
1472281cd5 Clarify percentage_command_topic and percentage_state_topic for MQTT fan (#157460)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-11-29 02:47:34 -06:00
Allen Porter
ceaa71d198 Bump python-roborock to 3.8.3 (#157512) 2025-11-29 09:34:22 +01: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
62 changed files with 1517 additions and 136 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import MSOB2CAuth
from pyanglianwater.exceptions import (
@@ -18,7 +19,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
@@ -33,7 +34,10 @@ async def async_setup_entry(
auth = MSOB2CAuth(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
session=async_create_clientsession(
hass,
cookie_jar=CookieJar(quote_cookie=False),
),
refresh_token=entry.data[CONF_ACCESS_TOKEN],
account_number=entry.data[CONF_ACCOUNT_NUMBER],
)

View File

@@ -421,6 +421,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
)
if short_form.search(model_alias):
model_alias += "-0"
if model_alias.endswith(("haiku", "opus", "sonnet")):
model_alias += "-latest"
model_options.append(
SelectOptionDict(
label=model_info.display_name,

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.73.0"]
"requirements": ["anthropic==0.75.0"]
}

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 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
DEFAULT_PORT: Final = 6053
STABLE_BLE_VERSION_STR = "2025.8.0"
STABLE_BLE_VERSION_STR = "2025.11.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",

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

@@ -157,7 +157,7 @@
"title": "[%key:component::assist_pipeline::issues::assist_in_progress_deprecated::title%]"
},
"ble_firmware_outdated": {
"description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device from ESPHome earlier than 2022.12.0, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme.",
"description": "ESPHome {version} introduces ultra-low latency event processing, reducing BLE event delays from 0-16 milliseconds to approximately 12 microseconds. This resolves stability issues when pairing, connecting, or handshaking with devices that require low latency, and makes Bluetooth proxy operations rival or exceed local adapters. We highly recommend updating {name} to take advantage of these improvements.",
"title": "Update {name} with ESPHome {version} or later"
},
"device_conflict": {

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

@@ -1486,6 +1486,7 @@ class MqttEntity(
entity_registry.async_update_entity(
self.entity_id, new_entity_id=self._update_registry_entity_id
)
self._update_registry_entity_id = None
await super().async_added_to_hass()
self._subscriptions = {}

View File

@@ -729,8 +729,8 @@
"data_description": {
"payload_reset_percentage": "A special payload that resets the fan speed percentage state attribute to unknown when received at the percentage state topic.",
"percentage_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the percentage command topic.",
"percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage. [Learn more.]({url}#percentage_command_topic)",
"percentage_state_topic": "The MQTT topic subscribed to receive fan speed based on percentage. [Learn more.]({url}#percentage_state_topic)",
"percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage setting. The value shall be in the range from \"speed range min\" to \"speed range max\". [Learn more.]({url}#percentage_command_topic)",
"percentage_state_topic": "The MQTT topic subscribed to receive fan speed state. This is a value in the range from \"speed range min\" to \"speed range max\". [Learn more.]({url}#percentage_state_topic)",
"percentage_value_template": "Defines a [template]({value_templating_url}) to extract the speed percentage value.",
"speed_range_max": "The maximum of numeric output range (representing 100 %). The percentage step is 100 / number of speeds within the \"speed range\".",
"speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The percentage step is 100 / the number of speeds within the \"speed range\"."

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

@@ -19,7 +19,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==3.8.1",
"python-roborock==3.8.4",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -30,7 +30,7 @@ from .entity import (
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
ShellySleepingRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rest,
async_setup_entry_rpc,
)
@@ -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",
@@ -372,19 +372,19 @@ def _async_setup_block_entry(
) -> None:
"""Set up entities for BLOCK device."""
if config_entry.data[CONF_SLEEP_PERIOD]:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSleepingBinarySensor,
)
else:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockBinarySensor,
)
async_setup_entry_rest(

View File

@@ -27,7 +27,7 @@ from .entity import (
RpcEntityDescription,
ShellyBlockAttributeEntity,
ShellyRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
rpc_call,
)
@@ -81,7 +81,7 @@ def _async_setup_block_entry(
coordinator = config_entry.runtime_data.block
assert coordinator
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass, config_entry, async_add_entities, BLOCK_COVERS, BlockShellyCover
)

View File

@@ -34,14 +34,14 @@ from .utils import (
@callback
def async_setup_entry_attribute_entities(
def async_setup_entry_block(
hass: HomeAssistant,
config_entry: ShellyConfigEntry,
async_add_entities: AddEntitiesCallback,
sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for attributes."""
"""Set up block entities."""
coordinator = config_entry.runtime_data.block
assert coordinator
if coordinator.device.initialized:
@@ -150,7 +150,7 @@ def async_setup_entry_rpc(
sensors: Mapping[str, RpcEntityDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for RPC sensors."""
"""Set up RPC entities."""
coordinator = config_entry.runtime_data.rpc
assert coordinator

View File

@@ -18,7 +18,6 @@ from homeassistant.components.event import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
BASIC_INPUTS_EVENTS_TYPES,
@@ -26,7 +25,7 @@ from .const import (
SHIX3_1_INPUTS_EVENTS_TYPES,
)
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, get_entity_rpc_device_info
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import (
async_remove_orphaned_entities,
async_remove_shelly_entity,
@@ -136,7 +135,7 @@ def _async_setup_rpc_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up entities for RPC device."""
entities: list[ShellyRpcEvent] = []
entities: list[ShellyRpcEvent | ShellyRpcScriptEvent] = []
coordinator = config_entry.runtime_data.rpc
if TYPE_CHECKING:
@@ -162,7 +161,9 @@ def _async_setup_rpc_entry(
continue
if script_events and (event_types := script_events[get_rpc_key_id(script)]):
entities.append(ShellyRpcScriptEvent(coordinator, script, event_types))
entities.append(
ShellyRpcScriptEvent(coordinator, script, SCRIPT_EVENT, event_types)
)
# If a script is removed, from the device configuration, we need to remove orphaned entities
async_remove_orphaned_entities(
@@ -227,7 +228,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity):
self.async_write_ha_state()
class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
class ShellyRpcEvent(ShellyRpcEntity, EventEntity):
"""Represent RPC event entity."""
_attr_has_entity_name = True
@@ -240,25 +241,19 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
description: ShellyRpcEventDescription,
) -> None:
"""Initialize Shelly entity."""
super().__init__(coordinator)
self._attr_device_info = get_entity_rpc_device_info(coordinator, key)
self._attr_unique_id = f"{coordinator.mac}-{key}"
super().__init__(coordinator, key)
self.entity_description = description
if description.key == "input":
_, component, component_id = get_rpc_key(key)
if custom_name := get_rpc_custom_name(coordinator.device, key):
self._attr_name = custom_name
else:
self._attr_translation_placeholders = {
"input_number": component_id
if get_rpc_number_of_channels(coordinator.device, component) > 1
else ""
}
self.event_id = int(component_id)
elif description.key == "script":
self._attr_name = get_rpc_custom_name(coordinator.device, key)
self.event_id = get_rpc_key_id(key)
_, component, component_id = get_rpc_key(key)
if custom_name := get_rpc_custom_name(coordinator.device, key):
self._attr_name = custom_name
else:
self._attr_translation_placeholders = {
"input_number": component_id
if get_rpc_number_of_channels(coordinator.device, component) > 1
else ""
}
self.event_id = int(component_id)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
@@ -270,30 +265,36 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
@callback
def _async_handle_event(self, event: dict[str, Any]) -> None:
"""Handle the demo button event."""
"""Handle the event."""
if event["id"] == self.event_id:
self._trigger_event(event["event"])
self.async_write_ha_state()
class ShellyRpcScriptEvent(ShellyRpcEvent):
class ShellyRpcScriptEvent(ShellyRpcEntity, EventEntity):
"""Represent RPC script event entity."""
_attr_has_entity_name = True
entity_description: ShellyRpcEventDescription
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
description: ShellyRpcEventDescription,
event_types: list[str],
) -> None:
"""Initialize Shelly script event entity."""
super().__init__(coordinator, key, SCRIPT_EVENT)
self.component = key
super().__init__(coordinator, key)
self.entity_description = description
self._attr_event_types = event_types
self._attr_name = get_rpc_custom_name(coordinator.device, key)
self.event_id = get_rpc_key_id(key)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super(CoordinatorEntity, self).async_added_to_hass()
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_subscribe_events(self._async_handle_event)
@@ -302,7 +303,7 @@ class ShellyRpcScriptEvent(ShellyRpcEvent):
@callback
def _async_handle_event(self, event: dict[str, Any]) -> None:
"""Handle script event."""
if event.get("component") == self.component:
if event.get("component") == self.key:
event_type = event.get("event")
if event_type not in self.event_types:
# This can happen if we didn't find this event type in the script

View File

@@ -44,7 +44,7 @@ from .entity import (
RpcEntityDescription,
ShellyBlockAttributeEntity,
ShellyRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
)
from .utils import (
@@ -101,7 +101,7 @@ def _async_setup_block_entry(
coordinator = config_entry.runtime_data.block
assert coordinator
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass, config_entry, async_add_entities, BLOCK_LIGHTS, BlockShellyLight
)

View File

@@ -42,7 +42,7 @@ from .entity import (
RpcEntityDescription,
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
rpc_call,
)
@@ -353,7 +353,7 @@ def _async_setup_block_entry(
) -> None:
"""Set up entities for BLOCK device."""
if config_entry.data[CONF_SLEEP_PERIOD]:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,

View File

@@ -53,7 +53,7 @@ from .entity import (
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
ShellySleepingRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rest,
async_setup_entry_rpc,
get_entity_rpc_device_info,
@@ -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,
@@ -1736,19 +1736,19 @@ def _async_setup_block_entry(
) -> None:
"""Set up entities for BLOCK device."""
if config_entry.data[CONF_SLEEP_PERIOD]:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSleepingSensor,
)
else:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSensor,
)
async_setup_entry_rest(

View File

@@ -36,7 +36,7 @@ from .entity import (
ShellyBlockAttributeEntity,
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
rpc_call,
)
@@ -337,11 +337,11 @@ def _async_setup_block_entry(
coordinator = config_entry.runtime_data.block
assert coordinator
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass, config_entry, async_add_entities, BLOCK_RELAY_SWITCHES, BlockRelaySwitch
)
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,

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

@@ -39,6 +39,7 @@ from .entity import (
)
_KEY_DOOR = "door"
PARALLEL_UPDATES = 0
@dataclasses.dataclass(frozen=True, kw_only=True)

View File

@@ -33,6 +33,7 @@ from .entity import (
)
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)

View File

@@ -32,6 +32,7 @@ from .entity import ProtectDeviceEntity
from .utils import get_camera_base_name
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@callback
@@ -91,7 +92,11 @@ def _get_camera_channels(
# no RTSP enabled use first channel with no stream
if is_default and not camera.is_third_party_camera:
_create_rtsp_repair(hass, entry, data, camera)
# Only create repair issue if RTSP is not disabled globally
if not data.disable_stream:
_create_rtsp_repair(hass, entry, data, camera)
else:
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")
yield camera, camera.channels[0], True
else:
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")

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

@@ -40,6 +40,8 @@ from .data import (
)
from .entity import EventEntityMixin, ProtectDeviceEntity, ProtectEventMixin
PARALLEL_UPDATES = 0
# Select best thumbnail
# Prefer thumbnails with LPR data, sorted by confidence

View File

@@ -15,6 +15,7 @@ from .data import ProtectDeviceType, UFPConfigEntry
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(

View File

@@ -20,6 +20,7 @@ from .data import ProtectDeviceType, UFPConfigEntry
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(

View File

@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.29.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.31.0", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -23,10 +23,12 @@ 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
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
_SPEAKER_DESCRIPTION = MediaPlayerEntityDescription(
key="speaker",
@@ -122,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
@@ -131,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

@@ -29,6 +29,8 @@ from .entity import (
async_all_device_entities,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ProtectNumberEntityDescription(

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

@@ -45,6 +45,7 @@ from .utils import async_get_light_motion_current
_LOGGER = logging.getLogger(__name__)
_KEY_LIGHT_MOTION = "light_motion"
PARALLEL_UPDATES = 0
HDR_MODES = [
{"id": "always", "name": "Always On"},

View File

@@ -55,6 +55,7 @@ from .utils import async_get_light_motion_current
_LOGGER = logging.getLogger(__name__)
OBJECT_TYPE_NONE = "none"
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)

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

View File

@@ -36,6 +36,7 @@ from .entity import (
ATTR_PREV_MIC = "prev_mic_level"
ATTR_PREV_RECORD = "prev_record_mode"
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)

View File

@@ -27,6 +27,8 @@ from .entity import (
async_all_device_entities,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ProtectTextEntityDescription(ProtectSetableKeysMixin[T], TextEntityDescription):

14
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
@@ -504,7 +504,7 @@ anova-wifi==0.17.0
anthemav==1.4.1
# homeassistant.components.anthropic
anthropic==0.73.0
anthropic==0.75.0
# homeassistant.components.mcp_server
anyio==4.10.0
@@ -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
@@ -2560,7 +2560,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==3.8.1
python-roborock==3.8.4
# homeassistant.components.smarttub
python-smarttub==0.0.45
@@ -3053,7 +3053,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.29.0
uiprotect==7.31.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

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
@@ -480,7 +480,7 @@ anova-wifi==0.17.0
anthemav==1.4.1
# homeassistant.components.anthropic
anthropic==0.73.0
anthropic==0.75.0
# homeassistant.components.mcp_server
anyio==4.10.0
@@ -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
@@ -2138,7 +2138,7 @@ python-pooldose==0.8.0
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==3.8.1
python-roborock==3.8.4
# homeassistant.components.smarttub
python-smarttub==0.0.45
@@ -2538,7 +2538,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.29.0
uiprotect==7.31.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@@ -128,6 +128,12 @@ async def mock_init_component(
"""Initialize integration."""
model_list = AsyncPage(
data=[
ModelInfo(
id="claude-opus-4-5-20251101",
created_at=datetime.datetime(2025, 11, 1, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Opus 4.5",
type="model",
),
ModelInfo(
id="claude-haiku-4-5-20251001",
created_at=datetime.datetime(2025, 10, 15, 0, 0, tzinfo=datetime.UTC),

View File

@@ -357,6 +357,10 @@ async def test_model_list(
assert options["type"] == FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["data_schema"].schema["chat_model"].config["options"] == [
{
"label": "Claude Opus 4.5",
"value": "claude-opus-4-5",
},
{
"label": "Claude Haiku 4.5",
"value": "claude-haiku-4-5",
@@ -379,11 +383,11 @@ async def test_model_list(
},
{
"label": "Claude Sonnet 3.7",
"value": "claude-3-7-sonnet",
"value": "claude-3-7-sonnet-latest",
},
{
"label": "Claude Haiku 3.5",
"value": "claude-3-5-haiku",
"value": "claude-3-5-haiku-latest",
},
{
"label": "Claude Haiku 3",

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

@@ -1584,6 +1584,7 @@ async def test_discovery_with_object_id(
async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
entity_registry: er.EntityRegistry,
) -> None:
"""Test discovering an MQTT entity with default_entity_id and unique_id."""
@@ -1598,6 +1599,7 @@ async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
)
initial_entity_id = "sensor.hello_id"
new_entity_id = "sensor.updated_hello_id"
later_entity_id = "sensor.later_hello_id"
name = "Hello World 11"
domain = "sensor"
@@ -1626,6 +1628,14 @@ async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
assert state.name == name
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered
# Assert the entity ID can be changed later
entity_registry.async_update_entity(new_entity_id, new_entity_id=later_entity_id)
await hass.async_block_till_done()
state = hass.states.get(later_entity_id)
assert state is not None
assert state.name == name
async def test_discovery_incl_nodeid(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator

View File

@@ -7,9 +7,10 @@ from unittest.mock import AsyncMock
from uiprotect.data import Camera, CloudAccount, Version
from homeassistant.components.unifiprotect.const import DOMAIN
from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from .utils import MockUFPFixture, init_entry
@@ -282,3 +283,26 @@ async def test_rtsp_no_fix_if_third_party(
assert msg["success"]
assert not msg["result"]["issues"]
async def test_rtsp_no_fix_if_globally_disabled(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test no RTSP disabled warning if RTSP is globally disabled on integration."""
for channel in doorbell.channels:
channel.is_rtsp_enabled = False
# Set RTSP globally disabled in config entry options
hass.config_entries.async_update_entry(
ufp.entry,
options={**ufp.entry.options, CONF_DISABLE_RTSP: True},
)
await init_entry(hass, ufp, [doorbell])
await async_process_repairs_platforms(hass)
assert len(issue_registry.issues) == 0

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,