Sync event timed_fired and the context ulid time (#71854)
This commit is contained in:
parent
8c2743bb67
commit
ebce5660e3
8 changed files with 370 additions and 36 deletions
|
@ -55,10 +55,6 @@ SCHEMA_VERSION = 28
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# EPOCHORDINAL is not exposed as a constant
|
||||
# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12
|
||||
EPOCHORDINAL = datetime(1970, 1, 1).toordinal()
|
||||
|
||||
DB_TIMEZONE = "+00:00"
|
||||
|
||||
TABLE_EVENTS = "events"
|
||||
|
@ -649,16 +645,8 @@ def process_datetime_to_timestamp(ts: datetime) -> float:
|
|||
Mirrors the behavior of process_timestamp_to_utc_isoformat
|
||||
except it returns the epoch time.
|
||||
"""
|
||||
if ts.tzinfo is None:
|
||||
# Taken from
|
||||
# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L185
|
||||
return (
|
||||
(ts.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ ts.hour * 3600
|
||||
+ ts.minute * 60
|
||||
+ ts.second
|
||||
+ (ts.microsecond / 1000000)
|
||||
)
|
||||
if ts.tzinfo is None or ts.tzinfo == dt_util.UTC:
|
||||
return dt_util.utc_to_timestamp(ts)
|
||||
return ts.timestamp()
|
||||
|
||||
|
||||
|
|
|
@ -734,7 +734,9 @@ class Event:
|
|||
self.data = data or {}
|
||||
self.origin = origin
|
||||
self.time_fired = time_fired or dt_util.utcnow()
|
||||
self.context: Context = context or Context()
|
||||
self.context: Context = context or Context(
|
||||
id=ulid_util.ulid(dt_util.utc_to_timestamp(self.time_fired))
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Make hashable."""
|
||||
|
@ -1363,11 +1365,11 @@ class StateMachine:
|
|||
if same_state and same_attr:
|
||||
return
|
||||
|
||||
if context is None:
|
||||
context = Context()
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if context is None:
|
||||
context = Context(id=ulid_util.ulid(dt_util.utc_to_timestamp(now)))
|
||||
|
||||
state = State(
|
||||
entity_id,
|
||||
new_state,
|
||||
|
|
|
@ -14,6 +14,10 @@ DATE_STR_FORMAT = "%Y-%m-%d"
|
|||
UTC = dt.timezone.utc
|
||||
DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc
|
||||
|
||||
# EPOCHORDINAL is not exposed as a constant
|
||||
# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12
|
||||
EPOCHORDINAL = dt.datetime(1970, 1, 1).toordinal()
|
||||
|
||||
# Copyright (c) Django Software Foundation and individual contributors.
|
||||
# All rights reserved.
|
||||
# https://github.com/django/django/blob/master/LICENSE
|
||||
|
@ -98,6 +102,19 @@ def utc_from_timestamp(timestamp: float) -> dt.datetime:
|
|||
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC)
|
||||
|
||||
|
||||
def utc_to_timestamp(utc_dt: dt.datetime) -> float:
|
||||
"""Fast conversion of a datetime in UTC to a timestamp."""
|
||||
# Taken from
|
||||
# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L185
|
||||
return (
|
||||
(utc_dt.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ utc_dt.hour * 3600
|
||||
+ utc_dt.minute * 60
|
||||
+ utc_dt.second
|
||||
+ (utc_dt.microsecond / 1000000)
|
||||
)
|
||||
|
||||
|
||||
def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime:
|
||||
"""Return local datetime object of start of day from date or datetime."""
|
||||
if dt_or_d is None:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Helpers to generate ulids."""
|
||||
from __future__ import annotations
|
||||
|
||||
from random import getrandbits
|
||||
import time
|
||||
|
@ -17,7 +18,7 @@ def ulid_hex() -> str:
|
|||
return f"{int(time.time()*1000):012x}{getrandbits(80):020x}"
|
||||
|
||||
|
||||
def ulid() -> str:
|
||||
def ulid(timestamp: float | None = None) -> str:
|
||||
"""Generate a ULID.
|
||||
|
||||
This ulid should not be used for cryptographically secure
|
||||
|
@ -34,9 +35,9 @@ def ulid() -> str:
|
|||
import ulid
|
||||
ulid.parse(ulid_util.ulid())
|
||||
"""
|
||||
ulid_bytes = int(time.time() * 1000).to_bytes(6, byteorder="big") + int(
|
||||
getrandbits(80)
|
||||
).to_bytes(10, byteorder="big")
|
||||
ulid_bytes = int((timestamp or time.time()) * 1000).to_bytes(
|
||||
6, byteorder="big"
|
||||
) + int(getrandbits(80)).to_bytes(10, byteorder="big")
|
||||
|
||||
# This is base32 crockford encoding with the loop unrolled for performance
|
||||
#
|
||||
|
|
|
@ -811,7 +811,7 @@ async def test_humidity_change_dry_trigger_on_not_long_enough(hass, setup_comp_4
|
|||
async def test_humidity_change_dry_trigger_on_long_enough(hass, setup_comp_4):
|
||||
"""Test if humidity change turn dry on."""
|
||||
fake_changed = datetime.datetime(
|
||||
1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
|
@ -845,7 +845,7 @@ async def test_humidity_change_dry_trigger_off_not_long_enough(hass, setup_comp_
|
|||
async def test_humidity_change_dry_trigger_off_long_enough(hass, setup_comp_4):
|
||||
"""Test if humidity change turn dry on."""
|
||||
fake_changed = datetime.datetime(
|
||||
1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
|
@ -967,7 +967,7 @@ async def test_humidity_change_humidifier_trigger_on_not_long_enough(
|
|||
async def test_humidity_change_humidifier_trigger_on_long_enough(hass, setup_comp_6):
|
||||
"""Test if humidity change turn humidifier on after min cycle."""
|
||||
fake_changed = datetime.datetime(
|
||||
1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
|
@ -989,7 +989,7 @@ async def test_humidity_change_humidifier_trigger_on_long_enough(hass, setup_com
|
|||
async def test_humidity_change_humidifier_trigger_off_long_enough(hass, setup_comp_6):
|
||||
"""Test if humidity change turn humidifier off after min cycle."""
|
||||
fake_changed = datetime.datetime(
|
||||
1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
|
|
|
@ -749,7 +749,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4):
|
|||
|
||||
async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4):
|
||||
"""Test if temperature change turn ac on."""
|
||||
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
):
|
||||
|
@ -775,7 +775,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4):
|
|||
|
||||
async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4):
|
||||
"""Test if temperature change turn ac on."""
|
||||
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
):
|
||||
|
@ -855,7 +855,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5):
|
|||
|
||||
async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5):
|
||||
"""Test if temperature change turn ac on."""
|
||||
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
):
|
||||
|
@ -881,7 +881,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5):
|
|||
|
||||
async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5):
|
||||
"""Test if temperature change turn ac on."""
|
||||
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
):
|
||||
|
@ -969,7 +969,7 @@ async def test_temp_change_heater_trigger_on_not_long_enough(hass, setup_comp_6)
|
|||
|
||||
async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6):
|
||||
"""Test if temperature change turn heater on after min cycle."""
|
||||
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
):
|
||||
|
@ -986,7 +986,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6):
|
|||
|
||||
async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6):
|
||||
"""Test if temperature change turn heater off after min cycle."""
|
||||
fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
|
||||
with patch(
|
||||
"homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed
|
||||
):
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
"""Test to verify that Home Assistant core works."""
|
||||
from __future__ import annotations
|
||||
|
||||
# pylint: disable=protected-access
|
||||
import array
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import functools
|
||||
|
@ -28,6 +31,7 @@ from homeassistant.const import (
|
|||
__version__,
|
||||
)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.core import State
|
||||
from homeassistant.exceptions import (
|
||||
InvalidEntityFormatError,
|
||||
InvalidStateError,
|
||||
|
@ -1489,9 +1493,300 @@ async def test_reserving_states(hass):
|
|||
assert hass.states.async_available("light.bedroom") is True
|
||||
|
||||
|
||||
async def test_state_change_events_match_state_time(hass):
|
||||
"""Test last_updated and timed_fired only call utcnow once."""
|
||||
def _ulid_timestamp(ulid: str) -> int:
|
||||
encoded = ulid[:10].encode("ascii")
|
||||
# This unpacks the time from the ulid
|
||||
|
||||
# Copied from
|
||||
# https://github.com/ahawker/ulid/blob/06289583e9de4286b4d80b4ad000d137816502ca/ulid/base32.py#L296
|
||||
decoding = array.array(
|
||||
"B",
|
||||
(
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x00,
|
||||
0x01,
|
||||
0x02,
|
||||
0x03,
|
||||
0x04,
|
||||
0x05,
|
||||
0x06,
|
||||
0x07,
|
||||
0x08,
|
||||
0x09,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x0A,
|
||||
0x0B,
|
||||
0x0C,
|
||||
0x0D,
|
||||
0x0E,
|
||||
0x0F,
|
||||
0x10,
|
||||
0x11,
|
||||
0x01,
|
||||
0x12,
|
||||
0x13,
|
||||
0x01,
|
||||
0x14,
|
||||
0x15,
|
||||
0x00,
|
||||
0x16,
|
||||
0x17,
|
||||
0x18,
|
||||
0x19,
|
||||
0x1A,
|
||||
0xFF,
|
||||
0x1B,
|
||||
0x1C,
|
||||
0x1D,
|
||||
0x1E,
|
||||
0x1F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x0A,
|
||||
0x0B,
|
||||
0x0C,
|
||||
0x0D,
|
||||
0x0E,
|
||||
0x0F,
|
||||
0x10,
|
||||
0x11,
|
||||
0x01,
|
||||
0x12,
|
||||
0x13,
|
||||
0x01,
|
||||
0x14,
|
||||
0x15,
|
||||
0x00,
|
||||
0x16,
|
||||
0x17,
|
||||
0x18,
|
||||
0x19,
|
||||
0x1A,
|
||||
0xFF,
|
||||
0x1B,
|
||||
0x1C,
|
||||
0x1D,
|
||||
0x1E,
|
||||
0x1F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
),
|
||||
)
|
||||
return int.from_bytes(
|
||||
bytes(
|
||||
(
|
||||
((decoding[encoded[0]] << 5) | decoding[encoded[1]]) & 0xFF,
|
||||
((decoding[encoded[2]] << 3) | (decoding[encoded[3]] >> 2)) & 0xFF,
|
||||
(
|
||||
(decoding[encoded[3]] << 6)
|
||||
| (decoding[encoded[4]] << 1)
|
||||
| (decoding[encoded[5]] >> 4)
|
||||
)
|
||||
& 0xFF,
|
||||
((decoding[encoded[5]] << 4) | (decoding[encoded[6]] >> 1)) & 0xFF,
|
||||
(
|
||||
(decoding[encoded[6]] << 7)
|
||||
| (decoding[encoded[7]] << 2)
|
||||
| (decoding[encoded[8]] >> 3)
|
||||
)
|
||||
& 0xFF,
|
||||
((decoding[encoded[8]] << 5) | (decoding[encoded[9]])) & 0xFF,
|
||||
)
|
||||
),
|
||||
byteorder="big",
|
||||
)
|
||||
|
||||
|
||||
async def test_state_change_events_context_id_match_state_time(hass):
|
||||
"""Test last_updated, timed_fired, and the ulid all have the same time."""
|
||||
events = []
|
||||
|
||||
@ha.callback
|
||||
|
@ -1502,6 +1797,31 @@ async def test_state_change_events_match_state_time(hass):
|
|||
|
||||
hass.states.async_set("light.bedroom", "on")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.bedroom")
|
||||
|
||||
state: State = hass.states.get("light.bedroom")
|
||||
assert state.last_updated == events[0].time_fired
|
||||
assert len(state.context.id) == 26
|
||||
# ULIDs store time to 3 decimal places compared to python timestamps
|
||||
assert _ulid_timestamp(state.context.id) == int(
|
||||
state.last_updated.timestamp() * 1000
|
||||
)
|
||||
|
||||
|
||||
async def test_state_firing_event_matches_context_id_ulid_time(hass):
|
||||
"""Test timed_fired and the ulid have the same time."""
|
||||
events = []
|
||||
|
||||
@ha.callback
|
||||
def _event_listener(event):
|
||||
events.append(event)
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STARTED, _event_listener)
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
event = events[0]
|
||||
assert len(event.context.id) == 26
|
||||
# ULIDs store time to 3 decimal places compared to python timestamps
|
||||
assert _ulid_timestamp(event.context.id) == int(
|
||||
events[0].time_fired.timestamp() * 1000
|
||||
)
|
||||
|
|
|
@ -106,6 +106,12 @@ def test_utc_from_timestamp():
|
|||
)
|
||||
|
||||
|
||||
def test_timestamp_to_utc():
|
||||
"""Test we can convert a utc datetime to a timestamp."""
|
||||
utc_now = dt_util.utcnow()
|
||||
assert dt_util.utc_to_timestamp(utc_now) == utc_now.timestamp()
|
||||
|
||||
|
||||
def test_as_timestamp():
|
||||
"""Test as_timestamp method."""
|
||||
ts = 1462401234
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue