First Production Push
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user