支持设备定时定位上报与OTA通道选择
This commit is contained in:
@@ -151,6 +151,11 @@ CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
|||||||
`serial_number` VARCHAR(64) NOT NULL,
|
`serial_number` VARCHAR(64) NOT NULL,
|
||||||
`mac_address` VARCHAR(512) NULL,
|
`mac_address` VARCHAR(512) NULL,
|
||||||
`firmware_version` VARCHAR(64) NOT NULL,
|
`firmware_version` VARCHAR(64) NOT NULL,
|
||||||
|
`ota_channel` VARCHAR(32) NULL,
|
||||||
|
`target_version` VARCHAR(64) NULL,
|
||||||
|
`firmware_url` TEXT NULL,
|
||||||
|
`firmware_id` INT NULL,
|
||||||
|
`source` VARCHAR(32) NULL,
|
||||||
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
||||||
`progress` FLOAT NULL DEFAULT 0.0,
|
`progress` FLOAT NULL DEFAULT 0.0,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -159,6 +164,38 @@ CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
|||||||
UNIQUE INDEX `device_id_UNIQUE` (`device_id` ASC)
|
UNIQUE INDEX `device_id_UNIQUE` (`device_id` ASC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `device_ota_channels` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`channel_key` VARCHAR(32) NOT NULL,
|
||||||
|
`name` VARCHAR(64) NOT NULL,
|
||||||
|
`device_type` TINYINT NULL,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_device_ota_channels_key` (`channel_key`),
|
||||||
|
KEY `idx_device_ota_channels_device_type` (`device_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `device_ota_firmwares` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`channel_key` VARCHAR(32) NOT NULL,
|
||||||
|
`version` VARCHAR(64) NOT NULL,
|
||||||
|
`firmware_url` TEXT NOT NULL,
|
||||||
|
`checksum` VARCHAR(128) NULL,
|
||||||
|
`file_size` BIGINT NULL,
|
||||||
|
`release_status` VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||||
|
`release_channel` VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||||
|
`is_latest` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`published_at` DATETIME NULL,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_device_ota_firmware_channel_version` (`channel_key`, `version`),
|
||||||
|
KEY `idx_device_ota_firmware_latest` (`channel_key`, `release_status`, `is_latest`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `system_config` (
|
CREATE TABLE IF NOT EXISTS `system_config` (
|
||||||
`id` INT NOT NULL AUTO_INCREMENT,
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
`config_key` VARCHAR(128) NOT NULL,
|
`config_key` VARCHAR(128) NOT NULL,
|
||||||
@@ -180,6 +217,13 @@ VALUES
|
|||||||
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
||||||
ON DUPLICATE KEY UPDATE `config_value` = `config_value`;
|
ON DUPLICATE KEY UPDATE `config_value` = `config_value`;
|
||||||
|
|
||||||
|
INSERT INTO `device_ota_channels` (`channel_key`, `name`, `device_type`, `enabled`)
|
||||||
|
VALUES
|
||||||
|
('legacy', 'Legacy devices', NULL, 1),
|
||||||
|
('no_gps', 'NFC without GPS', 1, 1),
|
||||||
|
('gps', 'NFC with GPS', 2, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE `channel_key` = `channel_key`;
|
||||||
|
|
||||||
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
||||||
VALUES (
|
VALUES (
|
||||||
'assistant',
|
'assistant',
|
||||||
@@ -435,6 +479,9 @@ CREATE TABLE IF NOT EXISTS `device_settings` (
|
|||||||
`power` TINYINT UNSIGNED NULL,
|
`power` TINYINT UNSIGNED NULL,
|
||||||
`signal` TINYINT UNSIGNED NULL,
|
`signal` TINYINT UNSIGNED NULL,
|
||||||
`version` VARCHAR(64) NULL,
|
`version` VARCHAR(64) NULL,
|
||||||
|
`imei` VARCHAR(64) NULL,
|
||||||
|
`device_type` TINYINT NULL,
|
||||||
|
`ota_channel` VARCHAR(32) NULL,
|
||||||
`disable_weekdays` VARCHAR(32) NULL,
|
`disable_weekdays` VARCHAR(32) NULL,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ class DeviceDAO(BaseDAO):
|
|||||||
ds.volume,
|
ds.volume,
|
||||||
ds.`signal` AS signal_strength,
|
ds.`signal` AS signal_strength,
|
||||||
ds.`version` AS version,
|
ds.`version` AS version,
|
||||||
|
ds.imei,
|
||||||
|
ds.device_type,
|
||||||
|
ds.ota_channel,
|
||||||
ds.updated_at AS settings_updated_at,
|
ds.updated_at AS settings_updated_at,
|
||||||
cl.coord_type,
|
cl.coord_type,
|
||||||
cl.lat,
|
cl.lat,
|
||||||
|
|||||||
@@ -23,16 +23,19 @@ class DeviceSettingDAO(BaseDAO):
|
|||||||
signal: Optional[int] = None,
|
signal: Optional[int] = None,
|
||||||
version: Optional[str] = None,
|
version: Optional[str] = None,
|
||||||
power: Optional[int] = None,
|
power: Optional[int] = None,
|
||||||
|
imei: Optional[str] = None,
|
||||||
|
device_type: Optional[int] = None,
|
||||||
|
ota_channel: Optional[str] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
result = await self.execute(
|
result = await self.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO device_settings (
|
INSERT INTO device_settings (
|
||||||
device_id, sleep_mode, manual_sleep_mode, schedule_suppressed_until, disable_time_start, disable_time_end,
|
device_id, sleep_mode, manual_sleep_mode, schedule_suppressed_until, disable_time_start, disable_time_end,
|
||||||
timezone, volume, brightness, disable_weekdays, `signal`, `version`, power
|
timezone, volume, brightness, disable_weekdays, `signal`, `version`, power, imei, device_type, ota_channel
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
:device_id, :sleep_mode, :manual_sleep_mode, :schedule_suppressed_until, :disable_time_start, :disable_time_end,
|
:device_id, :sleep_mode, :manual_sleep_mode, :schedule_suppressed_until, :disable_time_start, :disable_time_end,
|
||||||
:timezone, :volume, :brightness, :disable_weekdays, :signal, :version, :power
|
:timezone, :volume, :brightness, :disable_weekdays, :signal, :version, :power, :imei, :device_type, :ota_channel
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
@@ -49,6 +52,9 @@ class DeviceSettingDAO(BaseDAO):
|
|||||||
"signal": signal,
|
"signal": signal,
|
||||||
"version": version,
|
"version": version,
|
||||||
"power": power,
|
"power": power,
|
||||||
|
"imei": imei,
|
||||||
|
"device_type": device_type,
|
||||||
|
"ota_channel": ota_channel,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await self.commit()
|
await self.commit()
|
||||||
@@ -86,6 +92,9 @@ class DeviceSettingDAO(BaseDAO):
|
|||||||
signal: Optional[int] = None,
|
signal: Optional[int] = None,
|
||||||
version: Optional[str] = None,
|
version: Optional[str] = None,
|
||||||
power: Optional[int] = None,
|
power: Optional[int] = None,
|
||||||
|
imei: Optional[str] = None,
|
||||||
|
device_type: Optional[int] = None,
|
||||||
|
ota_channel: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.execute(
|
await self.execute(
|
||||||
"""
|
"""
|
||||||
@@ -105,7 +114,10 @@ class DeviceSettingDAO(BaseDAO):
|
|||||||
disable_weekdays = COALESCE(:disable_weekdays, disable_weekdays),
|
disable_weekdays = COALESCE(:disable_weekdays, disable_weekdays),
|
||||||
`signal` = COALESCE(:signal, `signal`),
|
`signal` = COALESCE(:signal, `signal`),
|
||||||
`version` = COALESCE(:version, `version`),
|
`version` = COALESCE(:version, `version`),
|
||||||
power = COALESCE(:power, power)
|
power = COALESCE(:power, power),
|
||||||
|
imei = COALESCE(:imei, imei),
|
||||||
|
device_type = COALESCE(:device_type, device_type),
|
||||||
|
ota_channel = COALESCE(:ota_channel, ota_channel)
|
||||||
WHERE device_id = :device_id
|
WHERE device_id = :device_id
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
@@ -123,6 +135,9 @@ class DeviceSettingDAO(BaseDAO):
|
|||||||
"signal": signal,
|
"signal": signal,
|
||||||
"version": version,
|
"version": version,
|
||||||
"power": power,
|
"power": power,
|
||||||
|
"imei": imei,
|
||||||
|
"device_type": device_type,
|
||||||
|
"ota_channel": ota_channel,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await self.commit()
|
await self.commit()
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ class DeviceStatusResponse(BaseModel):
|
|||||||
volume: int | None = None
|
volume: int | None = None
|
||||||
signal: int | None = None
|
signal: int | None = None
|
||||||
version: str | None = None
|
version: str | None = None
|
||||||
|
imei: str | None = None
|
||||||
|
device_type: int | None = None
|
||||||
|
ota_channel: str | None = None
|
||||||
settings_updated_at: datetime | None = None
|
settings_updated_at: datetime | None = None
|
||||||
coord_type: str | None = None
|
coord_type: str | None = None
|
||||||
lat: float | None = None
|
lat: float | None = None
|
||||||
@@ -182,6 +185,9 @@ class DeviceFirmwareStatusResponse(BaseModel):
|
|||||||
device_id: str
|
device_id: str
|
||||||
current_version: str | None = None
|
current_version: str | None = None
|
||||||
latest_version: str | None = None
|
latest_version: str | None = None
|
||||||
|
ota_channel: str | None = None
|
||||||
|
firmware_id: int | None = None
|
||||||
|
firmware_url: str | None = None
|
||||||
update_available: bool
|
update_available: bool
|
||||||
can_update: bool
|
can_update: bool
|
||||||
update_status: str
|
update_status: str
|
||||||
@@ -306,6 +312,9 @@ def _row_to_device_status_response(row: Mapping) -> DeviceStatusResponse:
|
|||||||
volume=row["volume"],
|
volume=row["volume"],
|
||||||
signal=row["signal_strength"],
|
signal=row["signal_strength"],
|
||||||
version=row["version"],
|
version=row["version"],
|
||||||
|
imei=row.get("imei"),
|
||||||
|
device_type=row.get("device_type"),
|
||||||
|
ota_channel=row.get("ota_channel"),
|
||||||
settings_updated_at=row["settings_updated_at"],
|
settings_updated_at=row["settings_updated_at"],
|
||||||
coord_type=row["coord_type"],
|
coord_type=row["coord_type"],
|
||||||
lat=float(row["lat"]) if row["lat"] is not None else None,
|
lat=float(row["lat"]) if row["lat"] is not None else None,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from fastapi import HTTPException
|
|||||||
|
|
||||||
from banban.dao.device import DeviceDAO
|
from banban.dao.device import DeviceDAO
|
||||||
from banban.service.device_setting import device_setting_service
|
from banban.service.device_setting import device_setting_service
|
||||||
|
from banban.service.ota_firmware import ota_firmware_service
|
||||||
from services.device_update_manager import device_firmware_update_manager
|
from services.device_update_manager import device_firmware_update_manager
|
||||||
from services.system_config_manager import system_config_manager
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE_COMMAND_SLEEP_BLOCKED_DETAIL = "设备不在线或在休眠中"
|
DEVICE_COMMAND_SLEEP_BLOCKED_DETAIL = "设备不在线或在休眠中"
|
||||||
@@ -310,10 +310,9 @@ class DeviceService(DatabaseServiceBase):
|
|||||||
status_row = await self.get_device_status(device_id=device_id, user_id=user_id)
|
status_row = await self.get_device_status(device_id=device_id, user_id=user_id)
|
||||||
current_version = status_row.get("version")
|
current_version = status_row.get("version")
|
||||||
|
|
||||||
latest_version_config = await system_config_manager.get_config("latest_firmware_version")
|
firmware_selection = await ota_firmware_service.select_latest_for_device(status_row)
|
||||||
firmware_url_config = await system_config_manager.get_config("update_firmware_url")
|
latest_version = firmware_selection.version if firmware_selection else None
|
||||||
latest_version = latest_version_config.config_value if latest_version_config else None
|
firmware_url = firmware_selection.firmware_url if firmware_selection else None
|
||||||
firmware_url = firmware_url_config.config_value if firmware_url_config else None
|
|
||||||
update_available = self._compare_versions(current_version, latest_version)
|
update_available = self._compare_versions(current_version, latest_version)
|
||||||
|
|
||||||
update_record = await device_firmware_update_manager.get_firmware_update_dict(device_id)
|
update_record = await device_firmware_update_manager.get_firmware_update_dict(device_id)
|
||||||
@@ -332,6 +331,9 @@ class DeviceService(DatabaseServiceBase):
|
|||||||
"target_version": target_version,
|
"target_version": target_version,
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
"can_update": bool(update_available and latest_version and firmware_url),
|
"can_update": bool(update_available and latest_version and firmware_url),
|
||||||
|
"ota_channel": firmware_selection.channel_key if firmware_selection else None,
|
||||||
|
"firmware_id": firmware_selection.firmware_id if firmware_selection else None,
|
||||||
|
"firmware_url": firmware_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def start_firmware_update(
|
async def start_firmware_update(
|
||||||
@@ -348,8 +350,7 @@ class DeviceService(DatabaseServiceBase):
|
|||||||
if not latest_version:
|
if not latest_version:
|
||||||
raise HTTPException(status_code=404, detail="latest firmware version not configured")
|
raise HTTPException(status_code=404, detail="latest firmware version not configured")
|
||||||
|
|
||||||
firmware_url_config = await system_config_manager.get_config("update_firmware_url")
|
firmware_url = firmware_status.get("firmware_url")
|
||||||
firmware_url = firmware_url_config.config_value if firmware_url_config else None
|
|
||||||
if not firmware_url:
|
if not firmware_url:
|
||||||
raise HTTPException(status_code=404, detail="firmware url not configured")
|
raise HTTPException(status_code=404, detail="firmware url not configured")
|
||||||
|
|
||||||
@@ -364,6 +365,11 @@ class DeviceService(DatabaseServiceBase):
|
|||||||
firmware_version=latest_version,
|
firmware_version=latest_version,
|
||||||
update_status="sent",
|
update_status="sent",
|
||||||
progress=0.0,
|
progress=0.0,
|
||||||
|
ota_channel=firmware_status.get("ota_channel"),
|
||||||
|
target_version=latest_version,
|
||||||
|
firmware_url=firmware_url,
|
||||||
|
firmware_id=firmware_status.get("firmware_id"),
|
||||||
|
source="manual",
|
||||||
)
|
)
|
||||||
msg_id = await service.send_ota_command(device_id, firmware_url, latest_version)
|
msg_id = await service.send_ota_command(device_id, firmware_url, latest_version)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ from services.database_service_base import DatabaseServiceBase
|
|||||||
from banban.dao.device_setting import DeviceSettingDAO
|
from banban.dao.device_setting import DeviceSettingDAO
|
||||||
|
|
||||||
|
|
||||||
|
def infer_ota_channel_from_device_type(device_type: int | None) -> str | None:
|
||||||
|
if device_type == 2:
|
||||||
|
return "gps"
|
||||||
|
if device_type == 1:
|
||||||
|
return "no_gps"
|
||||||
|
if device_type == 0:
|
||||||
|
return "legacy"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_device_type(device_type: object) -> int | None:
|
||||||
|
if device_type is None or device_type == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = int(device_type)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if value in (0, 1, 2):
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class DeviceSettingService(DatabaseServiceBase):
|
class DeviceSettingService(DatabaseServiceBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(service_name="device_setting_service")
|
super().__init__(service_name="device_setting_service")
|
||||||
@@ -26,6 +48,9 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
disable_weekdays: Optional[str] = None,
|
disable_weekdays: Optional[str] = None,
|
||||||
signal_strength: Optional[int] = None,
|
signal_strength: Optional[int] = None,
|
||||||
version_str: Optional[str] = None,
|
version_str: Optional[str] = None,
|
||||||
|
imei: Optional[str] = None,
|
||||||
|
device_type: Optional[int] = None,
|
||||||
|
ota_channel: Optional[str] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
db_session = await self.get_session()
|
db_session = await self.get_session()
|
||||||
try:
|
try:
|
||||||
@@ -44,6 +69,9 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
disable_weekdays=disable_weekdays,
|
disable_weekdays=disable_weekdays,
|
||||||
signal=signal_strength,
|
signal=signal_strength,
|
||||||
version=version_str,
|
version=version_str,
|
||||||
|
imei=imei,
|
||||||
|
device_type=device_type,
|
||||||
|
ota_channel=ota_channel,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await db_session.close()
|
await db_session.close()
|
||||||
@@ -72,6 +100,9 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
disable_weekdays: Optional[str] = None,
|
disable_weekdays: Optional[str] = None,
|
||||||
signal_strength: Optional[int] = None,
|
signal_strength: Optional[int] = None,
|
||||||
version_str: Optional[str] = None,
|
version_str: Optional[str] = None,
|
||||||
|
imei: Optional[str] = None,
|
||||||
|
device_type: Optional[int] = None,
|
||||||
|
ota_channel: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
db_session = await self.get_session()
|
db_session = await self.get_session()
|
||||||
try:
|
try:
|
||||||
@@ -91,6 +122,9 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
power=power,
|
power=power,
|
||||||
signal=signal_strength,
|
signal=signal_strength,
|
||||||
version=version_str,
|
version=version_str,
|
||||||
|
imei=imei,
|
||||||
|
device_type=device_type,
|
||||||
|
ota_channel=ota_channel,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await db_session.close()
|
await db_session.close()
|
||||||
@@ -110,13 +144,39 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
volume: Optional[int],
|
volume: Optional[int],
|
||||||
signal_strength: Optional[int],
|
signal_strength: Optional[int],
|
||||||
version_str: Optional[str],
|
version_str: Optional[str],
|
||||||
|
imei: Optional[str] = None,
|
||||||
|
device_type: Optional[int] = None,
|
||||||
|
ota_channel: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
|
normalized_device_type = normalize_device_type(device_type)
|
||||||
current_row = await self.get_setting_by_device_id(device_id=device_id)
|
current_row = await self.get_setting_by_device_id(device_id=device_id)
|
||||||
if current_row:
|
if current_row:
|
||||||
await self.update_setting(device_id=device_id, power=power, volume=volume, signal_strength=signal_strength, version_str=version_str)
|
resolved_ota_channel = ota_channel
|
||||||
|
if not resolved_ota_channel and not current_row.get("ota_channel"):
|
||||||
|
resolved_ota_channel = infer_ota_channel_from_device_type(normalized_device_type)
|
||||||
|
await self.update_setting(
|
||||||
|
device_id=device_id,
|
||||||
|
power=power,
|
||||||
|
volume=volume,
|
||||||
|
signal_strength=signal_strength,
|
||||||
|
version_str=version_str,
|
||||||
|
imei=imei,
|
||||||
|
device_type=normalized_device_type,
|
||||||
|
ota_channel=resolved_ota_channel,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.create_setting(device_id=device_id, power=power, volume=volume, signal_strength=signal_strength, version_str=version_str)
|
resolved_ota_channel = ota_channel or infer_ota_channel_from_device_type(normalized_device_type)
|
||||||
|
await self.create_setting(
|
||||||
|
device_id=device_id,
|
||||||
|
power=power,
|
||||||
|
volume=volume,
|
||||||
|
signal_strength=signal_strength,
|
||||||
|
version_str=version_str,
|
||||||
|
imei=imei,
|
||||||
|
device_type=normalized_device_type,
|
||||||
|
ota_channel=resolved_ota_channel,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
113
talkingq-url/banban/service/ota_firmware.py
Normal file
113
talkingq-url/banban/service/ota_firmware.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from services.database_service_base import DatabaseServiceBase
|
||||||
|
from services.system_config_manager import system_config_manager
|
||||||
|
|
||||||
|
|
||||||
|
LEGACY_OTA_CHANNEL = "legacy"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OtaFirmwareSelection:
|
||||||
|
channel_key: str
|
||||||
|
version: str
|
||||||
|
firmware_url: str
|
||||||
|
firmware_id: int | None = None
|
||||||
|
source: str = "channel"
|
||||||
|
|
||||||
|
|
||||||
|
class OtaFirmwareService(DatabaseServiceBase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(service_name="ota_firmware_service")
|
||||||
|
|
||||||
|
def infer_channel_from_device_type(self, device_type: Any) -> str | None:
|
||||||
|
if device_type is None or device_type == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = int(device_type)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if value == 2:
|
||||||
|
return "gps"
|
||||||
|
if value == 1:
|
||||||
|
return "no_gps"
|
||||||
|
if value == 0:
|
||||||
|
return LEGACY_OTA_CHANNEL
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def resolve_channel_for_status(self, status_row: Mapping[str, Any]) -> str | None:
|
||||||
|
configured_channel = str(status_row.get("ota_channel") or "").strip()
|
||||||
|
if configured_channel:
|
||||||
|
return configured_channel
|
||||||
|
return self.infer_channel_from_device_type(status_row.get("device_type"))
|
||||||
|
|
||||||
|
async def get_latest_firmware_for_channel(self, channel_key: str) -> OtaFirmwareSelection | None:
|
||||||
|
db_session = await self.get_session()
|
||||||
|
try:
|
||||||
|
result = await db_session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
f.id,
|
||||||
|
f.channel_key,
|
||||||
|
f.version,
|
||||||
|
f.firmware_url
|
||||||
|
FROM device_ota_firmwares AS f
|
||||||
|
JOIN device_ota_channels AS c
|
||||||
|
ON c.channel_key = f.channel_key
|
||||||
|
WHERE f.channel_key = :channel_key
|
||||||
|
AND f.release_status = 'published'
|
||||||
|
AND f.is_latest = 1
|
||||||
|
AND c.enabled = 1
|
||||||
|
ORDER BY f.published_at DESC, f.updated_at DESC, f.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"channel_key": channel_key},
|
||||||
|
)
|
||||||
|
row = result.mappings().first()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
return OtaFirmwareSelection(
|
||||||
|
firmware_id=int(row["id"]),
|
||||||
|
channel_key=str(row["channel_key"]),
|
||||||
|
version=str(row["version"]),
|
||||||
|
firmware_url=str(row["firmware_url"]),
|
||||||
|
source="channel",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await db_session.close()
|
||||||
|
|
||||||
|
async def get_legacy_firmware(self) -> OtaFirmwareSelection | None:
|
||||||
|
latest_version_config = await system_config_manager.get_config("latest_firmware_version")
|
||||||
|
firmware_url_config = await system_config_manager.get_config("update_firmware_url")
|
||||||
|
latest_version = latest_version_config.config_value if latest_version_config else None
|
||||||
|
firmware_url = firmware_url_config.config_value if firmware_url_config else None
|
||||||
|
if not latest_version or not firmware_url:
|
||||||
|
return None
|
||||||
|
return OtaFirmwareSelection(
|
||||||
|
channel_key=LEGACY_OTA_CHANNEL,
|
||||||
|
version=str(latest_version),
|
||||||
|
firmware_url=str(firmware_url),
|
||||||
|
source="legacy_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def select_latest_for_device(self, status_row: Mapping[str, Any]) -> OtaFirmwareSelection | None:
|
||||||
|
channel_key = await self.resolve_channel_for_status(status_row)
|
||||||
|
if not channel_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
selection = await self.get_latest_firmware_for_channel(channel_key)
|
||||||
|
if selection:
|
||||||
|
return selection
|
||||||
|
|
||||||
|
if channel_key == LEGACY_OTA_CHANNEL:
|
||||||
|
return await self.get_legacy_firmware()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
ota_firmware_service = OtaFirmwareService()
|
||||||
@@ -94,6 +94,89 @@ async def _ensure_device_alarm_location_columns(conn) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_device_settings_metadata_columns(conn) -> None:
|
||||||
|
await _ensure_columns(
|
||||||
|
conn,
|
||||||
|
table_name="device_settings",
|
||||||
|
columns=[
|
||||||
|
("imei", "imei VARCHAR(64) NULL AFTER `version`"),
|
||||||
|
("device_type", "device_type TINYINT NULL AFTER imei"),
|
||||||
|
("ota_channel", "ota_channel VARCHAR(32) NULL AFTER device_type"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_device_firmware_update_ota_columns(conn) -> None:
|
||||||
|
await _ensure_columns(
|
||||||
|
conn,
|
||||||
|
table_name="device_firmware_update",
|
||||||
|
columns=[
|
||||||
|
("ota_channel", "ota_channel VARCHAR(32) NULL AFTER firmware_version"),
|
||||||
|
("target_version", "target_version VARCHAR(64) NULL AFTER ota_channel"),
|
||||||
|
("firmware_url", "firmware_url TEXT NULL AFTER target_version"),
|
||||||
|
("firmware_id", "firmware_id INT NULL AFTER firmware_url"),
|
||||||
|
("source", "source VARCHAR(32) NULL AFTER firmware_id"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_device_ota_tables(conn) -> None:
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS device_ota_channels (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
channel_key VARCHAR(32) NOT NULL,
|
||||||
|
name VARCHAR(64) NOT NULL,
|
||||||
|
device_type TINYINT NULL,
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_device_ota_channels_key (channel_key),
|
||||||
|
KEY idx_device_ota_channels_device_type (device_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS device_ota_firmwares (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
channel_key VARCHAR(32) NOT NULL,
|
||||||
|
version VARCHAR(64) NOT NULL,
|
||||||
|
firmware_url TEXT NOT NULL,
|
||||||
|
checksum VARCHAR(128) NULL,
|
||||||
|
file_size BIGINT NULL,
|
||||||
|
release_status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||||
|
release_channel VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||||
|
is_latest TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
notes TEXT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
published_at DATETIME NULL,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_device_ota_firmware_channel_version (channel_key, version),
|
||||||
|
KEY idx_device_ota_firmware_latest (channel_key, release_status, is_latest)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
INSERT INTO device_ota_channels (channel_key, name, device_type, enabled)
|
||||||
|
VALUES
|
||||||
|
('legacy', 'Legacy devices', NULL, 1),
|
||||||
|
('no_gps', 'NFC without GPS', 1, 1),
|
||||||
|
('gps', 'NFC with GPS', 2, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE channel_key = channel_key
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_child_location_address_columns(conn) -> None:
|
async def _ensure_child_location_address_columns(conn) -> None:
|
||||||
await _ensure_columns(
|
await _ensure_columns(
|
||||||
conn,
|
conn,
|
||||||
@@ -283,6 +366,9 @@ async def init_db():
|
|||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
await _ensure_manual_sleep_mode_column(conn)
|
await _ensure_manual_sleep_mode_column(conn)
|
||||||
await _ensure_schedule_suppressed_until_column(conn)
|
await _ensure_schedule_suppressed_until_column(conn)
|
||||||
|
await _ensure_device_settings_metadata_columns(conn)
|
||||||
|
await _ensure_device_firmware_update_ota_columns(conn)
|
||||||
|
await _ensure_device_ota_tables(conn)
|
||||||
await _ensure_device_alarm_location_columns(conn)
|
await _ensure_device_alarm_location_columns(conn)
|
||||||
await _ensure_child_location_address_columns(conn)
|
await _ensure_child_location_address_columns(conn)
|
||||||
await _ensure_bind_session_card_columns(conn)
|
await _ensure_bind_session_card_columns(conn)
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ class DeviceFirmwareUpdate(Base):
|
|||||||
serial_number = Column(String(64), nullable=False, default="")
|
serial_number = Column(String(64), nullable=False, default="")
|
||||||
mac_address = Column(String(512), nullable=True)
|
mac_address = Column(String(512), nullable=True)
|
||||||
firmware_version = Column(String(64), nullable=False)
|
firmware_version = Column(String(64), nullable=False)
|
||||||
|
ota_channel = Column(String(32), nullable=True)
|
||||||
|
target_version = Column(String(64), nullable=True)
|
||||||
|
firmware_url = Column(Text, nullable=True)
|
||||||
|
firmware_id = Column(Integer, nullable=True)
|
||||||
|
source = Column(String(32), nullable=True)
|
||||||
update_status = Column(String(32), nullable=False, default="success") # 更新状态,如 updating/success/failed
|
update_status = Column(String(32), nullable=False, default="success") # 更新状态,如 updating/success/failed
|
||||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||||
@@ -137,6 +142,44 @@ class DeviceFirmwareUpdate(Base):
|
|||||||
{'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'}
|
{'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceOtaChannel(Base):
|
||||||
|
__tablename__ = "device_ota_channels"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
channel_key = Column(String(32), unique=True, nullable=False, index=True)
|
||||||
|
name = Column(String(64), nullable=False)
|
||||||
|
device_type = Column(Integer, nullable=True, index=True)
|
||||||
|
enabled = Column(Boolean, nullable=False, default=True)
|
||||||
|
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
|
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
{'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceOtaFirmware(Base):
|
||||||
|
__tablename__ = "device_ota_firmwares"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
channel_key = Column(String(32), nullable=False, index=True)
|
||||||
|
version = Column(String(64), nullable=False)
|
||||||
|
firmware_url = Column(Text, nullable=False)
|
||||||
|
checksum = Column(String(128), nullable=True)
|
||||||
|
file_size = Column(BigInteger, nullable=True)
|
||||||
|
release_status = Column(String(32), nullable=False, default="draft", index=True)
|
||||||
|
release_channel = Column(String(32), nullable=False, default="stable")
|
||||||
|
is_latest = Column(Boolean, nullable=False, default=False, index=True)
|
||||||
|
notes = Column(Text, nullable=True)
|
||||||
|
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
|
published_at = Column(DateTime, nullable=True)
|
||||||
|
updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("channel_key", "version", name="uq_device_ota_firmware_channel_version"),
|
||||||
|
Index("idx_device_ota_firmware_latest", "channel_key", "release_status", "is_latest"),
|
||||||
|
{'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'}
|
||||||
|
)
|
||||||
|
|
||||||
class SystemConfig(Base):
|
class SystemConfig(Base):
|
||||||
__tablename__ = "system_config"
|
__tablename__ = "system_config"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
@@ -325,6 +368,9 @@ class DeviceSetting(Base):
|
|||||||
power: Mapped[Optional[int]] = mapped_column(Integer)
|
power: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
signal: Mapped[Optional[int]] = mapped_column("signal", Integer)
|
signal: Mapped[Optional[int]] = mapped_column("signal", Integer)
|
||||||
version: Mapped[Optional[str]] = mapped_column("version", String(64))
|
version: Mapped[Optional[str]] = mapped_column("version", String(64))
|
||||||
|
imei: Mapped[Optional[str]] = mapped_column(String(64))
|
||||||
|
device_type: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
ota_channel: Mapped[Optional[str]] = mapped_column(String(32))
|
||||||
disable_weekdays: Mapped[Optional[str]] = mapped_column(String(32))
|
disable_weekdays: Mapped[Optional[str]] = mapped_column(String(32))
|
||||||
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
|
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
|
||||||
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
|||||||
@@ -158,7 +158,18 @@ class TalkingQMQTTService:
|
|||||||
signal = data.get("signal")
|
signal = data.get("signal")
|
||||||
version = data.get("version")
|
version = data.get("version")
|
||||||
voice = data.get("voice")
|
voice = data.get("voice")
|
||||||
logger.info(device_id, "", f"[设备信息] 设备 {d_id} 信息: 电量={power}, 信号强度={signal}, 版本号={version}, 音量={voice}")
|
imei = data.get("imei")
|
||||||
|
device_type = data.get("device_type")
|
||||||
|
latitude = data.get("latitude")
|
||||||
|
longitude = data.get("longitude")
|
||||||
|
logger.info(
|
||||||
|
device_id,
|
||||||
|
"",
|
||||||
|
(
|
||||||
|
f"[设备信息] 设备 {d_id} 信息: 电量={power}, 信号强度={signal}, "
|
||||||
|
f"版本号={version}, 音量={voice}, IMEI={imei}, 设备类型={device_type}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await self._schedule_persistence(
|
await self._schedule_persistence(
|
||||||
device_id,
|
device_id,
|
||||||
@@ -169,6 +180,25 @@ class TalkingQMQTTService:
|
|||||||
signal_strength=data.get("signal"),
|
signal_strength=data.get("signal"),
|
||||||
version_str=data.get("version"),
|
version_str=data.get("version"),
|
||||||
volume=data.get("voice"),
|
volume=data.get("voice"),
|
||||||
|
imei=str(imei).strip() if imei is not None and str(imei).strip() else None,
|
||||||
|
device_type=device_type,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if latitude is not None and longitude is not None:
|
||||||
|
await self._schedule_persistence(
|
||||||
|
device_id,
|
||||||
|
"device_info_location",
|
||||||
|
location_service.report_mqtt_device_location(
|
||||||
|
device_id=device_id,
|
||||||
|
latitude=latitude,
|
||||||
|
longitude=longitude,
|
||||||
|
coord_type=data.get("coord_type"),
|
||||||
|
accuracy_m=data.get("accuracy_m"),
|
||||||
|
altitude_m=data.get("altitude_m"),
|
||||||
|
speed_mps=data.get("speed_mps"),
|
||||||
|
heading_deg=data.get("heading_deg"),
|
||||||
|
source=data.get("source", 0),
|
||||||
|
battery_pct=data.get("battery_pct") or data.get("power"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# await self._sync_desired_volume(device_id, current_volume)
|
# await self._sync_desired_volume(device_id, current_volume)
|
||||||
@@ -275,7 +305,7 @@ class TalkingQMQTTService:
|
|||||||
"ota",
|
"ota",
|
||||||
device_firmware_update_manager.update_firmware_update(
|
device_firmware_update_manager.update_firmware_update(
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
firmware_version=data.get("version") or data.get("new_version") or "unknown",
|
firmware_version=data.get("target_version") or data.get("version") or data.get("new_version") or "unknown",
|
||||||
update_status="accepted",
|
update_status="accepted",
|
||||||
progress=progress_value if progress_value is not None else 0.0,
|
progress=progress_value if progress_value is not None else 0.0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
|||||||
`serial_number` VARCHAR(64) NOT NULL,
|
`serial_number` VARCHAR(64) NOT NULL,
|
||||||
`mac_address` VARCHAR(512) NULL,
|
`mac_address` VARCHAR(512) NULL,
|
||||||
`firmware_version` VARCHAR(64) NOT NULL,
|
`firmware_version` VARCHAR(64) NOT NULL,
|
||||||
|
`ota_channel` VARCHAR(32) NULL,
|
||||||
|
`target_version` VARCHAR(64) NULL,
|
||||||
|
`firmware_url` TEXT NULL,
|
||||||
|
`firmware_id` INT NULL,
|
||||||
|
`source` VARCHAR(32) NULL,
|
||||||
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
||||||
`progress` FLOAT NULL DEFAULT 0.0,
|
`progress` FLOAT NULL DEFAULT 0.0,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -124,6 +129,38 @@ CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
|||||||
UNIQUE INDEX `device_id_UNIQUE` (`device_id` ASC)
|
UNIQUE INDEX `device_id_UNIQUE` (`device_id` ASC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `device_ota_channels` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`channel_key` VARCHAR(32) NOT NULL,
|
||||||
|
`name` VARCHAR(64) NOT NULL,
|
||||||
|
`device_type` TINYINT NULL,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_device_ota_channels_key` (`channel_key`),
|
||||||
|
KEY `idx_device_ota_channels_device_type` (`device_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `device_ota_firmwares` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`channel_key` VARCHAR(32) NOT NULL,
|
||||||
|
`version` VARCHAR(64) NOT NULL,
|
||||||
|
`firmware_url` TEXT NOT NULL,
|
||||||
|
`checksum` VARCHAR(128) NULL,
|
||||||
|
`file_size` BIGINT NULL,
|
||||||
|
`release_status` VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||||
|
`release_channel` VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||||
|
`is_latest` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`published_at` DATETIME NULL,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_device_ota_firmware_channel_version` (`channel_key`, `version`),
|
||||||
|
KEY `idx_device_ota_firmware_latest` (`channel_key`, `release_status`, `is_latest`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 创建system_config表
|
-- 创建system_config表
|
||||||
CREATE TABLE IF NOT EXISTS `system_config` (
|
CREATE TABLE IF NOT EXISTS `system_config` (
|
||||||
`id` INT NOT NULL AUTO_INCREMENT,
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
@@ -148,6 +185,13 @@ VALUES
|
|||||||
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
||||||
ON DUPLICATE KEY UPDATE `config_value`=`config_value`;
|
ON DUPLICATE KEY UPDATE `config_value`=`config_value`;
|
||||||
|
|
||||||
|
INSERT INTO `device_ota_channels` (`channel_key`, `name`, `device_type`, `enabled`)
|
||||||
|
VALUES
|
||||||
|
('legacy', 'Legacy devices', NULL, 1),
|
||||||
|
('no_gps', 'NFC without GPS', 1, 1),
|
||||||
|
('gps', 'NFC with GPS', 2, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE `channel_key`=`channel_key`;
|
||||||
|
|
||||||
-- 添加默认角色
|
-- 添加默认角色
|
||||||
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
||||||
VALUES (
|
VALUES (
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
|||||||
`serial_number` VARCHAR(64) NOT NULL,
|
`serial_number` VARCHAR(64) NOT NULL,
|
||||||
`mac_address` VARCHAR(512) NULL,
|
`mac_address` VARCHAR(512) NULL,
|
||||||
`firmware_version` VARCHAR(64) NOT NULL,
|
`firmware_version` VARCHAR(64) NOT NULL,
|
||||||
|
`ota_channel` VARCHAR(32) NULL,
|
||||||
|
`target_version` VARCHAR(64) NULL,
|
||||||
|
`firmware_url` TEXT NULL,
|
||||||
|
`firmware_id` INT NULL,
|
||||||
|
`source` VARCHAR(32) NULL,
|
||||||
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
||||||
`progress` FLOAT NULL DEFAULT 0.0,
|
`progress` FLOAT NULL DEFAULT 0.0,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -159,6 +164,38 @@ CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
|||||||
UNIQUE INDEX `device_id_UNIQUE` (`device_id` ASC)
|
UNIQUE INDEX `device_id_UNIQUE` (`device_id` ASC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `device_ota_channels` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`channel_key` VARCHAR(32) NOT NULL,
|
||||||
|
`name` VARCHAR(64) NOT NULL,
|
||||||
|
`device_type` TINYINT NULL,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_device_ota_channels_key` (`channel_key`),
|
||||||
|
KEY `idx_device_ota_channels_device_type` (`device_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `device_ota_firmwares` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`channel_key` VARCHAR(32) NOT NULL,
|
||||||
|
`version` VARCHAR(64) NOT NULL,
|
||||||
|
`firmware_url` TEXT NOT NULL,
|
||||||
|
`checksum` VARCHAR(128) NULL,
|
||||||
|
`file_size` BIGINT NULL,
|
||||||
|
`release_status` VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||||
|
`release_channel` VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||||
|
`is_latest` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`published_at` DATETIME NULL,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_device_ota_firmware_channel_version` (`channel_key`, `version`),
|
||||||
|
KEY `idx_device_ota_firmware_latest` (`channel_key`, `release_status`, `is_latest`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `system_config` (
|
CREATE TABLE IF NOT EXISTS `system_config` (
|
||||||
`id` INT NOT NULL AUTO_INCREMENT,
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
`config_key` VARCHAR(128) NOT NULL,
|
`config_key` VARCHAR(128) NOT NULL,
|
||||||
@@ -180,6 +217,13 @@ VALUES
|
|||||||
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
||||||
ON DUPLICATE KEY UPDATE `config_value` = `config_value`;
|
ON DUPLICATE KEY UPDATE `config_value` = `config_value`;
|
||||||
|
|
||||||
|
INSERT INTO `device_ota_channels` (`channel_key`, `name`, `device_type`, `enabled`)
|
||||||
|
VALUES
|
||||||
|
('legacy', 'Legacy devices', NULL, 1),
|
||||||
|
('no_gps', 'NFC without GPS', 1, 1),
|
||||||
|
('gps', 'NFC with GPS', 2, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE `channel_key` = `channel_key`;
|
||||||
|
|
||||||
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
||||||
VALUES (
|
VALUES (
|
||||||
'assistant',
|
'assistant',
|
||||||
@@ -435,6 +479,9 @@ CREATE TABLE IF NOT EXISTS `device_settings` (
|
|||||||
`power` TINYINT UNSIGNED NULL,
|
`power` TINYINT UNSIGNED NULL,
|
||||||
`signal` TINYINT UNSIGNED NULL,
|
`signal` TINYINT UNSIGNED NULL,
|
||||||
`version` VARCHAR(64) NULL,
|
`version` VARCHAR(64) NULL,
|
||||||
|
`imei` VARCHAR(64) NULL,
|
||||||
|
`device_type` TINYINT NULL,
|
||||||
|
`ota_channel` VARCHAR(32) NULL,
|
||||||
`disable_weekdays` VARCHAR(32) NULL,
|
`disable_weekdays` VARCHAR(32) NULL,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -33,13 +33,30 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
return {
|
return {
|
||||||
"device_id": row.device_id,
|
"device_id": row.device_id,
|
||||||
"firmware_version": row.firmware_version,
|
"firmware_version": row.firmware_version,
|
||||||
|
"ota_channel": getattr(row, "ota_channel", None),
|
||||||
|
"target_version": getattr(row, "target_version", None),
|
||||||
|
"firmware_url": getattr(row, "firmware_url", None),
|
||||||
|
"firmware_id": getattr(row, "firmware_id", None),
|
||||||
|
"source": getattr(row, "source", None),
|
||||||
"update_status": row.update_status,
|
"update_status": row.update_status,
|
||||||
"progress": row.progress,
|
"progress": row.progress,
|
||||||
"created_at": row.created_at,
|
"created_at": row.created_at,
|
||||||
"updated_at": row.updated_at,
|
"updated_at": row.updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def create_firmware_update(self, device_id: str, firmware_version: str, update_status: str = "success", progress: float = 0.0):
|
async def create_firmware_update(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
firmware_version: str,
|
||||||
|
update_status: str = "success",
|
||||||
|
progress: float = 0.0,
|
||||||
|
*,
|
||||||
|
ota_channel: str | None = None,
|
||||||
|
target_version: str | None = None,
|
||||||
|
firmware_url: str | None = None,
|
||||||
|
firmware_id: int | None = None,
|
||||||
|
source: str | None = None,
|
||||||
|
):
|
||||||
"""新增固件更新记录"""
|
"""新增固件更新记录"""
|
||||||
await self._init_database()
|
await self._init_database()
|
||||||
db_session = await self.get_session()
|
db_session = await self.get_session()
|
||||||
@@ -55,7 +72,12 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
.values(
|
.values(
|
||||||
firmware_version=firmware_version,
|
firmware_version=firmware_version,
|
||||||
update_status=update_status,
|
update_status=update_status,
|
||||||
progress=progress
|
progress=progress,
|
||||||
|
ota_channel=ota_channel,
|
||||||
|
target_version=target_version,
|
||||||
|
firmware_url=firmware_url,
|
||||||
|
firmware_id=firmware_id,
|
||||||
|
source=source,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await db_session.execute(update_stmt)
|
await db_session.execute(update_stmt)
|
||||||
@@ -66,7 +88,12 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
serial_number=serial_number,
|
serial_number=serial_number,
|
||||||
firmware_version=firmware_version,
|
firmware_version=firmware_version,
|
||||||
update_status=update_status,
|
update_status=update_status,
|
||||||
progress=progress
|
progress=progress,
|
||||||
|
ota_channel=ota_channel,
|
||||||
|
target_version=target_version,
|
||||||
|
firmware_url=firmware_url,
|
||||||
|
firmware_id=firmware_id,
|
||||||
|
source=source,
|
||||||
)
|
)
|
||||||
await db_session.execute(insert_stmt)
|
await db_session.execute(insert_stmt)
|
||||||
|
|
||||||
@@ -75,6 +102,11 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
self._update_cache(device_id, {
|
self._update_cache(device_id, {
|
||||||
"device_id": device_id,
|
"device_id": device_id,
|
||||||
"firmware_version": firmware_version,
|
"firmware_version": firmware_version,
|
||||||
|
"ota_channel": ota_channel,
|
||||||
|
"target_version": target_version,
|
||||||
|
"firmware_url": firmware_url,
|
||||||
|
"firmware_id": firmware_id,
|
||||||
|
"source": source,
|
||||||
"update_status": update_status,
|
"update_status": update_status,
|
||||||
"progress": progress
|
"progress": progress
|
||||||
})
|
})
|
||||||
@@ -113,6 +145,12 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
firmware_version: str,
|
firmware_version: str,
|
||||||
update_status: str,
|
update_status: str,
|
||||||
progress: Optional[float] = None,
|
progress: Optional[float] = None,
|
||||||
|
*,
|
||||||
|
ota_channel: str | None = None,
|
||||||
|
target_version: str | None = None,
|
||||||
|
firmware_url: str | None = None,
|
||||||
|
firmware_id: int | None = None,
|
||||||
|
source: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""更新设备固件信息(支持部分字段更新)"""
|
"""更新设备固件信息(支持部分字段更新)"""
|
||||||
await self._init_database()
|
await self._init_database()
|
||||||
@@ -124,6 +162,14 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
}
|
}
|
||||||
if progress is not None:
|
if progress is not None:
|
||||||
update_values["progress"] = progress
|
update_values["progress"] = progress
|
||||||
|
optional_values = {
|
||||||
|
"ota_channel": ota_channel,
|
||||||
|
"target_version": target_version,
|
||||||
|
"firmware_url": firmware_url,
|
||||||
|
"firmware_id": firmware_id,
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
update_values.update({key: value for key, value in optional_values.items() if value is not None})
|
||||||
|
|
||||||
query = select(DeviceFirmwareUpdate).where(DeviceFirmwareUpdate.device_id == device_id)
|
query = select(DeviceFirmwareUpdate).where(DeviceFirmwareUpdate.device_id == device_id)
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
@@ -144,6 +190,11 @@ class DeviceFirmwareUpdateManager(DatabaseServiceBase):
|
|||||||
firmware_version=firmware_version,
|
firmware_version=firmware_version,
|
||||||
update_status=update_status,
|
update_status=update_status,
|
||||||
progress=progress if progress is not None else 0.0,
|
progress=progress if progress is not None else 0.0,
|
||||||
|
ota_channel=ota_channel,
|
||||||
|
target_version=target_version,
|
||||||
|
firmware_url=firmware_url,
|
||||||
|
firmware_id=firmware_id,
|
||||||
|
source=source,
|
||||||
)
|
)
|
||||||
await db_session.execute(insert_stmt)
|
await db_session.execute(insert_stmt)
|
||||||
|
|
||||||
|
|||||||
111
talkingq-url/tests/test_mqtt_device_info_report.py
Normal file
111
talkingq-url/tests/test_mqtt_device_info_report.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from handlers.mqtt_handler import TalkingQMQTTService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_device_info_report_persists_metadata_and_periodic_location(monkeypatch):
|
||||||
|
service = TalkingQMQTTService({"broker": "127.0.0.1"})
|
||||||
|
calls = []
|
||||||
|
published = []
|
||||||
|
|
||||||
|
async def fake_publish(topic, payload):
|
||||||
|
published.append((topic, payload))
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def fake_schedule_persistence(device_id, label, coro):
|
||||||
|
calls.append(("schedule", device_id, label))
|
||||||
|
await coro
|
||||||
|
|
||||||
|
async def fake_insert_or_update(**kwargs):
|
||||||
|
calls.append(("settings", kwargs))
|
||||||
|
|
||||||
|
async def fake_report_mqtt_device_location(**kwargs):
|
||||||
|
calls.append(("location", kwargs))
|
||||||
|
return {"device_id": kwargs["device_id"], "lat": kwargs["latitude"], "lng": kwargs["longitude"]}
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "_publish", fake_publish)
|
||||||
|
monkeypatch.setattr(service, "_schedule_persistence", fake_schedule_persistence)
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.device_setting_service.insert_or_update", fake_insert_or_update)
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.location_service.report_mqtt_device_location", fake_report_mqtt_device_location)
|
||||||
|
|
||||||
|
await service._handle_device_info(
|
||||||
|
"TalkingQ_XQSN00001005",
|
||||||
|
{
|
||||||
|
"msg_id": "000",
|
||||||
|
"data": {
|
||||||
|
"id": "TalkingQ_XQSN00001005",
|
||||||
|
"power": 80,
|
||||||
|
"signal": 4,
|
||||||
|
"voice": 60,
|
||||||
|
"imei": "861921071409242",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"latitude": 23.1173393,
|
||||||
|
"longitude": 113.2855975,
|
||||||
|
"device_type": "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert published == [
|
||||||
|
("device/TalkingQ_XQSN00001005/event_resp", {"msg_id": "000", "status": "success"}),
|
||||||
|
]
|
||||||
|
assert calls == [
|
||||||
|
("schedule", "TalkingQ_XQSN00001005", "device_info"),
|
||||||
|
(
|
||||||
|
"settings",
|
||||||
|
{
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"power": 80,
|
||||||
|
"signal_strength": 4,
|
||||||
|
"version_str": "1.0.1",
|
||||||
|
"volume": 60,
|
||||||
|
"imei": "861921071409242",
|
||||||
|
"device_type": "2",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("schedule", "TalkingQ_XQSN00001005", "device_info_location"),
|
||||||
|
(
|
||||||
|
"location",
|
||||||
|
{
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"latitude": 23.1173393,
|
||||||
|
"longitude": 113.2855975,
|
||||||
|
"coord_type": None,
|
||||||
|
"accuracy_m": None,
|
||||||
|
"altitude_m": None,
|
||||||
|
"speed_mps": None,
|
||||||
|
"heading_deg": None,
|
||||||
|
"source": 0,
|
||||||
|
"battery_pct": 80,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_device_info_report_without_location_does_not_write_location(monkeypatch):
|
||||||
|
service = TalkingQMQTTService({"broker": "127.0.0.1"})
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
async def fake_publish(topic, payload):
|
||||||
|
del topic, payload
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def fake_schedule_persistence(device_id, label, coro):
|
||||||
|
calls.append(label)
|
||||||
|
await coro
|
||||||
|
|
||||||
|
async def fake_insert_or_update(**kwargs):
|
||||||
|
calls.append(("settings", kwargs["device_type"]))
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "_publish", fake_publish)
|
||||||
|
monkeypatch.setattr(service, "_schedule_persistence", fake_schedule_persistence)
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.device_setting_service.insert_or_update", fake_insert_or_update)
|
||||||
|
|
||||||
|
await service._handle_device_info(
|
||||||
|
"TalkingQ_XQSN00001005",
|
||||||
|
{"msg_id": "000", "data": {"power": 20, "signal": 2, "voice": 40, "version": "1.0.0", "device_type": 1}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls == ["device_info", ("settings", 1)]
|
||||||
201
talkingq-url/tests/test_ota_channel_selection.py
Normal file
201
talkingq-url/tests/test_ota_channel_selection.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from banban.service.device import DeviceService
|
||||||
|
from banban.service.ota_firmware import OtaFirmwareSelection
|
||||||
|
from banban.service.device_setting import DeviceSettingService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_firmware_status_selects_gps_channel(monkeypatch):
|
||||||
|
service = DeviceService()
|
||||||
|
|
||||||
|
async def fake_get_device_status(**kwargs):
|
||||||
|
del kwargs
|
||||||
|
return {
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"device_type": 2,
|
||||||
|
"ota_channel": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fake_select_latest_for_device(status_row):
|
||||||
|
assert status_row["device_type"] == 2
|
||||||
|
return OtaFirmwareSelection(
|
||||||
|
channel_key="gps",
|
||||||
|
version="1.1.0",
|
||||||
|
firmware_url="https://example.com/gps-1.1.0.bin",
|
||||||
|
firmware_id=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def fake_get_firmware_update_dict(device_id):
|
||||||
|
del device_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "get_device_status", fake_get_device_status)
|
||||||
|
monkeypatch.setattr("banban.service.device.ota_firmware_service.select_latest_for_device", fake_select_latest_for_device)
|
||||||
|
monkeypatch.setattr("banban.service.device.device_firmware_update_manager.get_firmware_update_dict", fake_get_firmware_update_dict)
|
||||||
|
|
||||||
|
result = await service.get_firmware_status(device_id="TalkingQ_XQSN00001005", user_id=1)
|
||||||
|
|
||||||
|
assert result["ota_channel"] == "gps"
|
||||||
|
assert result["firmware_id"] == 10
|
||||||
|
assert result["latest_version"] == "1.1.0"
|
||||||
|
assert result["firmware_url"] == "https://example.com/gps-1.1.0.bin"
|
||||||
|
assert result["update_available"] is True
|
||||||
|
assert result["can_update"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_firmware_status_unknown_channel_cannot_update(monkeypatch):
|
||||||
|
service = DeviceService()
|
||||||
|
|
||||||
|
async def fake_get_device_status(**kwargs):
|
||||||
|
del kwargs
|
||||||
|
return {
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"device_type": None,
|
||||||
|
"ota_channel": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fake_select_latest_for_device(status_row):
|
||||||
|
assert status_row["device_type"] is None
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def fake_get_firmware_update_dict(device_id):
|
||||||
|
del device_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "get_device_status", fake_get_device_status)
|
||||||
|
monkeypatch.setattr("banban.service.device.ota_firmware_service.select_latest_for_device", fake_select_latest_for_device)
|
||||||
|
monkeypatch.setattr("banban.service.device.device_firmware_update_manager.get_firmware_update_dict", fake_get_firmware_update_dict)
|
||||||
|
|
||||||
|
result = await service.get_firmware_status(device_id="TalkingQ_XQSN00001005", user_id=1)
|
||||||
|
|
||||||
|
assert result["ota_channel"] is None
|
||||||
|
assert result["latest_version"] is None
|
||||||
|
assert result["can_update"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_firmware_update_records_channel_and_keeps_mqtt_payload(monkeypatch):
|
||||||
|
service = DeviceService()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
async def fake_get_firmware_status(**kwargs):
|
||||||
|
del kwargs
|
||||||
|
return {
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"current_version": "1.0.0",
|
||||||
|
"latest_version": "1.1.0",
|
||||||
|
"ota_channel": "no_gps",
|
||||||
|
"firmware_id": 22,
|
||||||
|
"firmware_url": "https://example.com/no-gps-1.1.0.bin",
|
||||||
|
"update_available": True,
|
||||||
|
"can_update": True,
|
||||||
|
"update_status": "idle",
|
||||||
|
"progress": 0.0,
|
||||||
|
"target_version": None,
|
||||||
|
"updated_at": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fake_update_firmware_update(**kwargs):
|
||||||
|
calls.append(("record", kwargs))
|
||||||
|
return True
|
||||||
|
|
||||||
|
class FakeMqttService:
|
||||||
|
async def send_ota_command(self, device_id, url, version):
|
||||||
|
calls.append(("mqtt", {"device_id": device_id, "url": url, "version": version}))
|
||||||
|
return "003"
|
||||||
|
|
||||||
|
async def fake_get_instance():
|
||||||
|
return FakeMqttService()
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "get_firmware_status", fake_get_firmware_status)
|
||||||
|
monkeypatch.setattr("banban.service.device.device_firmware_update_manager.update_firmware_update", fake_update_firmware_update)
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.TalkingQMQTTService.get_instance", fake_get_instance)
|
||||||
|
|
||||||
|
result = await service.start_firmware_update(device_id="TalkingQ_XQSN00001005", user_id=1)
|
||||||
|
|
||||||
|
assert calls == [
|
||||||
|
(
|
||||||
|
"record",
|
||||||
|
{
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"firmware_version": "1.1.0",
|
||||||
|
"update_status": "sent",
|
||||||
|
"progress": 0.0,
|
||||||
|
"ota_channel": "no_gps",
|
||||||
|
"target_version": "1.1.0",
|
||||||
|
"firmware_url": "https://example.com/no-gps-1.1.0.bin",
|
||||||
|
"firmware_id": 22,
|
||||||
|
"source": "manual",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mqtt",
|
||||||
|
{
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"url": "https://example.com/no-gps-1.1.0.bin",
|
||||||
|
"version": "1.1.0",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
assert result["msg_id"] == "003"
|
||||||
|
assert result["ota_channel"] == "no_gps"
|
||||||
|
assert result["target_version"] == "1.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_firmware_update_without_known_channel_is_blocked(monkeypatch):
|
||||||
|
service = DeviceService()
|
||||||
|
|
||||||
|
async def fake_get_firmware_status(**kwargs):
|
||||||
|
del kwargs
|
||||||
|
return {
|
||||||
|
"device_id": "TalkingQ_XQSN00001005",
|
||||||
|
"current_version": "1.0.0",
|
||||||
|
"latest_version": None,
|
||||||
|
"update_available": False,
|
||||||
|
"can_update": False,
|
||||||
|
"update_status": "idle",
|
||||||
|
"progress": 0.0,
|
||||||
|
"target_version": None,
|
||||||
|
"updated_at": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "get_firmware_status", fake_get_firmware_status)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await service.start_firmware_update(device_id="TalkingQ_XQSN00001005", user_id=1)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_device_type_update_preserves_existing_manual_ota_channel(monkeypatch):
|
||||||
|
service = DeviceSettingService()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
async def fake_get_setting_by_device_id(device_id):
|
||||||
|
assert device_id == "TalkingQ_XQSN00001005"
|
||||||
|
return {"device_id": device_id, "ota_channel": "factory_gps"}
|
||||||
|
|
||||||
|
async def fake_update_setting(**kwargs):
|
||||||
|
calls.append(kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "get_setting_by_device_id", fake_get_setting_by_device_id)
|
||||||
|
monkeypatch.setattr(service, "update_setting", fake_update_setting)
|
||||||
|
|
||||||
|
await service.insert_or_update(
|
||||||
|
device_id="TalkingQ_XQSN00001005",
|
||||||
|
power=80,
|
||||||
|
volume=60,
|
||||||
|
signal_strength=4,
|
||||||
|
version_str="1.0.0",
|
||||||
|
device_type=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls[0]["device_type"] == 1
|
||||||
|
assert calls[0]["ota_channel"] is None
|
||||||
Reference in New Issue
Block a user