feat(ota): support forced firmware dispatch
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user