add 告警和联调问题修改
This commit is contained in:
@@ -77,6 +77,10 @@ class DeviceDAO(BaseDAO):
|
|||||||
db.device_id,
|
db.device_id,
|
||||||
db.child_id,
|
db.child_id,
|
||||||
c.child_name,
|
c.child_name,
|
||||||
|
ds.sleep_mode,
|
||||||
|
ds.disable_time_start,
|
||||||
|
ds.disable_time_end,
|
||||||
|
ds.timezone,
|
||||||
ds.power,
|
ds.power,
|
||||||
ds.volume,
|
ds.volume,
|
||||||
ds.`signal` AS signal_strength,
|
ds.`signal` AS signal_strength,
|
||||||
|
|||||||
86
talkingq-url/banban/dao/device_alarm.py
Normal file
86
talkingq-url/banban/dao/device_alarm.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from banban.dao import BaseDAO
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAlarmDAO(BaseDAO):
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
device_id: str,
|
||||||
|
owner_user_id: int | None,
|
||||||
|
child_id: int | None,
|
||||||
|
source_msg_id: str = "010",
|
||||||
|
) -> int:
|
||||||
|
result = await self.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO device_alarm_events (
|
||||||
|
device_id, owner_user_id, child_id, source_msg_id, created_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
:device_id, :owner_user_id, :child_id, :source_msg_id, CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"device_id": device_id,
|
||||||
|
"owner_user_id": owner_user_id,
|
||||||
|
"child_id": child_id,
|
||||||
|
"source_msg_id": source_msg_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self.commit()
|
||||||
|
return int(result.lastrowid)
|
||||||
|
|
||||||
|
async def get_active_binding_context(self, *, device_id: str) -> Mapping[str, Any] | None:
|
||||||
|
result = await self.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT owner_user_id, child_id
|
||||||
|
FROM device_bindings
|
||||||
|
WHERE device_id = :device_id
|
||||||
|
AND status = 1
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"device_id": device_id},
|
||||||
|
)
|
||||||
|
return result.mappings().first()
|
||||||
|
|
||||||
|
async def list_by_device(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
device_id: str,
|
||||||
|
owner_user_id: int,
|
||||||
|
limit: int,
|
||||||
|
) -> list[Mapping[str, Any]]:
|
||||||
|
result = await self.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
dae.alarm_id,
|
||||||
|
dae.device_id,
|
||||||
|
dae.owner_user_id,
|
||||||
|
dae.child_id,
|
||||||
|
dae.source_msg_id,
|
||||||
|
dae.created_at,
|
||||||
|
c.child_name
|
||||||
|
FROM device_alarm_events AS dae
|
||||||
|
LEFT JOIN children AS c
|
||||||
|
ON c.child_id = dae.child_id
|
||||||
|
AND c.status = 1
|
||||||
|
WHERE dae.device_id = :device_id
|
||||||
|
AND dae.owner_user_id = :owner_user_id
|
||||||
|
ORDER BY dae.created_at DESC, dae.alarm_id DESC
|
||||||
|
LIMIT :limit
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"device_id": device_id,
|
||||||
|
"owner_user_id": owner_user_id,
|
||||||
|
"limit": limit,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return list(result.mappings().all())
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime
|
from datetime import datetime, time
|
||||||
from services.connection_manager import connection_manager
|
from services.connection_manager import connection_manager
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -15,6 +15,7 @@ try:
|
|||||||
DeviceLocationTrajectoryItem,
|
DeviceLocationTrajectoryItem,
|
||||||
DeviceLocationTrajectoryResponse,
|
DeviceLocationTrajectoryResponse,
|
||||||
)
|
)
|
||||||
|
from banban.service.device_alarm import device_alarm_service
|
||||||
from banban.service.location import location_service
|
from banban.service.location import location_service
|
||||||
from banban.service.device import device_service
|
from banban.service.device import device_service
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
@@ -24,6 +25,7 @@ except ModuleNotFoundError:
|
|||||||
DeviceLocationTrajectoryItem,
|
DeviceLocationTrajectoryItem,
|
||||||
DeviceLocationTrajectoryResponse,
|
DeviceLocationTrajectoryResponse,
|
||||||
)
|
)
|
||||||
|
from banban.service.device_alarm import device_alarm_service
|
||||||
from banban.service.location import location_service
|
from banban.service.location import location_service
|
||||||
from banban.service.device import device_service
|
from banban.service.device import device_service
|
||||||
|
|
||||||
@@ -53,6 +55,10 @@ class DeviceStatusResponse(BaseModel):
|
|||||||
device_id: str
|
device_id: str
|
||||||
child_id: int | None = None
|
child_id: int | None = None
|
||||||
child_name: str | None = None
|
child_name: str | None = None
|
||||||
|
sleep_mode: int | None = None
|
||||||
|
disable_time_start: str | None = None
|
||||||
|
disable_time_end: str | None = None
|
||||||
|
timezone: str | None = None
|
||||||
power: int | None = None
|
power: int | None = None
|
||||||
volume: int | None = None
|
volume: int | None = None
|
||||||
signal: int | None = None
|
signal: int | None = None
|
||||||
@@ -72,6 +78,20 @@ class DeviceStatusResponse(BaseModel):
|
|||||||
location_updated_at: datetime | None = None
|
location_updated_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAlarmItem(BaseModel):
|
||||||
|
alarm_id: int
|
||||||
|
device_id: str
|
||||||
|
child_id: int | None = None
|
||||||
|
child_name: str | None = None
|
||||||
|
source_msg_id: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAlarmListResponse(BaseModel):
|
||||||
|
items: list[DeviceAlarmItem]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
class DeviceVolumeUpdateRequest(BaseModel):
|
class DeviceVolumeUpdateRequest(BaseModel):
|
||||||
level: int = Field(ge=0, le=100)
|
level: int = Field(ge=0, le=100)
|
||||||
|
|
||||||
@@ -82,6 +102,31 @@ class DeviceVolumeUpdateResponse(BaseModel):
|
|||||||
msg_id: str
|
msg_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSleepScheduleUpdateRequest(BaseModel):
|
||||||
|
start: str = Field(..., pattern=r"^\d{2}:\d{2}$")
|
||||||
|
end: str = Field(..., pattern=r"^\d{2}:\d{2}$")
|
||||||
|
timezone: str = Field(default="Asia/Shanghai", min_length=1, max_length=32)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSleepScheduleResponse(BaseModel):
|
||||||
|
device_id: str
|
||||||
|
sleep_mode: int
|
||||||
|
start: str | None = None
|
||||||
|
end: str | None = None
|
||||||
|
timezone: str
|
||||||
|
msg_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRemoteSleepWakeRequest(BaseModel):
|
||||||
|
switch: str = Field(..., pattern=r"^(on|off)$")
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRemoteSleepWakeResponse(BaseModel):
|
||||||
|
device_id: str
|
||||||
|
switch: str
|
||||||
|
msg_id: str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -140,11 +185,31 @@ def _row_to_trajectory_item(row: Mapping, *, child_name: str | None) -> DeviceLo
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_time_value(value: object) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, time):
|
||||||
|
return value.strftime("%H:%M")
|
||||||
|
text_value = str(value)
|
||||||
|
return text_value[:5] if len(text_value) >= 5 else text_value
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_time_value(value: str, field_name: str) -> time:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, "%H:%M").time()
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=f"{field_name} must use HH:MM format") from exc
|
||||||
|
|
||||||
|
|
||||||
def _row_to_device_status_response(row: Mapping) -> DeviceStatusResponse:
|
def _row_to_device_status_response(row: Mapping) -> DeviceStatusResponse:
|
||||||
return DeviceStatusResponse(
|
return DeviceStatusResponse(
|
||||||
device_id=str(row["device_id"]),
|
device_id=str(row["device_id"]),
|
||||||
child_id=int(row["child_id"]) if row["child_id"] is not None else None,
|
child_id=int(row["child_id"]) if row["child_id"] is not None else None,
|
||||||
child_name=row.get("child_name"),
|
child_name=row.get("child_name"),
|
||||||
|
sleep_mode=row.get("sleep_mode"),
|
||||||
|
disable_time_start=_format_time_value(row.get("disable_time_start")),
|
||||||
|
disable_time_end=_format_time_value(row.get("disable_time_end")),
|
||||||
|
timezone=row.get("timezone"),
|
||||||
power=row["power"],
|
power=row["power"],
|
||||||
volume=row["volume"],
|
volume=row["volume"],
|
||||||
signal=row["signal_strength"],
|
signal=row["signal_strength"],
|
||||||
@@ -165,6 +230,17 @@ def _row_to_device_status_response(row: Mapping) -> DeviceStatusResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_alarm_item(row: Mapping) -> DeviceAlarmItem:
|
||||||
|
return DeviceAlarmItem(
|
||||||
|
alarm_id=int(row["alarm_id"]),
|
||||||
|
device_id=str(row["device_id"]),
|
||||||
|
child_id=int(row["child_id"]) if row["child_id"] is not None else None,
|
||||||
|
child_name=row.get("child_name"),
|
||||||
|
source_msg_id=str(row["source_msg_id"]),
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{device_id}/messages", response_model=DeviceMessageListResponse)
|
@router.get("/{device_id}/messages", response_model=DeviceMessageListResponse)
|
||||||
async def list_device_messages(
|
async def list_device_messages(
|
||||||
device_id: str,
|
device_id: str,
|
||||||
@@ -226,6 +302,36 @@ async def get_device_status(
|
|||||||
return _row_to_device_status_response(row)
|
return _row_to_device_status_response(row)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{device_id}/alarms", response_model=DeviceAlarmListResponse)
|
||||||
|
async def list_device_alarms(
|
||||||
|
device_id: str,
|
||||||
|
request: Request,
|
||||||
|
limit: int = Query(default=20, ge=1, le=100),
|
||||||
|
current_user_id: int = Depends(get_current_user_id),
|
||||||
|
) -> DeviceAlarmListResponse:
|
||||||
|
await device_service.ensure_device_access(device_id=device_id, user_id=current_user_id)
|
||||||
|
rows = await device_alarm_service.list_device_alarms(
|
||||||
|
device_id=device_id,
|
||||||
|
owner_user_id=current_user_id,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"device alarms fetched",
|
||||||
|
extra={
|
||||||
|
"event": "device_alarm_list",
|
||||||
|
"request_id": getattr(request.state, "request_id", None),
|
||||||
|
"user_id": current_user_id,
|
||||||
|
"device_id": device_id,
|
||||||
|
"returned_count": len(rows),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return DeviceAlarmListResponse(
|
||||||
|
items=[_row_to_alarm_item(row) for row in rows],
|
||||||
|
total=len(rows),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{device_id}/volume", response_model=DeviceVolumeUpdateResponse)
|
@router.post("/{device_id}/volume", response_model=DeviceVolumeUpdateResponse)
|
||||||
async def set_device_volume(
|
async def set_device_volume(
|
||||||
device_id: str,
|
device_id: str,
|
||||||
@@ -253,6 +359,77 @@ async def set_device_volume(
|
|||||||
return DeviceVolumeUpdateResponse(device_id=device_id, level=payload.level, msg_id=msg_id)
|
return DeviceVolumeUpdateResponse(device_id=device_id, level=payload.level, msg_id=msg_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{device_id}/sleep-schedule", response_model=DeviceSleepScheduleResponse)
|
||||||
|
async def set_device_sleep_schedule(
|
||||||
|
device_id: str,
|
||||||
|
payload: DeviceSleepScheduleUpdateRequest,
|
||||||
|
request: Request,
|
||||||
|
current_user_id: int = Depends(get_current_user_id),
|
||||||
|
) -> DeviceSleepScheduleResponse:
|
||||||
|
start_time = _parse_time_value(payload.start, "start")
|
||||||
|
end_time = _parse_time_value(payload.end, "end")
|
||||||
|
msg_id = await device_service.set_sleep_schedule(
|
||||||
|
device_id=device_id,
|
||||||
|
user_id=current_user_id,
|
||||||
|
start=start_time,
|
||||||
|
end=end_time,
|
||||||
|
timezone=payload.timezone,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"device sleep schedule command sent",
|
||||||
|
extra={
|
||||||
|
"event": "device_sleep_schedule_set",
|
||||||
|
"request_id": getattr(request.state, "request_id", None),
|
||||||
|
"user_id": current_user_id,
|
||||||
|
"device_id": device_id,
|
||||||
|
"start": payload.start,
|
||||||
|
"end": payload.end,
|
||||||
|
"timezone": payload.timezone,
|
||||||
|
"msg_id": msg_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return DeviceSleepScheduleResponse(
|
||||||
|
device_id=device_id,
|
||||||
|
sleep_mode=1,
|
||||||
|
start=payload.start,
|
||||||
|
end=payload.end,
|
||||||
|
timezone=payload.timezone,
|
||||||
|
msg_id=msg_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{device_id}/sleep-wake", response_model=DeviceRemoteSleepWakeResponse)
|
||||||
|
async def set_device_remote_sleep_wake(
|
||||||
|
device_id: str,
|
||||||
|
payload: DeviceRemoteSleepWakeRequest,
|
||||||
|
request: Request,
|
||||||
|
current_user_id: int = Depends(get_current_user_id),
|
||||||
|
) -> DeviceRemoteSleepWakeResponse:
|
||||||
|
msg_id = await device_service.set_remote_sleep_wake(
|
||||||
|
device_id=device_id,
|
||||||
|
user_id=current_user_id,
|
||||||
|
switch=payload.switch,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"device remote sleep wake command sent",
|
||||||
|
extra={
|
||||||
|
"event": "device_remote_sleep_wake_set",
|
||||||
|
"request_id": getattr(request.state, "request_id", None),
|
||||||
|
"user_id": current_user_id,
|
||||||
|
"device_id": device_id,
|
||||||
|
"switch": payload.switch,
|
||||||
|
"msg_id": msg_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return DeviceRemoteSleepWakeResponse(
|
||||||
|
device_id=device_id,
|
||||||
|
switch=payload.switch,
|
||||||
|
msg_id=msg_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{device_id}/location", response_model=DeviceLocationCurrentResponse)
|
@router.get("/{device_id}/location", response_model=DeviceLocationCurrentResponse)
|
||||||
async def get_current_device_location(
|
async def get_current_device_location(
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any, List
|
from datetime import time
|
||||||
|
from typing import Any, List, Literal
|
||||||
|
|
||||||
from services.database_service_base import DatabaseServiceBase
|
from services.database_service_base import DatabaseServiceBase
|
||||||
from fastapi import HTTPException
|
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
|
||||||
|
|
||||||
|
|
||||||
class DeviceService(DatabaseServiceBase):
|
class DeviceService(DatabaseServiceBase):
|
||||||
@@ -68,6 +70,51 @@ class DeviceService(DatabaseServiceBase):
|
|||||||
raise HTTPException(status_code=503, detail="MQTT 服务未初始化")
|
raise HTTPException(status_code=503, detail="MQTT 服务未初始化")
|
||||||
return await service.send_volume_command(device_id, level)
|
return await service.send_volume_command(device_id, level)
|
||||||
|
|
||||||
|
async def set_sleep_schedule(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
device_id: str,
|
||||||
|
user_id: int,
|
||||||
|
start: time,
|
||||||
|
end: time,
|
||||||
|
timezone: str,
|
||||||
|
) -> str:
|
||||||
|
await self.ensure_device_access(device_id=device_id, user_id=user_id)
|
||||||
|
|
||||||
|
from handlers.mqtt_handler import TalkingQMQTTService
|
||||||
|
|
||||||
|
service = await TalkingQMQTTService.get_instance()
|
||||||
|
if service is None:
|
||||||
|
raise HTTPException(status_code=503, detail="MQTT 服务未初始化")
|
||||||
|
|
||||||
|
start_text = start.strftime("%H:%M")
|
||||||
|
end_text = end.strftime("%H:%M")
|
||||||
|
msg_id = await service.send_sleep_schedule_command(device_id, start_text, end_text)
|
||||||
|
await device_setting_service.upsert_sleep_schedule(
|
||||||
|
device_id=device_id,
|
||||||
|
sleep_mode=1,
|
||||||
|
disable_time_start=start,
|
||||||
|
disable_time_end=end,
|
||||||
|
timezone=timezone,
|
||||||
|
)
|
||||||
|
return msg_id
|
||||||
|
|
||||||
|
async def set_remote_sleep_wake(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
device_id: str,
|
||||||
|
user_id: int,
|
||||||
|
switch: Literal["on", "off"],
|
||||||
|
) -> str:
|
||||||
|
await self.ensure_device_access(device_id=device_id, user_id=user_id)
|
||||||
|
|
||||||
|
from handlers.mqtt_handler import TalkingQMQTTService
|
||||||
|
|
||||||
|
service = await TalkingQMQTTService.get_instance()
|
||||||
|
if service is None:
|
||||||
|
raise HTTPException(status_code=503, detail="MQTT 服务未初始化")
|
||||||
|
return await service.send_remote_sleep_wake_command(device_id, switch)
|
||||||
|
|
||||||
|
|
||||||
# 创建全局 DeviceService 实例
|
# 创建全局 DeviceService 实例
|
||||||
device_service = DeviceService()
|
device_service = DeviceService()
|
||||||
|
|||||||
47
talkingq-url/banban/service/device_alarm.py
Normal file
47
talkingq-url/banban/service/device_alarm.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from banban.dao.device_alarm import DeviceAlarmDAO
|
||||||
|
from services.database_service_base import DatabaseServiceBase
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAlarmService(DatabaseServiceBase):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(service_name="device_alarm_service")
|
||||||
|
|
||||||
|
async def record_alarm_event(self, *, device_id: str, source_msg_id: str = "010") -> int | None:
|
||||||
|
db_session = await self.get_session()
|
||||||
|
try:
|
||||||
|
dao = DeviceAlarmDAO(db_session)
|
||||||
|
binding = await dao.get_active_binding_context(device_id=device_id)
|
||||||
|
if not binding:
|
||||||
|
return None
|
||||||
|
return await dao.create(
|
||||||
|
device_id=device_id,
|
||||||
|
owner_user_id=int(binding["owner_user_id"]) if binding.get("owner_user_id") is not None else None,
|
||||||
|
child_id=int(binding["child_id"]) if binding.get("child_id") is not None else None,
|
||||||
|
source_msg_id=source_msg_id,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await db_session.close()
|
||||||
|
|
||||||
|
async def list_device_alarms(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
device_id: str,
|
||||||
|
owner_user_id: int,
|
||||||
|
limit: int,
|
||||||
|
) -> list[Mapping[str, Any]]:
|
||||||
|
db_session = await self.get_session()
|
||||||
|
try:
|
||||||
|
dao = DeviceAlarmDAO(db_session)
|
||||||
|
return await dao.list_by_device(
|
||||||
|
device_id=device_id,
|
||||||
|
owner_user_id=owner_user_id,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await db_session.close()
|
||||||
|
|
||||||
|
|
||||||
|
device_alarm_service = DeviceAlarmService()
|
||||||
@@ -93,7 +93,14 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
finally:
|
finally:
|
||||||
await db_session.close()
|
await db_session.close()
|
||||||
|
|
||||||
async def insert_or_update(self, device_id: str, power: int, volume: int, signal_strength: int, version_str: str) -> None:
|
async def insert_or_update(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
power: Optional[int],
|
||||||
|
volume: Optional[int],
|
||||||
|
signal_strength: Optional[int],
|
||||||
|
version_str: Optional[str],
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
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:
|
||||||
@@ -103,5 +110,33 @@ class DeviceSettingService(DatabaseServiceBase):
|
|||||||
finally:
|
finally:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def upsert_sleep_schedule(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
device_id: str,
|
||||||
|
sleep_mode: int,
|
||||||
|
disable_time_start: time,
|
||||||
|
disable_time_end: time,
|
||||||
|
timezone: str,
|
||||||
|
) -> None:
|
||||||
|
current_row = await self.get_setting_by_device_id(device_id=device_id)
|
||||||
|
if current_row:
|
||||||
|
await self.update_setting(
|
||||||
|
device_id=device_id,
|
||||||
|
sleep_mode=sleep_mode,
|
||||||
|
disable_time_start=disable_time_start,
|
||||||
|
disable_time_end=disable_time_end,
|
||||||
|
timezone=timezone,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.create_setting(
|
||||||
|
device_id=device_id,
|
||||||
|
sleep_mode=sleep_mode,
|
||||||
|
disable_time_start=disable_time_start,
|
||||||
|
disable_time_end=disable_time_end,
|
||||||
|
timezone=timezone,
|
||||||
|
)
|
||||||
|
|
||||||
# 创建全局 DeviceSettingService 实例
|
# 创建全局 DeviceSettingService 实例
|
||||||
device_setting_service = DeviceSettingService()
|
device_setting_service = DeviceSettingService()
|
||||||
@@ -267,6 +267,22 @@ class DeviceSetting(Base):
|
|||||||
onupdate=datetime.utcnow,
|
onupdate=datetime.utcnow,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAlarmEvent(Base):
|
||||||
|
__tablename__ = "device_alarm_events"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_device_alarm_device_created", "device_id", "created_at"),
|
||||||
|
Index("idx_device_alarm_owner_created", "owner_user_id", "created_at"),
|
||||||
|
Index("idx_device_alarm_child_created", "child_id", "created_at"),
|
||||||
|
)
|
||||||
|
|
||||||
|
alarm_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
device_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
owner_user_id: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
child_id: Mapped[Optional[int]] = mapped_column(Integer)
|
||||||
|
source_msg_id: Mapped[str] = mapped_column(String(8), server_default=text("'010'"))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
class IMConversation(Base):
|
class IMConversation(Base):
|
||||||
__tablename__ = "im_conversations"
|
__tablename__ = "im_conversations"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import asyncio
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, Callable, Awaitable
|
from typing import Awaitable, Callable, Dict, Optional
|
||||||
from banban.service.device_setting import device_setting_service
|
|
||||||
|
import aiomqtt
|
||||||
|
|
||||||
from banban.service.binding import BindingService
|
from banban.service.binding import BindingService
|
||||||
|
from banban.service.device_alarm import device_alarm_service
|
||||||
|
from banban.service.device_setting import device_setting_service
|
||||||
|
from banban.service.location import location_service
|
||||||
from config import settings
|
from config import settings
|
||||||
from services.card_service import card_service
|
from services.card_service import card_service
|
||||||
from services.offline_audio_cache import offline_audio_cache
|
from services.offline_audio_cache import offline_audio_cache
|
||||||
@@ -12,6 +18,8 @@ from banban.service.location import location_service
|
|||||||
from database.models import ChildLocationCurrent
|
from database.models import ChildLocationCurrent
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from services.device_target_cache import device_target_cache
|
from services.device_target_cache import device_target_cache
|
||||||
|
from services.offline_audio_cache import offline_audio_cache
|
||||||
|
from services.task_manager import task_manager
|
||||||
from utils.logger import session_logger as logger
|
from utils.logger import session_logger as logger
|
||||||
from services.task_manager import task_manager
|
from services.task_manager import task_manager
|
||||||
|
|
||||||
@@ -69,30 +77,28 @@ class TalkingQMQTTService:
|
|||||||
try:
|
try:
|
||||||
topic = str(message.topic)
|
topic = str(message.topic)
|
||||||
parts = topic.split("/")
|
parts = topic.split("/")
|
||||||
if parts[0] == "device":
|
if not parts or parts[0] != "device":
|
||||||
device_id = parts[1] if len(parts) >= 2 else "unknown"
|
continue
|
||||||
|
|
||||||
if not device_id.startswith(f"{self.device_prefix}_"):
|
device_id = parts[1] if len(parts) >= 2 else "unknown"
|
||||||
continue
|
if not device_id.startswith(f"{self.device_prefix}_"):
|
||||||
|
continue
|
||||||
|
|
||||||
payload = json.loads(message.payload.decode("utf-8"))
|
payload = json.loads(message.payload.decode("utf-8"))
|
||||||
|
msg_id = payload.get("msg_id")
|
||||||
msg_id = payload.get("msg_id")
|
handler = self._msg_handlers.get(msg_id)
|
||||||
|
if handler:
|
||||||
handler = self._msg_handlers.get(msg_id)
|
await handler(device_id, payload)
|
||||||
if handler:
|
else:
|
||||||
await handler(device_id, payload)
|
logger.warning(device_id, "", f"unknown msg_id={msg_id}")
|
||||||
else:
|
except json.JSONDecodeError as exc:
|
||||||
logger.warning(device_id, "", f"未知 msg_id: {msg_id}, 设备: {device_id}")
|
logger.error("", "", f"invalid mqtt payload: {exc}")
|
||||||
|
except Exception as exc:
|
||||||
except json.JSONDecodeError as e:
|
logger.error("", "", f"mqtt message handling failed: {exc}")
|
||||||
logger.error("", "", f"消息解析失败: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("", "", f"消息处理异常: {e}")
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logger.error("", "", f"消息循环异常: {e}")
|
logger.error("", "", f"mqtt loop failed: {exc}")
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
async def _schedule_persistence(self, device_id: str, label: str, coro) -> None:
|
async def _schedule_persistence(self, device_id: str, label: str, coro) -> None:
|
||||||
@@ -109,7 +115,6 @@ class TalkingQMQTTService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_device_info(self, device_id: str, payload: dict):
|
async def _handle_device_info(self, device_id: str, payload: dict):
|
||||||
# status = payload.get("status")
|
|
||||||
data = payload.get("data", {})
|
data = payload.get("data", {})
|
||||||
await self._schedule_persistence(
|
await self._schedule_persistence(
|
||||||
device_id,
|
device_id,
|
||||||
@@ -199,10 +204,8 @@ class TalkingQMQTTService:
|
|||||||
|
|
||||||
async def _handle_nfc_notice_response(self, device_id: str, payload: dict):
|
async def _handle_nfc_notice_response(self, device_id: str, payload: dict):
|
||||||
status = payload.get("status")
|
status = payload.get("status")
|
||||||
if status == "success":
|
if status != "success":
|
||||||
logger.info(device_id, "", f"[NFC通知] 设备 {device_id} 已收到留言提示")
|
logger.warning(device_id, "", f"[NFC notice] command failed: {payload}")
|
||||||
else:
|
|
||||||
logger.warning(device_id, "", f"[NFC通知] 设备 {device_id} 留言提示失败: {payload}")
|
|
||||||
|
|
||||||
async def _handle_nfc_listen_report(self, device_id: str, payload: dict):
|
async def _handle_nfc_listen_report(self, device_id: str, payload: dict):
|
||||||
params = payload.get("params", {})
|
params = payload.get("params", {})
|
||||||
@@ -223,7 +226,8 @@ class TalkingQMQTTService:
|
|||||||
if result is None:
|
if result is None:
|
||||||
logger.warning(device_id, "", "[NFC bind] no pending bind session found")
|
logger.warning(device_id, "", "[NFC bind] no pending bind session found")
|
||||||
return
|
return
|
||||||
logger.info(device_id, "", f"[NFC绑定] 设备 {device_id} 请求绑定卡片, UUID={nfc_uuid}, result={result}")
|
|
||||||
|
logger.info(device_id, "", f"[NFC bind] bind completed, uuid={nfc_uuid}, result={result}")
|
||||||
|
|
||||||
async def _handle_open_response(self, device_id: str, payload: dict):
|
async def _handle_open_response(self, device_id: str, payload: dict):
|
||||||
params = payload
|
params = payload
|
||||||
@@ -237,6 +241,25 @@ class TalkingQMQTTService:
|
|||||||
status = payload.get("status")
|
status = payload.get("status")
|
||||||
if status == "success":
|
if status == "success":
|
||||||
logger.info(device_id, "", f"[定时休眠] 设备 {device_id} 休眠时间设置成功")
|
logger.info(device_id, "", f"[定时休眠] 设备 {device_id} 休眠时间设置成功")
|
||||||
|
data = payload.get("data", {}) or {}
|
||||||
|
start = data.get("start")
|
||||||
|
end = data.get("end")
|
||||||
|
timezone = data.get("timezone") or "Asia/Shanghai"
|
||||||
|
if start and end:
|
||||||
|
try:
|
||||||
|
await self._schedule_persistence(
|
||||||
|
device_id,
|
||||||
|
"sleep_schedule",
|
||||||
|
device_setting_service.upsert_sleep_schedule(
|
||||||
|
device_id=device_id,
|
||||||
|
sleep_mode=1,
|
||||||
|
disable_time_start=datetime.strptime(start, "%H:%M").time(),
|
||||||
|
disable_time_end=datetime.strptime(end, "%H:%M").time(),
|
||||||
|
timezone=timezone,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(device_id, "", f"[定时休眠] 无法解析设备返回的时间: {payload}")
|
||||||
else:
|
else:
|
||||||
logger.warning(device_id, "", f"[定时休眠] 设备 {device_id} 休眠时间设置失败: {payload}")
|
logger.warning(device_id, "", f"[定时休眠] 设备 {device_id} 休眠时间设置失败: {payload}")
|
||||||
|
|
||||||
@@ -249,6 +272,11 @@ class TalkingQMQTTService:
|
|||||||
|
|
||||||
async def _handle_alarm_report(self, device_id: str, payload: dict):
|
async def _handle_alarm_report(self, device_id: str, payload: dict):
|
||||||
logger.info(device_id, "", f"[告警] 设备 {device_id} 发送紧急报警")
|
logger.info(device_id, "", f"[告警] 设备 {device_id} 发送紧急报警")
|
||||||
|
await self._schedule_persistence(
|
||||||
|
device_id,
|
||||||
|
"alarm_event",
|
||||||
|
device_alarm_service.record_alarm_event(device_id=device_id, source_msg_id="010"),
|
||||||
|
)
|
||||||
await self._publish(f"device/{device_id}/event_resp", {"msg_id": "010", "status": "success"})
|
await self._publish(f"device/{device_id}/event_resp", {"msg_id": "010", "status": "success"})
|
||||||
|
|
||||||
async def _handle_short_press_message(self, device_id: str, payload: dict):
|
async def _handle_short_press_message(self, device_id: str, payload: dict):
|
||||||
@@ -268,7 +296,6 @@ class TalkingQMQTTService:
|
|||||||
topic = f"device/{device_id}/event_resp"
|
topic = f"device/{device_id}/event_resp"
|
||||||
card = await card_service.get_card_by_uuid(nfc_uuid)
|
card = await card_service.get_card_by_uuid(nfc_uuid)
|
||||||
if not card:
|
if not card:
|
||||||
logger.warning("", "", f"[NFC收听] 设备 {device_id} 没有找到卡片{nfc_uuid},无法发送留言提示")
|
|
||||||
payload = {
|
payload = {
|
||||||
"msg_id": "005",
|
"msg_id": "005",
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@@ -292,6 +319,7 @@ class TalkingQMQTTService:
|
|||||||
}
|
}
|
||||||
await self._publish(topic, payload)
|
await self._publish(topic, payload)
|
||||||
return
|
return
|
||||||
|
|
||||||
if has_pending:
|
if has_pending:
|
||||||
audio_urls = await offline_audio_cache.get_audio_urls(device_id)
|
audio_urls = await offline_audio_cache.get_audio_urls(device_id)
|
||||||
# 53D92B6DA20001 测试卡片
|
# 53D92B6DA20001 测试卡片
|
||||||
@@ -314,49 +342,38 @@ class TalkingQMQTTService:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
params = {}
|
params = {f"url_{k}": audio_url for k, audio_url in enumerate(audio_urls, start=1)}
|
||||||
for k, audio_url in enumerate(audio_urls, start=1):
|
payload = {"msg_id": "005", "type": 0, "params": params}
|
||||||
params[f"url_{k}"] = audio_url
|
|
||||||
payload = {
|
|
||||||
"msg_id": "005",
|
|
||||||
"type": 0,
|
|
||||||
"params": params
|
|
||||||
}
|
|
||||||
await offline_audio_cache.clear_audio_urls(device_id)
|
await offline_audio_cache.clear_audio_urls(device_id)
|
||||||
await self._publish(topic, payload)
|
await self._publish(topic, payload)
|
||||||
else:
|
|
||||||
payload = {
|
|
||||||
"msg_id": "005",
|
|
||||||
"type": 0,
|
|
||||||
"params": {
|
|
||||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/no_message.mp3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await self._publish(topic, payload)
|
|
||||||
else:
|
|
||||||
# 检查卡片是否存在
|
|
||||||
existing_card = await card_service.get_card_by_uuid(nfc_uuid)
|
|
||||||
|
|
||||||
if existing_card:
|
|
||||||
# 卡片已存在,使用卡片绑定的设备ID作为目标设备ID
|
|
||||||
target_device_id = existing_card.device_id
|
|
||||||
logger.info(device_id, "card", f"卡片已存在,绑定的设备ID: {target_device_id}")
|
|
||||||
else:
|
|
||||||
# 卡片不存在,创建新卡片并绑定到当前设备
|
|
||||||
new_card = await card_service.activate_card(nfc_uuid, device_id)
|
|
||||||
logger.info(device_id, "card", f"新卡片{nfc_uuid}已创建并激活,绑定到设备: {device_id}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# 设置目标设备
|
|
||||||
await device_target_cache.set_target(device_id, target_device_id)
|
|
||||||
payload = {
|
payload = {
|
||||||
"msg_id": "005",
|
"msg_id": "005",
|
||||||
"type": 1,
|
"type": 0,
|
||||||
"params": {
|
"params": {
|
||||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/welcome.mp3"
|
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/no_message.mp3"
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
await self._publish(topic, payload)
|
await self._publish(topic, payload)
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_card = await card_service.get_card_by_uuid(nfc_uuid)
|
||||||
|
if existing_card:
|
||||||
|
target_device_id = existing_card.device_id
|
||||||
|
else:
|
||||||
|
await card_service.activate_card(nfc_uuid, device_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
await device_target_cache.set_target(device_id, target_device_id)
|
||||||
|
payload = {
|
||||||
|
"msg_id": "005",
|
||||||
|
"type": 1,
|
||||||
|
"params": {
|
||||||
|
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/welcome.mp3"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await self._publish(topic, payload)
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
async with self._connect_lock:
|
async with self._connect_lock:
|
||||||
@@ -373,19 +390,15 @@ class TalkingQMQTTService:
|
|||||||
)
|
)
|
||||||
await self._client.__aenter__()
|
await self._client.__aenter__()
|
||||||
self._connected = True
|
self._connected = True
|
||||||
logger.system_info("", f"TalkingQ MQTT 已连接: {self.broker}:{self.port}")
|
|
||||||
|
|
||||||
await self._client.subscribe("device/+/response", qos=self.qos)
|
await self._client.subscribe("device/+/response", qos=self.qos)
|
||||||
logger.system_info("", "已订阅: device/+/response")
|
|
||||||
|
|
||||||
await self._client.subscribe("device/+/event", qos=self.qos)
|
await self._client.subscribe("device/+/event", qos=self.qos)
|
||||||
logger.system_info("", "已订阅: device/+/event")
|
|
||||||
|
|
||||||
self._message_task = asyncio.create_task(self._message_loop())
|
self._message_task = asyncio.create_task(self._message_loop())
|
||||||
logger.info("", "", "TalkingQ MQTT 服务启动中...")
|
logger.info("", "", f"mqtt connected: {self.broker}:{self.port}")
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
self._connected = False
|
self._connected = False
|
||||||
logger.error("", "", f"TalkingQ MQTT 连接失败: {e}")
|
logger.error("", "", f"mqtt connect failed: {exc}")
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
if self._message_task and not self._message_task.done():
|
if self._message_task and not self._message_task.done():
|
||||||
@@ -412,35 +425,26 @@ class TalkingQMQTTService:
|
|||||||
await self._ensure_connected()
|
await self._ensure_connected()
|
||||||
try:
|
try:
|
||||||
await self._client.publish(topic, json.dumps(payload, ensure_ascii=False), qos=self.qos)
|
await self._client.publish(topic, json.dumps(payload, ensure_ascii=False), qos=self.qos)
|
||||||
logger.info("", "", f"下发命令 -> {topic}: {payload}")
|
logger.info("", "", f"mqtt publish topic={topic} payload={payload}")
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logger.error("", "", f"命令下发失败: {e}")
|
logger.error("", "", f"mqtt publish failed: {exc}")
|
||||||
|
|
||||||
async def send_gps_query(self, device_id: str) -> str:
|
async def send_gps_query(self, device_id: str) -> str:
|
||||||
topic = f"device/{device_id}/command"
|
await self._publish(f"device/{device_id}/command", {"msg_id": "001"})
|
||||||
payload = {"msg_id": "001"}
|
|
||||||
await self._publish(topic, payload)
|
|
||||||
return "001"
|
return "001"
|
||||||
|
|
||||||
async def send_volume_command(self, device_id: str, level: int) -> str:
|
async def send_volume_command(self, device_id: str, level: int) -> str:
|
||||||
topic = f"device/{device_id}/command"
|
await self._publish(
|
||||||
payload = {
|
f"device/{device_id}/command",
|
||||||
"msg_id": "002",
|
{"msg_id": "002", "params": {"level": level}},
|
||||||
"params": {"level": level}
|
)
|
||||||
}
|
|
||||||
await self._publish(topic, payload)
|
|
||||||
return "002"
|
return "002"
|
||||||
|
|
||||||
async def send_ota_command(self, device_id: str, url: str, version: str) -> str:
|
async def send_ota_command(self, device_id: str, url: str, version: str) -> str:
|
||||||
topic = f"device/{device_id}/command"
|
await self._publish(
|
||||||
payload = {
|
f"device/{device_id}/command",
|
||||||
"msg_id": "003",
|
{"msg_id": "003", "params": {"url": url, "version": version}},
|
||||||
"params": {
|
)
|
||||||
"url": url,
|
|
||||||
"version": version,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await self._publish(topic, payload)
|
|
||||||
return "003"
|
return "003"
|
||||||
|
|
||||||
async def send_nfc_notice(self, device_id: str, url: str) -> str:
|
async def send_nfc_notice(self, device_id: str, url: str) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user