1 """Helpers to make instant statistics about your history."""
3 from __future__
import annotations
14 _LOGGER = logging.getLogger(__name__)
17 DURATION_START =
"start"
23 duration: datetime.timedelta |
None,
24 start_template: Template |
None,
25 end_template: Template |
None,
26 ) -> tuple[datetime.datetime, datetime.datetime]:
27 """Parse the templates and return the period."""
28 bounds: dict[str, datetime.datetime |
None] = {
32 for bound, template
in (
33 (DURATION_START, start_template),
34 (DURATION_END, end_template),
40 rendered = template.async_render()
41 except (TemplateError, TypeError)
as ex:
42 if ex.args
and not ex.args[0].startswith(
43 "UndefinedError: 'None' has no attribute"
45 _LOGGER.error(
"Error parsing template for field %s", bound, exc_info=ex)
47 if isinstance(rendered, str):
48 bounds[bound] = dt_util.parse_datetime(rendered)
49 if bounds[bound]
is not None:
52 bounds[bound] = dt_util.as_local(
53 dt_util.utc_from_timestamp(math.floor(
float(rendered)))
55 except ValueError
as ex:
57 f
"Parsing error: {bound} must be a datetime or a timestamp: {ex}"
60 start = bounds[DURATION_START]
61 end = bounds[DURATION_END]
65 assert end
is not None
66 assert duration
is not None
67 start = end - duration
69 assert start
is not None
70 assert duration
is not None
71 end = start + duration
77 value: float, period: tuple[datetime.datetime, datetime.datetime]
79 """Format the ratio of value / period duration."""
80 if len(period) != 2
or period[0] == period[1]:
83 ratio = 100 * value / (period[1] - period[0]).total_seconds()
84 return round(ratio, 1)
88 """Calculate the floored value of a timestamp."""
89 return math.floor(dt_util.as_timestamp(incoming_dt))
float pretty_ratio(float value, tuple[datetime.datetime, datetime.datetime] period)
tuple[datetime.datetime, datetime.datetime] async_calculate_period(datetime.timedelta|None duration, Template|None start_template, Template|None end_template)
float floored_timestamp(datetime.datetime incoming_dt)