Initial pump monitor with Coolify deployment
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from .storage import Storage
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PowerSample:
|
||||
sampled_at: datetime
|
||||
watts: float
|
||||
|
||||
|
||||
class CycleDetector:
|
||||
"""Detect pump runs using two thresholds and consecutive-sample confirmation."""
|
||||
|
||||
def __init__(self, storage: Storage, start_watts: float, stop_watts: float, confirm_samples: int):
|
||||
self.storage = storage
|
||||
self.start_watts = start_watts
|
||||
self.stop_watts = stop_watts
|
||||
self.confirm_samples = confirm_samples
|
||||
self.running = False
|
||||
self.cycle_id: int | None = None
|
||||
self.pending_start: list[PowerSample] = []
|
||||
self.pending_stop: list[PowerSample] = []
|
||||
self.first_observation = True
|
||||
self.pending_incomplete_start = False
|
||||
self.last_sample_at: datetime | None = None
|
||||
|
||||
def process(self, sample: PowerSample) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
self.last_sample_at = sample.sampled_at
|
||||
|
||||
if not self.running:
|
||||
if sample.watts >= self.start_watts:
|
||||
if not self.pending_start:
|
||||
self.pending_incomplete_start = self.first_observation
|
||||
self.pending_start.append(sample)
|
||||
if len(self.pending_start) >= self.confirm_samples:
|
||||
started_at = self.pending_start[0].sampled_at
|
||||
self.cycle_id = self.storage.start_cycle(started_at, self.pending_incomplete_start)
|
||||
self.running = True
|
||||
events.append(
|
||||
{
|
||||
"type": "started",
|
||||
"cycle_id": self.cycle_id,
|
||||
"at": started_at,
|
||||
"incomplete_start": self.pending_incomplete_start,
|
||||
}
|
||||
)
|
||||
self.pending_start.clear()
|
||||
self.pending_incomplete_start = False
|
||||
else:
|
||||
self.pending_start.clear()
|
||||
self.pending_incomplete_start = False
|
||||
else:
|
||||
if sample.watts <= self.stop_watts:
|
||||
self.pending_stop.append(sample)
|
||||
if len(self.pending_stop) >= self.confirm_samples:
|
||||
ended_at = self.pending_stop[0].sampled_at
|
||||
cycle_id = self.cycle_id
|
||||
if cycle_id is None:
|
||||
raise RuntimeError("Running detector has no cycle id")
|
||||
cycle = self.storage.finish_cycle(cycle_id, ended_at)
|
||||
events.append({"type": "stopped", "cycle": cycle, "at": ended_at})
|
||||
self.running = False
|
||||
self.cycle_id = None
|
||||
self.pending_stop.clear()
|
||||
else:
|
||||
self.pending_stop.clear()
|
||||
|
||||
self.first_observation = False
|
||||
return events
|
||||
|
||||
def interrupt(self, ended_at: datetime | None = None) -> dict[str, Any] | None:
|
||||
event = None
|
||||
if self.running and self.cycle_id is not None:
|
||||
final_time = ended_at or self.last_sample_at
|
||||
if final_time is not None:
|
||||
cycle = self.storage.finish_cycle(self.cycle_id, final_time, incomplete_end=True)
|
||||
event = {"type": "interrupted", "cycle": cycle, "at": final_time}
|
||||
self.running = False
|
||||
self.cycle_id = None
|
||||
self.pending_start.clear()
|
||||
self.pending_stop.clear()
|
||||
self.pending_incomplete_start = False
|
||||
self.first_observation = True
|
||||
self.last_sample_at = None
|
||||
return event
|
||||
Reference in New Issue
Block a user