同步设备期望音量并补发音量指令
This commit is contained in:
@@ -3,6 +3,7 @@ from typing import Optional
|
|||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from banban.service.device_audio_cache import device_audio_cache_service
|
from banban.service.device_audio_cache import device_audio_cache_service
|
||||||
from banban.service.pending_voice_message import pending_voice_message_service
|
from banban.service.pending_voice_message import pending_voice_message_service
|
||||||
|
from services.device_volume_manager import device_volume_manager
|
||||||
from services.offline_audio_cache import offline_audio_cache
|
from services.offline_audio_cache import offline_audio_cache
|
||||||
from banban.schemas.mqtt_models import (
|
from banban.schemas.mqtt_models import (
|
||||||
GPSQueryRequest,
|
GPSQueryRequest,
|
||||||
@@ -52,6 +53,7 @@ async def set_volume(
|
|||||||
req: VolumeRequest,
|
req: VolumeRequest,
|
||||||
# current_user_id: int = Depends(get_current_user_id)
|
# current_user_id: int = Depends(get_current_user_id)
|
||||||
):
|
):
|
||||||
|
await device_volume_manager.set_volume(req.device_id, req.level)
|
||||||
service = await _get_service()
|
service = await _get_service()
|
||||||
msg_id = await service.send_volume_command(req.device_id, req.level)
|
msg_id = await service.send_volume_command(req.device_id, req.level)
|
||||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 services.device_volume_manager import device_volume_manager
|
||||||
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
|
from services.system_config_manager import system_config_manager
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ class DeviceService(DatabaseServiceBase):
|
|||||||
level: int,
|
level: int,
|
||||||
) -> str:
|
) -> str:
|
||||||
await self.ensure_device_access(device_id=device_id, user_id=user_id)
|
await self.ensure_device_access(device_id=device_id, user_id=user_id)
|
||||||
|
await device_volume_manager.set_volume(device_id, level)
|
||||||
|
|
||||||
from handlers.mqtt_handler import TalkingQMQTTService
|
from handlers.mqtt_handler import TalkingQMQTTService
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from services.device_identity_initializer import (
|
|||||||
device_identity_initializer,
|
device_identity_initializer,
|
||||||
)
|
)
|
||||||
from services.device_update_manager import device_firmware_update_manager
|
from services.device_update_manager import device_firmware_update_manager
|
||||||
|
from services.device_volume_manager import device_volume_manager
|
||||||
from services.offline_audio_cache import offline_audio_cache
|
from services.offline_audio_cache import offline_audio_cache
|
||||||
from services.task_manager import task_manager
|
from services.task_manager import task_manager
|
||||||
from utils.logger import session_logger as logger
|
from utils.logger import session_logger as logger
|
||||||
@@ -120,8 +121,29 @@ class TalkingQMQTTService:
|
|||||||
task_type="persistence",
|
task_type="persistence",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _sync_desired_volume(self, device_id: str, current_level) -> None:
|
||||||
|
if current_level is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
current_level_int = int(current_level)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
logger.warning(device_id, "volume", f"invalid current volume level: {current_level}")
|
||||||
|
return
|
||||||
|
|
||||||
|
desired_level = await device_volume_manager.get_configured_volume(device_id)
|
||||||
|
if desired_level is None or desired_level == current_level_int:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
device_id,
|
||||||
|
"volume",
|
||||||
|
f"当前音量 {current_level_int} 与期望音量 {desired_level} 不一致,补发音量指令",
|
||||||
|
)
|
||||||
|
await self.send_volume_command(device_id, desired_level)
|
||||||
|
|
||||||
async def _handle_device_info(self, device_id: str, payload: dict):
|
async def _handle_device_info(self, device_id: str, payload: dict):
|
||||||
data = payload.get("data", {})
|
data = payload.get("data", {})
|
||||||
|
current_volume = data.get("voice")
|
||||||
await self._schedule_persistence(
|
await self._schedule_persistence(
|
||||||
device_id,
|
device_id,
|
||||||
"device_info",
|
"device_info",
|
||||||
@@ -130,9 +152,10 @@ class TalkingQMQTTService:
|
|||||||
power=data.get("power"),
|
power=data.get("power"),
|
||||||
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=current_volume,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
await self._sync_desired_volume(device_id, current_volume)
|
||||||
await self._publish(f"device/{device_id}/event_resp", {"msg_id": "000", "status": "success"})
|
await self._publish(f"device/{device_id}/event_resp", {"msg_id": "000", "status": "success"})
|
||||||
|
|
||||||
async def _handle_gps_response(self, device_id: str, payload: dict):
|
async def _handle_gps_response(self, device_id: str, payload: dict):
|
||||||
@@ -187,6 +210,7 @@ class TalkingQMQTTService:
|
|||||||
version_str=None,
|
version_str=None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
await self._sync_desired_volume(device_id, current_level)
|
||||||
|
|
||||||
async def _handle_ota_response(self, device_id: str, payload: dict):
|
async def _handle_ota_response(self, device_id: str, payload: dict):
|
||||||
status = payload.get("status")
|
status = payload.get("status")
|
||||||
|
|||||||
@@ -39,6 +39,27 @@ class DeviceVolumeManager(DatabaseServiceBase):
|
|||||||
return self.default_volume # 出错时返回默认值
|
return self.default_volume # 出错时返回默认值
|
||||||
finally:
|
finally:
|
||||||
await db_session.close()
|
await db_session.close()
|
||||||
|
|
||||||
|
async def get_configured_volume(self, device_id: str) -> Optional[int]:
|
||||||
|
"""获取已持久化的期望音量;未配置时返回 None。"""
|
||||||
|
await self._init_database()
|
||||||
|
|
||||||
|
db_session = await self.get_session()
|
||||||
|
try:
|
||||||
|
query = select(DeviceConfig.volume).where(DeviceConfig.device_id == device_id)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
volume = result.scalar_one_or_none()
|
||||||
|
if volume is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized_volume = int(volume)
|
||||||
|
self.device_volumes[device_id] = normalized_volume
|
||||||
|
return normalized_volume
|
||||||
|
except Exception as e:
|
||||||
|
session_logger.error(device_id, "volume", f"获取期望音量失败: {str(e)}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
await db_session.close()
|
||||||
|
|
||||||
async def set_volume(self, device_id: str, volume: int) -> bool:
|
async def set_volume(self, device_id: str, volume: int) -> bool:
|
||||||
"""设置设备音量并保存到数据库"""
|
"""设置设备音量并保存到数据库"""
|
||||||
@@ -59,7 +80,7 @@ class DeviceVolumeManager(DatabaseServiceBase):
|
|||||||
update_stmt = (
|
update_stmt = (
|
||||||
update(DeviceConfig)
|
update(DeviceConfig)
|
||||||
.where(DeviceConfig.device_id == device_id)
|
.where(DeviceConfig.device_id == device_id)
|
||||||
.values(volume=volume)
|
.values(volume=volume, last_update_time=time.time())
|
||||||
)
|
)
|
||||||
await db_session.execute(update_stmt)
|
await db_session.execute(update_stmt)
|
||||||
else:
|
else:
|
||||||
|
|||||||
89
talkingq-url/tests/test_device_volume_sync.py
Normal file
89
talkingq-url/tests/test_device_volume_sync.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from handlers.mqtt_handler import TalkingQMQTTService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sync_desired_volume_republishes_when_reported_volume_differs(monkeypatch):
|
||||||
|
service = TalkingQMQTTService({"device_prefix": "TalkingQ"})
|
||||||
|
sent_commands = []
|
||||||
|
|
||||||
|
class FakeDeviceVolumeManager:
|
||||||
|
async def get_configured_volume(self, device_id):
|
||||||
|
assert device_id == "TalkingQ_XQSN00001001"
|
||||||
|
return 70
|
||||||
|
|
||||||
|
async def fake_send_volume_command(device_id, level):
|
||||||
|
sent_commands.append((device_id, level))
|
||||||
|
return "002"
|
||||||
|
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.device_volume_manager", FakeDeviceVolumeManager())
|
||||||
|
monkeypatch.setattr(service, "send_volume_command", fake_send_volume_command)
|
||||||
|
|
||||||
|
await service._sync_desired_volume("TalkingQ_XQSN00001001", 30)
|
||||||
|
|
||||||
|
assert sent_commands == [("TalkingQ_XQSN00001001", 70)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sync_desired_volume_skips_when_reported_volume_matches(monkeypatch):
|
||||||
|
service = TalkingQMQTTService({"device_prefix": "TalkingQ"})
|
||||||
|
sent_commands = []
|
||||||
|
|
||||||
|
class FakeDeviceVolumeManager:
|
||||||
|
async def get_configured_volume(self, device_id):
|
||||||
|
return 70
|
||||||
|
|
||||||
|
async def fake_send_volume_command(device_id, level):
|
||||||
|
sent_commands.append((device_id, level))
|
||||||
|
return "002"
|
||||||
|
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.device_volume_manager", FakeDeviceVolumeManager())
|
||||||
|
monkeypatch.setattr(service, "send_volume_command", fake_send_volume_command)
|
||||||
|
|
||||||
|
await service._sync_desired_volume("TalkingQ_XQSN00001001", "70")
|
||||||
|
|
||||||
|
assert sent_commands == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_device_service_persists_desired_volume_before_mqtt(monkeypatch):
|
||||||
|
from banban.service.device import DeviceService
|
||||||
|
|
||||||
|
service = DeviceService()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
async def fake_ensure_device_access(*, device_id, user_id):
|
||||||
|
calls.append(("access", device_id, user_id))
|
||||||
|
|
||||||
|
class FakeDeviceVolumeManager:
|
||||||
|
async def set_volume(self, device_id, level):
|
||||||
|
calls.append(("persist", device_id, level))
|
||||||
|
return True
|
||||||
|
|
||||||
|
class FakeMQTTService:
|
||||||
|
async def send_volume_command(self, device_id, level):
|
||||||
|
calls.append(("mqtt", device_id, level))
|
||||||
|
return "002"
|
||||||
|
|
||||||
|
class FakeTalkingQMQTTService:
|
||||||
|
@classmethod
|
||||||
|
async def get_instance(cls):
|
||||||
|
return FakeMQTTService()
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "ensure_device_access", fake_ensure_device_access)
|
||||||
|
monkeypatch.setattr("banban.service.device.device_volume_manager", FakeDeviceVolumeManager())
|
||||||
|
monkeypatch.setattr("handlers.mqtt_handler.TalkingQMQTTService", FakeTalkingQMQTTService)
|
||||||
|
|
||||||
|
msg_id = await service.set_device_volume(
|
||||||
|
device_id="TalkingQ_XQSN00001001",
|
||||||
|
user_id=1,
|
||||||
|
level=70,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert msg_id == "002"
|
||||||
|
assert calls == [
|
||||||
|
("access", "TalkingQ_XQSN00001001", 1),
|
||||||
|
("persist", "TalkingQ_XQSN00001001", 70),
|
||||||
|
("mqtt", "TalkingQ_XQSN00001001", 70),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user