Files
bellsystems-cp/backend/devices/models.py
T
bonaminandClaude Sonnet 5 8ec13947cb fix(devices): restore warranty/maintenance dates missing from timestamp field list
subscrStart, warrantyStart, and maintainedOn are written to Firestore as
Timestamps like the other date fields here, but were missing from
_TIMESTAMP_FIELD_NAMES, so they weren't being converted back to ISO
strings on read.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 18:30:15 +03:00

229 lines
6.9 KiB
Python

from pydantic import BaseModel, Field
from typing import Any, Dict, List, Optional
from enum import Enum
# --- Enums ---
class HelperMelodyTypes(str, Enum):
orthodox = "orthodox"
catholic = "catholic"
all = "all"
class HelperRingAlerts(str, Enum):
disabled = "disabled"
single = "single"
multi = "multi"
class DeviceTiers(str, Enum):
basic = "basic"
small = "small"
mini = "mini"
premium = "premium"
vip = "vip"
custom = "custom"
# --- Nested Structs ---
class DeviceNetworkSettings(BaseModel):
hostname: str = ""
useStaticIP: bool = False
ipAddress: Any = []
gateway: Any = []
subnet: Any = []
dns1: Any = []
dns2: Any = []
class DeviceClockSettings(BaseModel):
clockOutputs: List[int] = []
clockTimings: List[int] = []
ringAlertsMasterOn: bool = False
ringAlerts: HelperRingAlerts = HelperRingAlerts.disabled
ringIntervals: int = 0
hourAlertsBell: int = 0
halfhourAlertsBell: int = 0
quarterAlertsBell: int = 0
isDaySilenceOn: bool = False
isNightSilenceOn: bool = False
daySilenceFrom: str = ""
daySilenceTo: str = ""
nightSilenceFrom: str = ""
nightSilenceTo: str = ""
backlightTurnOnTime: str = ""
backlightTurnOffTime: str = ""
isBacklightAutomationOn: bool = False
backlightOutput: int = 0
class DeviceAttributes(BaseModel):
hasAssistant: bool = False
hasClock: bool = False
hasBells: bool = False
totalBells: int = 0
bellOutputs: List[int] = []
hammerTimings: List[int] = []
bellGuardOn: bool = False
bellGuardSafetyOn: bool = False
warningsOn: bool = False
towerClockTime: str = ""
clockSettings: DeviceClockSettings = DeviceClockSettings()
deviceLocale: HelperMelodyTypes = HelperMelodyTypes.all
networkSettings: DeviceNetworkSettings = DeviceNetworkSettings()
serialLogLevel: int = 0
sdLogLevel: int = 0
mqttLogLevel: int = 0
class DeviceSubInformation(BaseModel):
subscrTier: DeviceTiers = DeviceTiers.basic
subscrStart: str = ""
subscrDuration: int = 0
maxUsers: int = 0
maxOutputs: int = 0
class DeviceStatistics(BaseModel):
totalPlaybacks: int = 0
totalHammerStrikes: int = 0
perBellStrikes: List[int] = []
totalWarningsGiven: int = 0
warrantyActive: bool = False
warrantyStart: str = ""
warrantyPeriod: int = 0
maintainedOn: str = ""
maintainancePeriod: int = 0
class HealthThresholdTier(BaseModel):
"""One severity tier's worth of metric thresholds. Two tiers (warning,
critical) make up DeviceHealthSettings — see there for why."""
restarts_per_week_threshold: int
rssi_floor_dbm: int
free_heap_floor_kb: int
cpu_temp_ceiling_c: int
class DeviceHealthSettings(BaseModel):
"""Per-device alert thresholds shown/edited on the Health tab's Settings
sub-tab. Purely advisory today — thresholds drive a client-computed
ok/warning/critical status icon, not any server-side alerting yet (no
email/push infra exists). Two severity tiers per metric (warning fires
first, critical is the harder breach) so the header status icon can show
orange vs. red rather than a single degraded/healthy binary.
offline_timeout_sec also feeds the Health tab's chart gap detection
(LineChart's gapThresholdMs) so both share one source of truth."""
warning: HealthThresholdTier = HealthThresholdTier(
restarts_per_week_threshold=6, rssi_floor_dbm=-80, free_heap_floor_kb=30, cpu_temp_ceiling_c=65,
)
critical: HealthThresholdTier = HealthThresholdTier(
restarts_per_week_threshold=12, rssi_floor_dbm=-90, free_heap_floor_kb=15, cpu_temp_ceiling_c=80,
)
offline_timeout_sec: int = 90
email_on_threshold: bool = False
push_on_crash_boot: bool = False
class MelodyMainItem(BaseModel):
"""Mirrors the Melody schema used in the melodies collection."""
information: dict = {}
default_settings: dict = {}
type: str = ""
url: str = ""
uid: str = ""
pid: str = ""
# --- Request / Response schemas ---
class DeviceCreate(BaseModel):
device_name: str = ""
# Console-only friendly name — set by staff, independent of the name the
# end user gave the device in the app. Never shown to app users, never
# synced from/to device_name. Falls back to device_name when empty.
console_name: str = ""
device_photo: str = ""
device_location: str = ""
is_Online: bool = False
device_attributes: DeviceAttributes = DeviceAttributes()
device_subscription: DeviceSubInformation = DeviceSubInformation()
device_stats: DeviceStatistics = DeviceStatistics()
device_health_settings: DeviceHealthSettings = DeviceHealthSettings()
events_on: bool = False
device_location_coordinates: Any = None # GeoPoint dict {lat, lng} or legacy str
device_melodies_all: List[MelodyMainItem] = []
device_melodies_favorites: List[str] = []
user_list: List[str] = []
websocket_url: str = ""
churchAssistantURL: str = ""
staffNotes: str = ""
hw_family: str = ""
hw_revision: str = ""
tags: List[str] = []
serial_number: str = ""
customer_id: str = ""
mfg_status: str = ""
class DeviceUpdate(BaseModel):
device_name: Optional[str] = None
console_name: Optional[str] = None
device_photo: Optional[str] = None
device_location: Optional[str] = None
is_Online: Optional[bool] = None
# Use raw dicts so only the fields actually sent are present — no Pydantic defaults
device_attributes: Optional[Dict[str, Any]] = None
device_subscription: Optional[Dict[str, Any]] = None
device_stats: Optional[Dict[str, Any]] = None
device_health_settings: Optional[Dict[str, Any]] = None
events_on: Optional[bool] = None
device_location_coordinates: Optional[Any] = None # dict {lat, lng} or legacy str
device_melodies_all: Optional[List[MelodyMainItem]] = None
device_melodies_favorites: Optional[List[str]] = None
user_list: Optional[List[str]] = None
websocket_url: Optional[str] = None
churchAssistantURL: Optional[str] = None
staffNotes: Optional[str] = None
hw_family: Optional[str] = None
hw_revision: Optional[str] = None
tags: Optional[List[str]] = None
customer_id: Optional[str] = None
mfg_status: Optional[str] = None
class DeviceInDB(DeviceCreate):
id: str
# Legacy field — kept for backwards compat; new docs use serial_number
device_id: str = ""
class DeviceListResponse(BaseModel):
devices: List[DeviceInDB]
total: int
class DeviceNoteCreate(BaseModel):
content: str
created_by: str = ""
class DeviceNoteUpdate(BaseModel):
content: str
class DeviceUserInfo(BaseModel):
"""User info resolved from device_users sub-collection or user_list."""
user_id: str = ""
display_name: str = ""
email: str = ""
role: str = ""
photo_url: str = ""
class DeviceUsersResponse(BaseModel):
users: List[DeviceUserInfo]
total: int