多宠物家庭智能管理平台:云端架构与多设备协同实战

系列第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
相关推荐
Li Ming&1 小时前
Python办公自动化:利用Python批量将PDF转换为图片文件
python·pdf·pip
Anova.YJ1 小时前
AI Notebook
人工智能·python·机器学习
To_OC2 小时前
LC 22 括号生成:刷完这道题,我终于搞懂回溯剪枝了
javascript·算法·leetcode
疯狂成瘾者2 小时前
Java 常见集合方法
java·windows·python
web守墓人2 小时前
【python】uv解决依赖安装缓慢的问题
开发语言·python·uv
asdzx672 小时前
使用 Python 精准操控 Word 字体:获取与替换方案
python·c#·word
胡耀超2 小时前
固件逆向:从一坨二进制,到一个可以被解释的系统
python·逆向工程·硬件安全·二进制分析·电子数据取证·嵌入式安全·固件逆向
To_OC2 小时前
LC 39 组合总和:回溯入门必刷题,我踩过的两个坑都在这了
javascript·算法·leetcode
数字杂技师2 小时前
每条推荐都很准,为什么我还是越刷越无聊?
算法