1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Fix Matter epoch timestamp sensors (#157600)

This commit is contained in:
Ludovic BOUÉ
2025-12-10 07:13:21 +01:00
committed by GitHub
parent 95e344ea44
commit 2c7763e350
5 changed files with 114 additions and 6 deletions

View File

@@ -183,10 +183,35 @@ PUMP_CONTROL_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.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
def matter_epoch_seconds_to_utc(x: int | None) -> datetime | None:
"""Convert Matter epoch seconds (since 2000-01-01) to UTC datetime.
Returns None for non-positive or None values (represents unknown/absent).
"""
if x is None or x <= 0:
return None
return dt_util.utc_from_timestamp(x + MATTER_2000_TO_UNIX_EPOCH_OFFSET)
def matter_epoch_microseconds_to_utc(x: int | None) -> datetime | None:
"""Convert Matter epoch microseconds (since 2000-01-01) to UTC datetime.
The value is in microseconds; convert to seconds before applying offset.
Returns None for non-positive or None values.
"""
if x is None or x <= 0:
return None
seconds = x // 1_000_000
return dt_util.utc_from_timestamp(seconds + MATTER_2000_TO_UNIX_EPOCH_OFFSET)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -1468,7 +1493,8 @@ DISCOVERY_SCHEMAS = [
translation_key="auto_close_time",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=None,
device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None),
# AutoCloseTime is defined as epoch-us in the spec
device_to_ha=matter_epoch_microseconds_to_utc,
),
entity_class=MatterSensor,
featuremap_contains=clusters.ValveConfigurationAndControl.Bitmaps.Feature.kTimeSync,
@@ -1483,7 +1509,8 @@ DISCOVERY_SCHEMAS = [
translation_key="estimated_end_time",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=None,
device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None),
# EstimatedEndTime is defined as epoch-s (Matter 2000 epoch) in the spec
device_to_ha=matter_epoch_seconds_to_utc,
),
entity_class=MatterSensor,
required_attributes=(clusters.ServiceArea.Attributes.EstimatedEndTime,),

View File

@@ -357,7 +357,7 @@
],
"1/336/2": [],
"1/336/3": 7,
"1/336/4": 1756501200,
"1/336/4": 809816400,
"1/336/5": [],
"1/336/65532": 6,
"1/336/65533": 1,

View File

@@ -239,7 +239,7 @@
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/129/0": 0,
"1/129/1": 0,
"1/129/2": 0,
"1/129/2": 789004800000000,
"1/129/3": null,
"1/129/4": 0,
"1/129/5": 0,
@@ -248,7 +248,7 @@
"1/129/8": 100,
"1/129/9": 0,
"1/129/10": 0,
"1/129/65532": 0,
"1/129/65532": 1,
"1/129/65533": 1,
"1/129/65528": [],
"1/129/65529": [0, 1],

View File

@@ -11147,6 +11147,55 @@
'state': 'stopped',
})
# ---
# name: test_sensors[valve][sensor.valve_auto_close_time-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.valve_auto_close_time',
'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': 'Auto-close time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'auto_close_time',
'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlAutoCloseTime-129-2',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[valve][sensor.valve_auto_close_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Valve Auto-close time',
}),
'context': <ANY>,
'entity_id': 'sensor.valve_auto_close_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-01-01T00:00:00+00:00',
})
# ---
# name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -642,7 +642,7 @@ async def test_vacuum_actions(
assert state
assert state.state == "2025-08-29T21:00:00+00:00"
set_node_attribute(matter_node, 1, 336, 4, 1756502000)
set_node_attribute(matter_node, 1, 336, 4, 809817200)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_vacuum_estimated_end_time")
@@ -732,3 +732,35 @@ async def test_optional_door_event_sensors_from_featuremap(
state = hass.states.get(entity_id_closed)
assert state
assert state.state == "8"
@pytest.mark.parametrize("node_fixture", ["valve"])
async def test_valve(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test valve AutoCloseTime sensor with Matter epoch microseconds conversion."""
# ValveConfigurationAndControl Cluster / AutoCloseTime attribute (1/129/2)
# Initial value is 789004800000000 microseconds = 2025-01-01 00:00:00 UTC
state = hass.states.get("sensor.valve_auto_close_time")
assert state
assert state.state == "2025-01-01T00:00:00+00:00"
# Set to another timestamp: 820540800000000 microseconds
# = 820540800 seconds since 2000-01-01 = 1767225600 Unix epoch
# = 2026-01-01 00:00:00 UTC
set_node_attribute(matter_node, 1, 129, 2, 820540800000000)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.valve_auto_close_time")
assert state
assert state.state == "2026-01-01T00:00:00+00:00"
# Test setting to 0 (invalid/null) - should result in unknown state
set_node_attribute(matter_node, 1, 129, 2, 0)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.valve_auto_close_time")
assert state
assert state.state == "unknown"