feat(ota): support forced firmware dispatch

This commit is contained in:
stu2not
2026-09-22 14:58:53 +08:00
parent 2467656806
commit e5ba92ba13
10 changed files with 401 additions and 27 deletions
@@ -1,8 +1,10 @@
from datetime import datetime, timedelta, timezone
from typing import Dict, Optional
import time
from collections.abc import Mapping
from sqlalchemy import select, update, insert, delete, text
from sqlalchemy.exc import IntegrityError
from database.models import DeviceFirmwareUpdate
from services.database_service_base import DatabaseServiceBase
from utils.logger import session_logger
@@ -44,6 +46,80 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
"updated_at": row.updated_at,
}
@staticmethod
def _can_claim_forced_update(record, target_version: str, retry_seconds: int) -> bool:
if record is None:
return True
status = str(record.update_status or "").strip().lower()
target = record.target_version or record.firmware_version
if target != target_version:
return True
if status in {"sent", "accepted", "updating"}:
return False
updated_at = record.updated_at
if not isinstance(updated_at, datetime):
return False
now = datetime.now(timezone.utc) if updated_at.tzinfo else datetime.now()
return now - updated_at >= timedelta(seconds=retry_seconds)
async def claim_forced_update(
self,
*,
device_id: str,
target_version: str,
firmware_url: str,
firmware_id: int | None,
ota_channel: str,
retry_seconds: int,
) -> bool:
"""Atomically reserve one MQTT send per device and retry window."""
db_session = await self.get_session()
try:
result = await db_session.execute(
select(DeviceFirmwareUpdate)
.where(DeviceFirmwareUpdate.device_id == device_id)
.with_for_update()
)
record = result.scalar_one_or_none()
if not self._can_claim_forced_update(record, target_version, retry_seconds):
return False
values = {
"firmware_version": target_version,
"target_version": target_version,
"firmware_url": firmware_url,
"firmware_id": firmware_id,
"ota_channel": ota_channel,
"update_status": "sent",
"progress": 0.0,
"source": "forced",
}
if record:
await db_session.execute(
update(DeviceFirmwareUpdate)
.where(DeviceFirmwareUpdate.device_id == device_id)
.values(**values)
)
else:
serial_number = await self._get_serial_number(db_session, device_id)
await db_session.execute(
insert(DeviceFirmwareUpdate).values(
device_id=device_id, serial_number=serial_number, **values
)
)
await db_session.commit()
self._clear_cache(device_id)
return True
except IntegrityError:
await db_session.rollback()
return False
except Exception as exc:
await db_session.rollback()
session_logger.error(device_id, "firmware_update", f"强制 OTA 抢占发送资格失败: {exc}")
return False
finally:
await db_session.close()
async def create_firmware_update(
self,
device_id: str,