d1d694302c
So a viewer sees recent trend on open instead of a blank chart. Viewing
only — reports still use the device's FTP .rnd data.
- NL43Reading table (auto-creates; no migration): unit_id, timestamp,
lp/leq/lmax/ln1/ln2.
- Monitor stores one downsampled reading per MONITOR_TRAIL_SAMPLE_S
(default 60s) from its keepalive poll loop, pruning rows older than
MONITOR_TRAIL_RETENTION_HOURS (default 24h). ~1440 rows/unit max.
- GET /api/nl43/{unit}/history?hours=N -> the trail for the last N hours
(clamped 0.1-48h), oldest-first.
Because keepalive runs 24/7, the trail fills continuously, so the history
is there whenever someone opens the live view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
152 lines
6.9 KiB
Python
152 lines
6.9 KiB
Python
from sqlalchemy import Column, String, DateTime, Boolean, Integer, Float, Text, func
|
|
from app.database import Base
|
|
|
|
|
|
class NL43Config(Base):
|
|
"""
|
|
NL43 connection/config metadata for the standalone SLMM addon.
|
|
"""
|
|
|
|
__tablename__ = "nl43_config"
|
|
|
|
unit_id = Column(String, primary_key=True, index=True)
|
|
host = Column(String, default="127.0.0.1")
|
|
tcp_port = Column(Integer, default=2255) # NL43 TCP control port (standard: 2255)
|
|
tcp_enabled = Column(Boolean, default=True)
|
|
ftp_enabled = Column(Boolean, default=False)
|
|
ftp_port = Column(Integer, default=21) # FTP port (standard: 21)
|
|
ftp_username = Column(String, nullable=True) # FTP login username
|
|
ftp_password = Column(String, nullable=True) # FTP login password
|
|
web_enabled = Column(Boolean, default=False)
|
|
|
|
# Background polling configuration
|
|
poll_interval_seconds = Column(Integer, nullable=True, default=60) # Polling interval (10-3600 seconds)
|
|
poll_enabled = Column(Boolean, default=True) # Enable/disable background polling for this device
|
|
|
|
# Live monitor (fan-out DOD feed). Keepalive runs it 24/7 even with no viewer,
|
|
# which is what makes alerting continuous. On by default; toggleable from the UI.
|
|
monitor_enabled = Column(Boolean, default=True)
|
|
|
|
|
|
class NL43Status(Base):
|
|
"""
|
|
Latest NL43 status snapshot for quick dashboard/API access.
|
|
"""
|
|
|
|
__tablename__ = "nl43_status"
|
|
|
|
unit_id = Column(String, primary_key=True, index=True)
|
|
last_seen = Column(DateTime, default=func.now())
|
|
measurement_state = Column(String, default="unknown") # Measure/Stop
|
|
measurement_start_time = Column(DateTime, nullable=True) # When measurement started (UTC)
|
|
counter = Column(String, nullable=True) # d0: Measurement interval counter (1-600)
|
|
lp = Column(String, nullable=True) # Instantaneous sound pressure level
|
|
leq = Column(String, nullable=True) # Equivalent continuous sound level
|
|
lmax = Column(String, nullable=True) # Maximum level
|
|
lmin = Column(String, nullable=True) # Minimum level
|
|
lpeak = Column(String, nullable=True) # Peak level
|
|
ln1 = Column(String, nullable=True) # Percentile slot LN1 (configurable; device default L5, contract L1)
|
|
ln2 = Column(String, nullable=True) # Percentile slot LN2 (configurable; device default L10)
|
|
battery_level = Column(String, nullable=True)
|
|
power_source = Column(String, nullable=True)
|
|
sd_remaining_mb = Column(String, nullable=True)
|
|
sd_free_ratio = Column(String, nullable=True)
|
|
raw_payload = Column(Text, nullable=True)
|
|
|
|
# Background polling status
|
|
is_reachable = Column(Boolean, default=True) # Device reachability status
|
|
consecutive_failures = Column(Integer, default=0) # Count of consecutive poll failures
|
|
last_poll_attempt = Column(DateTime, nullable=True) # Last time background poller attempted to poll
|
|
last_success = Column(DateTime, nullable=True) # Last successful poll timestamp
|
|
last_error = Column(Text, nullable=True) # Last error message (truncated to 500 chars)
|
|
|
|
# FTP start time sync tracking
|
|
start_time_sync_attempted = Column(Boolean, default=False) # True if FTP sync was attempted for current measurement
|
|
|
|
|
|
class DeviceLog(Base):
|
|
"""
|
|
Per-device log entries for debugging and audit trail.
|
|
Stores events like commands, state changes, errors, and FTP operations.
|
|
"""
|
|
|
|
__tablename__ = "device_logs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
unit_id = Column(String, index=True, nullable=False)
|
|
timestamp = Column(DateTime, default=func.now(), index=True)
|
|
level = Column(String, default="INFO") # DEBUG, INFO, WARNING, ERROR
|
|
category = Column(String, default="GENERAL") # TCP, FTP, POLL, COMMAND, STATE, SYNC
|
|
message = Column(Text, nullable=False)
|
|
|
|
|
|
class AlertRule(Base):
|
|
"""A threshold-alert rule evaluated against a unit's live monitor feed.
|
|
|
|
Source-agnostic: today it runs over the DOD monitor; the same rule transfers
|
|
unchanged if a unit's feed is later sourced from FTP intervals.
|
|
"""
|
|
|
|
__tablename__ = "alert_rules"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
unit_id = Column(String, index=True, nullable=False)
|
|
name = Column(String, nullable=False, default="Alert")
|
|
metric = Column(String, nullable=False, default="lp") # lp/leq/lmax/lmin/lpeak/ln1/ln2
|
|
comparison = Column(String, nullable=False, default="above") # above | below
|
|
threshold_db = Column(Float, nullable=False)
|
|
duration_s = Column(Integer, nullable=False, default=0) # sustained seconds (0 = instant)
|
|
clear_margin_db = Column(Float, nullable=False, default=2.0) # hysteresis band
|
|
cooldown_s = Column(Integer, nullable=False, default=300) # min seconds between onsets
|
|
# Optional time-of-day scoping (local time). schedule_start/end as "HH:MM";
|
|
# null = always active. schedule_days = CSV of 0-6 (Mon=0); null = every day.
|
|
schedule_start = Column(String, nullable=True)
|
|
schedule_end = Column(String, nullable=True)
|
|
schedule_days = Column(String, nullable=True)
|
|
channels = Column(String, nullable=False, default="log") # CSV: log,email,sms
|
|
recipients = Column(Text, nullable=True) # CSV of emails/phones
|
|
enabled = Column(Boolean, default=True)
|
|
created_at = Column(DateTime, default=func.now())
|
|
|
|
|
|
class AlertEvent(Base):
|
|
"""A fired alert (onset → clear), for history / inbox / acknowledgement."""
|
|
|
|
__tablename__ = "alert_events"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
rule_id = Column(Integer, index=True, nullable=False)
|
|
unit_id = Column(String, index=True, nullable=False)
|
|
rule_name = Column(String, nullable=True)
|
|
metric = Column(String, nullable=False)
|
|
threshold_db = Column(Float, nullable=False)
|
|
onset_at = Column(DateTime, default=func.now(), index=True)
|
|
onset_value = Column(Float, nullable=True)
|
|
peak_value = Column(Float, nullable=True)
|
|
clear_at = Column(DateTime, nullable=True)
|
|
status = Column(String, default="active") # active | cleared
|
|
acknowledged_at = Column(DateTime, nullable=True)
|
|
acknowledged_by = Column(String, nullable=True)
|
|
notes = Column(Text, nullable=True)
|
|
|
|
|
|
class NL43Reading(Base):
|
|
"""Downsampled time-series of live-monitor readings, for the live-chart
|
|
backfill (so a viewer sees recent trend on open, not a blank chart).
|
|
|
|
Viewing only — NOT the report source. Reports use the device's authoritative
|
|
FTP .rnd intervals. This is a short, capped trail (one row/minute, pruned to
|
|
a retention window) fed by the monitor's keepalive poll loop.
|
|
"""
|
|
|
|
__tablename__ = "nl43_readings"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
unit_id = Column(String, index=True, nullable=False)
|
|
timestamp = Column(DateTime, default=func.now(), index=True)
|
|
lp = Column(String, nullable=True)
|
|
leq = Column(String, nullable=True)
|
|
lmax = Column(String, nullable=True)
|
|
ln1 = Column(String, nullable=True)
|
|
ln2 = Column(String, nullable=True)
|