Added Alexa Music
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs.
|
||||
# Copyright (C) 2026 Lukas Bandura
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Recorder for raw cycle data in WashData."""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -13,8 +29,6 @@ from homeassistant.util import dt as dt_util
|
||||
from .const import (
|
||||
STORAGE_VERSION,
|
||||
STORAGE_KEY,
|
||||
SHORT_SILENCE_THRESHOLD_S,
|
||||
TRIM_BUFFER_S,
|
||||
)
|
||||
from .log_utils import DeviceLoggerAdapter
|
||||
|
||||
@@ -206,112 +220,3 @@ class CycleRecorder:
|
||||
elif not self._last_save:
|
||||
self.hass.add_job(self._async_save)
|
||||
|
||||
def get_trim_suggestions(
|
||||
self,
|
||||
data: list[tuple[str, float]],
|
||||
recording_start: datetime | None = None,
|
||||
recording_end: datetime | None = None,
|
||||
) -> tuple[float, float, float]:
|
||||
"""Analyze data to propose trims.
|
||||
|
||||
Args:
|
||||
data: List of (iso_timestamp, power)
|
||||
recording_start: Actual start time of recording (for head trim relative to start)
|
||||
recording_end: Actual end time of recording (for tail trim relative to end)
|
||||
|
||||
Returns: (head_trim_seconds, tail_trim_seconds, median_dt)
|
||||
"""
|
||||
if not data:
|
||||
# No data found - return full recording duration as trim
|
||||
if recording_start and recording_end:
|
||||
dur = (recording_end - recording_start).total_seconds()
|
||||
return 0.0, dur, 0.0
|
||||
return 0.0, 0.0, 0.0
|
||||
|
||||
# Parse timestamps and powers
|
||||
parsed: list[tuple[float, float]] = []
|
||||
for t_str, p in data:
|
||||
t = dt_util.parse_datetime(t_str)
|
||||
if t:
|
||||
parsed.append((t.timestamp(), p))
|
||||
|
||||
if not parsed:
|
||||
return 0.0, 0.0, 0.0
|
||||
|
||||
data_start_ts = parsed[0][0]
|
||||
data_end_ts = parsed[-1][0]
|
||||
|
||||
# Use provided bounds or fallback to data bounds
|
||||
rec_start_ts = recording_start.timestamp() if recording_start else data_start_ts
|
||||
rec_end_ts = recording_end.timestamp() if recording_end else data_end_ts
|
||||
|
||||
# Ensure bounds cover data
|
||||
rec_start_ts = min(rec_start_ts, data_start_ts)
|
||||
rec_end_ts = max(rec_end_ts, data_end_ts)
|
||||
|
||||
threshold = 1.0 # W
|
||||
|
||||
first_active_idx = -1
|
||||
last_active_idx = -1
|
||||
|
||||
for i, (_, p) in enumerate(parsed):
|
||||
if p > threshold:
|
||||
if first_active_idx == -1:
|
||||
first_active_idx = i
|
||||
last_active_idx = i
|
||||
|
||||
if first_active_idx == -1:
|
||||
# No activity found
|
||||
total_dur = rec_end_ts - rec_start_ts
|
||||
return 0.0, round(total_dur, 1), 0.0
|
||||
|
||||
head_ts = parsed[first_active_idx][0]
|
||||
tail_ts = parsed[last_active_idx][0]
|
||||
|
||||
if len(parsed) > 1:
|
||||
dts = [t - s for (t, _), (s, _) in zip(parsed[1:], parsed[:-1])]
|
||||
# Median calculation without numpy
|
||||
dts.sort()
|
||||
mid = len(dts) // 2
|
||||
if len(dts) % 2 == 0:
|
||||
median_dt = (dts[mid - 1] + dts[mid]) / 2.0
|
||||
else:
|
||||
median_dt = dts[mid]
|
||||
if median_dt <= 0:
|
||||
median_dt = 1.0 # Fallback
|
||||
else:
|
||||
median_dt = 1.0
|
||||
|
||||
# 1. Head Trim
|
||||
# Time from recording start to first active sample
|
||||
raw_head_trim = max(0.0, head_ts - rec_start_ts)
|
||||
|
||||
# Align to sampling rate (floor to keep buffer)
|
||||
# Example: raw=19s, dt=10s -> trim 10s. Buffer=9s.
|
||||
# Example: raw=21s, dt=10s -> trim 20s. Buffer=1s.
|
||||
# To ensure we don't cut active sample if jitter:
|
||||
# We start at rec_start_ts. We want start_time + trim <= head_ts
|
||||
# floor ensures this.
|
||||
|
||||
steps_head = int(raw_head_trim / median_dt)
|
||||
# Align trim to sampling rate (floor to keep buffer before active sample)
|
||||
|
||||
# However, if using the "floor" logic makes it 0, that's fine.
|
||||
head_trim = steps_head * median_dt
|
||||
|
||||
# 2. Tail Trim
|
||||
# Time from last active sample to recording end
|
||||
# For manual recordings, we want to be conservative because of drying phases.
|
||||
raw_tail_trim = max(0.0, rec_end_ts - tail_ts)
|
||||
|
||||
# If tail silence is less than SHORT_SILENCE_THRESHOLD_S, suggest 0 trim to be safe.
|
||||
# Dishwashers often have 5-10 min silent periods that are NOT the end.
|
||||
if raw_tail_trim < SHORT_SILENCE_THRESHOLD_S:
|
||||
tail_trim = 0.0
|
||||
else:
|
||||
# If it's very long, suggest trimming but keep a TRIM_BUFFER_S buffer
|
||||
tail_trim = max(0.0, raw_tail_trim - TRIM_BUFFER_S)
|
||||
steps_tail = int(tail_trim / median_dt)
|
||||
tail_trim = steps_tail * median_dt
|
||||
|
||||
return round(head_trim, 1), round(tail_trim, 1), round(median_dt, 1)
|
||||
|
||||
Reference in New Issue
Block a user