Skip to content

Commit 2c7763e

Browse files
authored
Fix Matter epoch timestamp sensors (home-assistant#157600)
1 parent 95e344e commit 2c7763e

File tree

5 files changed

+114
-6
lines changed

5 files changed

+114
-6
lines changed

homeassistant/components/matter/sensor.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,35 @@
183183
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
184184
}
185185

186+
MATTER_2000_TO_UNIX_EPOCH_OFFSET = (
187+
946684800 # Seconds from Matter 2000 epoch to Unix epoch
188+
)
186189
HUMIDITY_SCALING_FACTOR = 100
187190
TEMPERATURE_SCALING_FACTOR = 100
188191

189192

193+
def matter_epoch_seconds_to_utc(x: int | None) -> datetime | None:
194+
"""Convert Matter epoch seconds (since 2000-01-01) to UTC datetime.
195+
196+
Returns None for non-positive or None values (represents unknown/absent).
197+
"""
198+
if x is None or x <= 0:
199+
return None
200+
return dt_util.utc_from_timestamp(x + MATTER_2000_TO_UNIX_EPOCH_OFFSET)
201+
202+
203+
def matter_epoch_microseconds_to_utc(x: int | None) -> datetime | None:
204+
"""Convert Matter epoch microseconds (since 2000-01-01) to UTC datetime.
205+
206+
The value is in microseconds; convert to seconds before applying offset.
207+
Returns None for non-positive or None values.
208+
"""
209+
if x is None or x <= 0:
210+
return None
211+
seconds = x // 1_000_000
212+
return dt_util.utc_from_timestamp(seconds + MATTER_2000_TO_UNIX_EPOCH_OFFSET)
213+
214+
190215
async def async_setup_entry(
191216
hass: HomeAssistant,
192217
config_entry: ConfigEntry,
@@ -1468,7 +1493,8 @@ def _update_from_device(self) -> None:
14681493
translation_key="auto_close_time",
14691494
device_class=SensorDeviceClass.TIMESTAMP,
14701495
state_class=None,
1471-
device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None),
1496+
# AutoCloseTime is defined as epoch-us in the spec
1497+
device_to_ha=matter_epoch_microseconds_to_utc,
14721498
),
14731499
entity_class=MatterSensor,
14741500
featuremap_contains=clusters.ValveConfigurationAndControl.Bitmaps.Feature.kTimeSync,
@@ -1483,7 +1509,8 @@ def _update_from_device(self) -> None:
14831509
translation_key="estimated_end_time",
14841510
device_class=SensorDeviceClass.TIMESTAMP,
14851511
state_class=None,
1486-
device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None),
1512+
# EstimatedEndTime is defined as epoch-s (Matter 2000 epoch) in the spec
1513+
device_to_ha=matter_epoch_seconds_to_utc,
14871514
),
14881515
entity_class=MatterSensor,
14891516
required_attributes=(clusters.ServiceArea.Attributes.EstimatedEndTime,),

tests/components/matter/fixtures/nodes/vacuum_cleaner.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@
357357
],
358358
"1/336/2": [],
359359
"1/336/3": 7,
360-
"1/336/4": 1756501200,
360+
"1/336/4": 809816400,
361361
"1/336/5": [],
362362
"1/336/65532": 6,
363363
"1/336/65533": 1,

tests/components/matter/fixtures/nodes/valve.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@
239239
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
240240
"1/129/0": 0,
241241
"1/129/1": 0,
242-
"1/129/2": 0,
242+
"1/129/2": 789004800000000,
243243
"1/129/3": null,
244244
"1/129/4": 0,
245245
"1/129/5": 0,
@@ -248,7 +248,7 @@
248248
"1/129/8": 100,
249249
"1/129/9": 0,
250250
"1/129/10": 0,
251-
"1/129/65532": 0,
251+
"1/129/65532": 1,
252252
"1/129/65533": 1,
253253
"1/129/65528": [],
254254
"1/129/65529": [0, 1],

tests/components/matter/snapshots/test_sensor.ambr

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11147,6 +11147,55 @@
1114711147
'state': 'stopped',
1114811148
})
1114911149
# ---
11150+
# name: test_sensors[valve][sensor.valve_auto_close_time-entry]
11151+
EntityRegistryEntrySnapshot({
11152+
'aliases': set({
11153+
}),
11154+
'area_id': None,
11155+
'capabilities': None,
11156+
'config_entry_id': <ANY>,
11157+
'config_subentry_id': <ANY>,
11158+
'device_class': None,
11159+
'device_id': <ANY>,
11160+
'disabled_by': None,
11161+
'domain': 'sensor',
11162+
'entity_category': None,
11163+
'entity_id': 'sensor.valve_auto_close_time',
11164+
'has_entity_name': True,
11165+
'hidden_by': None,
11166+
'icon': None,
11167+
'id': <ANY>,
11168+
'labels': set({
11169+
}),
11170+
'name': None,
11171+
'options': dict({
11172+
}),
11173+
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
11174+
'original_icon': None,
11175+
'original_name': 'Auto-close time',
11176+
'platform': 'matter',
11177+
'previous_unique_id': None,
11178+
'suggested_object_id': None,
11179+
'supported_features': 0,
11180+
'translation_key': 'auto_close_time',
11181+
'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlAutoCloseTime-129-2',
11182+
'unit_of_measurement': None,
11183+
})
11184+
# ---
11185+
# name: test_sensors[valve][sensor.valve_auto_close_time-state]
11186+
StateSnapshot({
11187+
'attributes': ReadOnlyDict({
11188+
'device_class': 'timestamp',
11189+
'friendly_name': 'Valve Auto-close time',
11190+
}),
11191+
'context': <ANY>,
11192+
'entity_id': 'sensor.valve_auto_close_time',
11193+
'last_changed': <ANY>,
11194+
'last_reported': <ANY>,
11195+
'last_updated': <ANY>,
11196+
'state': '2025-01-01T00:00:00+00:00',
11197+
})
11198+
# ---
1115011199
# name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-entry]
1115111200
EntityRegistryEntrySnapshot({
1115211201
'aliases': set({

tests/components/matter/test_sensor.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ async def test_vacuum_actions(
642642
assert state
643643
assert state.state == "2025-08-29T21:00:00+00:00"
644644

645-
set_node_attribute(matter_node, 1, 336, 4, 1756502000)
645+
set_node_attribute(matter_node, 1, 336, 4, 809817200)
646646
await trigger_subscription_callback(hass, matter_client)
647647

648648
state = hass.states.get("sensor.mock_vacuum_estimated_end_time")
@@ -732,3 +732,35 @@ async def test_optional_door_event_sensors_from_featuremap(
732732
state = hass.states.get(entity_id_closed)
733733
assert state
734734
assert state.state == "8"
735+
736+
737+
@pytest.mark.parametrize("node_fixture", ["valve"])
738+
async def test_valve(
739+
hass: HomeAssistant,
740+
matter_client: MagicMock,
741+
matter_node: MatterNode,
742+
) -> None:
743+
"""Test valve AutoCloseTime sensor with Matter epoch microseconds conversion."""
744+
# ValveConfigurationAndControl Cluster / AutoCloseTime attribute (1/129/2)
745+
# Initial value is 789004800000000 microseconds = 2025-01-01 00:00:00 UTC
746+
state = hass.states.get("sensor.valve_auto_close_time")
747+
assert state
748+
assert state.state == "2025-01-01T00:00:00+00:00"
749+
750+
# Set to another timestamp: 820540800000000 microseconds
751+
# = 820540800 seconds since 2000-01-01 = 1767225600 Unix epoch
752+
# = 2026-01-01 00:00:00 UTC
753+
set_node_attribute(matter_node, 1, 129, 2, 820540800000000)
754+
await trigger_subscription_callback(hass, matter_client)
755+
756+
state = hass.states.get("sensor.valve_auto_close_time")
757+
assert state
758+
assert state.state == "2026-01-01T00:00:00+00:00"
759+
760+
# Test setting to 0 (invalid/null) - should result in unknown state
761+
set_node_attribute(matter_node, 1, 129, 2, 0)
762+
await trigger_subscription_callback(hass, matter_client)
763+
764+
state = hass.states.get("sensor.valve_auto_close_time")
765+
assert state
766+
assert state.state == "unknown"

0 commit comments

Comments
 (0)