First Production Push

This commit is contained in:
2026-02-25 21:29:56 +02:00
parent e62cffc10c
commit 8cb639c1bd
32 changed files with 3714 additions and 2719 deletions

View File

@@ -7,11 +7,12 @@ from typing import List, Optional
from builder import database as db
from builder.models import BuiltMelodyCreate, BuiltMelodyUpdate, BuiltMelodyInDB
from fastapi import HTTPException
from config import settings
logger = logging.getLogger("builder.service")
# Storage directory for built .bsm files
STORAGE_DIR = Path(__file__).parent.parent / "storage" / "built_melodies"
# Storage directory for built .bsm files — configurable via BUILT_MELODIES_STORAGE_PATH env var
STORAGE_DIR = Path(settings.built_melodies_storage_path)
def _ensure_storage_dir():

View File

@@ -24,6 +24,9 @@ class Settings(BaseSettings):
sqlite_db_path: str = "./mqtt_data.db"
mqtt_data_retention_days: int = 90
# Local file storage
built_melodies_storage_path: str = "./storage/built_melodies"
# App
backend_cors_origins: str = '["http://localhost:5173"]'
debug: bool = True

View File

@@ -1,5 +1,5 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from typing import Any, Dict, List, Optional
from enum import Enum
@@ -133,9 +133,10 @@ class DeviceUpdate(BaseModel):
device_photo: Optional[str] = None
device_location: Optional[str] = None
is_Online: Optional[bool] = None
device_attributes: Optional[DeviceAttributes] = None
device_subscription: Optional[DeviceSubInformation] = None
device_stats: Optional[DeviceStatistics] = 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
events_on: Optional[bool] = None
device_location_coordinates: Optional[str] = None
device_melodies_all: Optional[List[MelodyMainItem]] = None

View File

@@ -15,6 +15,33 @@ COLLECTION = "devices"
SN_CHARS = string.ascii_uppercase + string.digits
SN_SEGMENT_LEN = 4
# Clock/silence/backlight fields stored as Firestore Timestamps (written as datetime)
_TIMESTAMP_FIELD_NAMES = {
"daySilenceFrom", "daySilenceTo",
"nightSilenceFrom", "nightSilenceTo",
"backlightTurnOnTime", "backlightTurnOffTime",
}
def _restore_timestamps(d: dict) -> dict:
"""Recursively convert ISO 8601 strings for known timestamp fields to datetime objects.
Firestore stores Python datetime objects as native Timestamps, which Flutter
reads as DateTime. Plain strings would break the Flutter app.
"""
result = {}
for k, v in d.items():
if isinstance(v, dict):
result[k] = _restore_timestamps(v)
elif isinstance(v, str) and k in _TIMESTAMP_FIELD_NAMES:
try:
result[k] = datetime.fromisoformat(v.replace("Z", "+00:00"))
except ValueError:
result[k] = v
else:
result[k] = v
return result
def _generate_serial_number() -> str:
"""Generate a unique serial number in the format BS-XXXX-XXXX."""
@@ -139,6 +166,17 @@ def create_device(data: DeviceCreate) -> DeviceInDB:
return DeviceInDB(id=doc_ref.id, **doc_data)
def _deep_merge(base: dict, overrides: dict) -> dict:
"""Recursively merge overrides into base, preserving unmentioned nested keys."""
result = dict(base)
for k, v in overrides.items():
if isinstance(v, dict) and isinstance(result.get(k), dict):
result[k] = _deep_merge(result[k], v)
else:
result[k] = v
return result
def update_device(device_doc_id: str, data: DeviceUpdate) -> DeviceInDB:
"""Update an existing device document. Only provided fields are updated."""
db = get_db()
@@ -149,16 +187,16 @@ def update_device(device_doc_id: str, data: DeviceUpdate) -> DeviceInDB:
update_data = data.model_dump(exclude_none=True)
# For nested structs, merge with existing data rather than replacing
# Deep-merge nested structs so unmentioned sub-fields are preserved
existing = doc.to_dict()
nested_keys = (
"device_attributes", "device_subscription", "device_stats",
)
for key in nested_keys:
if key in update_data and key in existing:
merged = {**existing[key], **update_data[key]}
update_data[key] = merged
if key in update_data and isinstance(existing.get(key), dict):
update_data[key] = _deep_merge(existing[key], update_data[key])
update_data = _restore_timestamps(update_data)
doc_ref.update(update_data)
updated_doc = doc_ref.get()

Binary file not shown.