系列第8篇 | 从单设备监控到多宠物家庭的完整管理平台建设
前言
当一个家庭养了3只猫、2只狗,还有一只仓鼠时,单摄像头方案显然不够用了。你需要的是一个多设备、多宠物、多用户的统一管理平台。本文将从云端架构设计、设备管理、数据聚合、权限控制四个维度,手把手教你搭建一个可扩展的多宠物管理平台。
一、整体架构设计
1.1 系统架构图
┌─────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ 移动App │ │ Web管理台 │ │ 智能音箱/手表 │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ └──────────────┼─────────────────┘ │
├──────────────────────┼───────────────────────────────┤
│ API Gateway (Kong/Nginx) │
├──────────────────────┼───────────────────────────────┤
│ 服务层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │设备管理 │ │宠物管理 │ │告警服务 │ │用户服务 │ │
│ │Service │ │Service │ │Service │ │Service │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬────┘ │
├───────┼────────────┼────────────┼─────────────┼──────┤
│ 数据层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │PostgreSQL│ │Redis │ │InfluxDB │ │MinIO/S3 │ │
│ │业务数据 │ │缓存/实时 │ │时序指标 │ │视频/图片│ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
├──────────────────────────────────────────────────────┤
│ 设备层 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │摄像头1 │ │摄像头2 │ │喂食器 │ │智能猫砂 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
└──────────────────────────────────────────────────────┘
1.2 技术选型
| 组件 | 技术选型 | 选型理由 |
|---|---|---|
| API服务 | Go + Gin | 高并发、低内存,适合IoT场景 |
| 消息队列 | NATS | 轻量级,支持MQTT桥接,延迟低 |
| 时序数据库 | InfluxDB 2.x | 专为IoT指标设计,写入性能优异 |
| 关系数据库 | PostgreSQL | 支持JSONB,适合灵活的设备元数据 |
| 对象存储 | MinIO / 阿里云OSS | 视频片段和图片存储 |
| 实时通信 | WebSocket + WebRTC | 视频流和实时通知 |
二、设备管理服务
2.1 设备注册与发现
每个设备通过唯一DeviceID注册,支持自动发现局域网设备:
python
# device_registry.py
import uuid
import json
from datetime import datetime
from dataclasses import dataclass, field, asdict
from enum import Enum
from typing import Optional
class DeviceType(Enum):
CAMERA = "camera"
FEEDER = "feeder"
LITTER_BOX = "litter_box"
WATER_FOUNTAIN = "water_fountain"
DOOR_FLAP = "door_flap"
WEARABLE = "wearable"
class DeviceStatus(Enum):
ONLINE = "online"
OFFLINE = "offline"
UPDATING = "updating"
ERROR = "error"
@dataclass
class DeviceInfo:
device_id: str
device_type: DeviceType
name: str
location: str # 如 "客厅", "卧室"
firmware_version: str = "1.0.0"
ip_address: Optional[str] = None
mac_address: Optional[str] = None
status: DeviceStatus = DeviceStatus.OFFLINE
capabilities: list = field(default_factory=list)
bound_pets: list = field(default_factory=list)
last_heartbeat: Optional[datetime] = None
metadata: dict = field(default_factory=dict)
class DeviceRegistry:
"""设备注册中心 - 管理所有宠物监控设备"""
def __init__(self, redis_client):
self.redis = redis_client
self.prefix = "device:"
def register(self, device: DeviceInfo) -> bool:
"""注册新设备"""
device_key = f"{self.prefix}{device.device_id}"
data = asdict(device)
# Enum序列化
data["device_type"] = device.device_type.value
data["status"] = device.status.value
data["registered_at"] = datetime.utcnow().isoformat()
# 使用Hash存储设备信息
self.redis.hset(device_key, mapping={
k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
for k, v in data.items()
})
# 设置设备在线状态,TTL 60秒(心跳超时)
self.redis.setex(f"device:online:{device.device_id}", 60, "1")
# 添加到设备索引
self.redis.sadd("devices:all", device.device_id)
self.redis.sadd(f"devices:type:{device.device_type.value}", device.device_id)
return True
def heartbeat(self, device_id: str) -> bool:
"""设备心跳上报"""
self.redis.setex(f"device:online:{device_id}", 60, "1")
self.redis.hset(f"{self.prefix}{device_id}",
"last_heartbeat", datetime.utcnow().isoformat())
return True
def get_online_devices(self) -> list:
"""获取所有在线设备"""
all_devices = self.redis.smembers("devices:all")
online = []
for did in all_devices:
if self.redis.exists(f"device:online:{did}"):
info = self.redis.hgetall(f"{self.prefix}{did}")
online.append(info)
return online
def bind_pet(self, device_id: str, pet_id: str):
"""将宠物绑定到设备"""
self.redis.sadd(f"device:pets:{device_id}", pet_id)
self.redis.sadd(f"pet:devices:{pet_id}", device_id)
def get_device_pets(self, device_id: str) -> list:
"""获取设备关联的宠物列表"""
return list(self.redis.smembers(f"device:pets:{device_id}"))
2.2 心跳检测与离线告警
python
# heartbeat_monitor.py
import asyncio
import logging
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class HeartbeatMonitor:
"""设备心跳监控 - 检测设备离线并触发告警"""
def __init__(self, redis_client, notification_service):
self.redis = redis_client
self.notifier = notification_service
self.check_interval = 30 # 每30秒检查一次
self.offline_threshold = 120 # 120秒无心跳视为离线
async def start(self):
"""启动心跳监控循环"""
logger.info("心跳监控服务启动")
while True:
await self._check_all_devices()
await asyncio.sleep(self.check_interval)
async def _check_all_devices(self):
devices = self.redis.smembers("devices:all")
for device_id in devices:
device_id = device_id.decode() if isinstance(device_id, bytes) else device_id
is_online = self.redis.exists(f"device:online:{device_id}")
if not is_online:
# 检查是否已经标记离线(避免重复告警)
already_offline = self.redis.get(f"device:offline_flag:{device_id}")
if not already_offline:
await self._handle_device_offline(device_id)
async def _handle_device_offline(self, device_id: str):
"""处理设备离线事件"""
logger.warning(f"设备 {device_id} 离线")
# 标记离线,避免重复告警
self.redis.setex(f"device:offline_flag:{device_id}", 3600, "1")
# 获取设备信息和关联宠物
device_info = self.redis.hgetall(f"device:{device_id}")
bound_pets = self.redis.smembers(f"device:pets:{device_id}")
# 构建告警消息
pet_names = []
for pet_id in bound_pets:
pet_name = self.redis.hget(f"pet:{pet_id}", "name")
if pet_name:
pet_names.append(pet_name.decode())
device_name = device_info.get(b"name", device_id)
message = (
f"⚠️ 设备离线告警\n"
f"设备: {device_name}\n"
f"关联宠物: {', '.join(pet_names) or '无'}\n"
f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
f"请检查设备电源和网络连接"
)
# 获取设备所属用户并发送通知
user_id = device_info.get(b"user_id", b"").decode()
if user_id:
await self.notifier.send(user_id, message, priority="high")
三、多宠物数据聚合
3.1 宠物行为数据模型
python
# pet_data_aggregator.py
from dataclasses import dataclass
from datetime import datetime, date, timedelta
from typing import Optional
from enum import Enum
class ActivityType(Enum):
SLEEPING = "sleeping"
EATING = "eating"
PLAYING = "playing"
WALKING = "walking"
RESTING = "resting"
GROOMING = "grooming"
LITTER_USE = "litter_use"
DRINKING = "drinking"
@dataclass
class PetActivity:
pet_id: str
activity_type: ActivityType
start_time: datetime
end_time: Optional[datetime] = None
duration_seconds: int = 0
source_device: str = ""
confidence: float = 0.0
metadata: dict = None
class PetDataAggregator:
"""多宠物数据聚合服务 - 汇总各设备数据生成宠物健康报告"""
def __init__(self, influx_client, pg_client):
self.influx = influx_client
self.pg = pg_client
def record_activity(self, activity: PetActivity):
"""记录宠物行为到时序数据库"""
point = {
"measurement": "pet_activity",
"tags": {
"pet_id": activity.pet_id,
"activity": activity.activity_type.value,
"device": activity.source_device,
},
"fields": {
"duration": activity.duration_seconds,
"confidence": activity.confidence,
},
"time": activity.start_time,
}
self.influx.write_api.write(bucket="pet_monitor", record=point)
def get_daily_summary(self, pet_id: str, target_date: date = None) -> dict:
"""获取单只宠物每日行为摘要"""
if target_date is None:
target_date = date.today()
start = datetime.combine(target_date, datetime.min.time())
end = start + timedelta(days=1)
query = f'''
from(bucket: "pet_monitor")
|> range(start: {start.isoformat()}Z, stop: {end.isoformat()}Z)
|> filter(fn: (r) => r["pet_id"] == "{pet_id}")
|> filter(fn: (r) => r["_field"] == "duration")
|> group(columns: ["activity"])
|> sum()
'''
results = self.influx.query_api().query(query)
summary = {}
for table in results:
for record in table.records:
activity = record.values.get("activity", "unknown")
summary[activity] = record.get_value()
return {
"pet_id": pet_id,
"date": target_date.isoformat(),
"activities": summary,
"total_active_minutes": sum(summary.values()) / 60,
}
def get_multi_pet_comparison(self, household_id: str, days: int = 7) -> dict:
"""多宠物行为对比分析"""
pets = self._get_household_pets(household_id)
comparison = {}
for pet in pets:
daily_summarie