打造自主、安全、可控的摄像头:我的智能家居监控方案 - 少数派

Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。 

文章代表作者个人观点,少数派仅对标题和排版略作修改。

最近买了一个摄像头,放在家里看宠物,稍微折腾了下,目前算是比较满意了。

如果你对家庭监控的安全性和隐私性要求比较高,可以参考下我的方案。如果不想折腾,建议直接用原厂的 App 和云服务。

买了个支持 ONVIF 协议的 IP 摄像头,通过 ONVIF 协议接入和群晖的监控 Surveillance Station,摄像头进行 24H 监控,视频存储在群晖外挂的 USB 硬盘上,配合 Home Assistant 和 Node-RED,实现了一些动静通知、回家离家自动开关等自动化的功能。摄像头断开外网访问,只能在家里内网或者 VPN 访问,保证了视频的安全性。

几个月我写了一篇文章《脱钩: 我的个人网络安全策略》,分享我与中国互联网服务和产品脱钩的一些经验。

在那篇文章中,我介绍了我已经把大多数据资产都转移到了境外服务或者下云,主要目的就是为了保护自己的隐私和数据安全。

对「智能家居」来说,脱钩或者下云是不实际的,我是小米生态的重度用户,门锁和门禁摄像头,都是小米生的产品,这些产品基本都强依赖于小米的云服务。入住新家两年以来,这种基于云的服务,的确也带来了很多便利。

摄像头不同于其他智能设备,这是一个极其敏感的,打破了「网络」与「现实」边界的设备,他可以直接看到、听到镜头里的一切。

好几年前,我通过一个业内的渠道,得知某摄像头的云存储服务,曾经将用户的监控视频未加密处理,就直接存在某公有云的 bucket 里。从那之后,我就再也没在家里开启过摄像头。

前几年,众多在各种乱七八糟网站上泄漏出来的视频,想必大家也都有所耳闻。基于上面种种考量,我决定自建监控方案。首先说下我的这套监控方案的优缺点。

这次除了摄像头是新购的,其他都是原有的老设备。这里的花销大头是群晖的 NAS,理论上如果你有其他 NAS,或者开源的软路由什么的,也能实现类似的功能。无非就是需要花费更多时间折腾罢了。

上面就是我的这套监控系统的核心架构,除了 SurveillanceStation 属于傻瓜级上手可用,后面的 Home Assistant 和 Node-Red 都需要花费一定的时间和学习。由于我之前就有一些 HA 和 Node-RED 的使用经验,这次只是集成下 SurveillanceStation,所以整体花费的时间还是比较少的。

搭建自己的监控体系,基本的要求就是摄像头需要支持 ONVIF 协议或者 rtsp 协议,这是安防设备的通用协议,方便用户自己集成到各种系统中。

购买摄像头之前,需要务必确认摄像头支持 ONVIF 协议,小米的许多摄像头就不支持。

这次我购买的摄像头是 TP-Link 的 TL-IPC48AW,属于比较经典的系列。

硬伤是 Wi-Fi 不支持 5G。除了基础的硬件功能,还有各种云服务支持,AI 检测什么的,但是由于我也没打算用自带的 App(美区 App Store 居然还没 TP-Link App) ,所以也无所谓了。

防止摄像头能访问外网,首先就是关闭摄像头的外网访问权限,仅限内网用户访问。

按照以下步骤设置即可:

#TP-Link 摄像头 防火墙规则iptables -I FORWARD  -m mac --mac-source E4:DD:66:82:DE:F4 -j DROPiptables -I FORWARD -s 10.0.0.12 -d 10.0.0.0/16 -j ACCEPTiptables -I FORWARD -s 10.0.0.0/16 -d 10.0.0.12 -j ACCEPTiptables -I FORWARD -s 10.0.0.12 -j REJECT

我家使用的是小米路由器,网关使用的是 OpenWrt,除了在小米的控制台关闭外网权限,同时在防火墙设置里,通过添加过滤摄像头 IP 和 MAC 地址的规则,禁止摄像头访问外网。

设置完毕后,可以看到这个时候再访问 TP-Link 自带的App,提示是设备离线状态。但是如果你是在局域网通过内网 IP 访问,依旧可以正常访问。

将家庭内网暴露到外网,或者内网穿透,是一个与「监控」系统本身不太相关的问题。

从安全的角度考量,我不希望摄像头直接暴露在外网,所以我只是在有需要的时候,通过 zerotier 来访问家里的监控系统。实际上还有一些其他方案,比如 WireGuard 等,这里就不做讨论了。

群晖的 SurveillanceStation 是一个非常成熟的监控套件,这个也是我们的监控系统的核心。之所以选择群晖,是因为我家里有一台群晖。其他厂商也有类似的监控套件,也有第三方的开源解决方案,建议大家就自行研究了。

这部分的内容,以 ONVIF 群晖 NAS等关键词组合搜索,网上有很多教程,我在这里就不赘述了。

我的群晖 DS216+ 是双盘位,使用两块机械硬盘做了 RAID1。

由于监控的过程中一直会写入硬盘,会对硬盘寿命造成严重影响。强烈建议非专业需求,不要将监控视频与其他数据混存。

为了解决这个问题,我使用了一块没用的240G的SSD,通过USB3.0接口外接到群晖上,作为监控的存储盘。上图就是我目前实际挂载的样子,这样有个好处:

虽然这样做,不如直接将视频存储在 NAS 的主硬盘上保险,但是我的目的主要就是看看最近的视频,监控录制的视频也不需要长期存储,所以这样做对于我来说倒是一个更经济合理的选择。

由于群晖默认不允许外接硬盘作为存储盘,这里需要一定的小技巧,外接硬盘挂载成正常的文件路径,需要通过 SSH 登录到群晖,配合 ln -s 软链的方式挂载外接硬盘。

首先登录群晖的 SurveillanceStation,在「网络摄像机」>「编辑摄像机」>「录制」里添加新的存储器,这里默认是只能在群晖主硬盘空间上存储,这里我命名为 camera_storage。

接下来需要通过 SSH 登录到群晖,进入跟路径,找到当前挂载的盘符,这个时候可以看到外接的硬盘已经被挂载到了 /volumeUSB1/ 目录下。

# 通过 SSH 登录群晖,通过软链将外挂硬盘挂载到共享文件夹,突破群晖的限制ssh  [email protected] -s /volumeUSB1/usbshare/ /volume2/camera_storage#DSM 7.0 后,上述挂载的方法需要做一些调整,具体方法见下

接下来再去「编辑摄像机」>「录制」里,把录制存储器选择为刚刚创建的 camera_storage,这样后续监控视频都会默认直接保存在外接硬盘上了。

DSM 7.0 后,上述挂载的方法需要做一些调整,不能再通过软链接ln -s的方式映射盘符。简要说明新的步骤

如果想在 iPhone 的 Home App 里看到监控视频,需要通过 Home Assistant 将摄像头接入到 HomeKit。这一部分也有比较多的教程和文章,我找了几篇比较详细的,供大家参考。

在这里分享一个小优化点,HA 自带的 ONVIF 协议添加摄像头看到的实时画面延迟较大(我自己观察约5秒),如果使用 WebRTC Camera 取流可以大大降低延迟。具体的步骤可参考:

截止到一步,家庭监控的接入、录制、存储、实时监控、回放等功能都已经实现了。目前我主要就是通过群晖的 DS Cam 来进行实时监控和回放。

DS Cam 有网页版和 App 版,使用起来还是很方便的,在监控画面有动静的时候,在时间轴上也有标记,方便快速定位。

出门在外时,一般我就使用 App 查看监控,TP-Link 这个摄像头本身支持语音通话,但是通过ONVIF接入群晖这么一圈后,就没有了,暂时还没找到解决方案,没法远程逗狗了,算是一个小小的不方便。

同样的,通过 iOS 自动的 Home 也能看到监控画面,但是 HomeKit 的实时监控画面延迟也是比较大的,大概是每 10 秒刷新一次。通过 Home Assistant,则是可以实现更低延迟的监控画面。

接下来就是一些进阶的功能了,主要是为了解决监控动静提醒,以及离家在家模式的自动化管理。

首先思考下面几个问题?

现在不少人家里都有摄像头,有些摄像头是用来对着门口,有些摄像头对着客餐厅,有些摄像头对着儿童房的婴儿床。不同的位置,对于监控的需求是不一样的。

我家目前就装了一台摄像头,对着客厅和大门玄关入口。实际上,我并不太需要这个摄像头一直录制。毕竟有时候,我在家还是比较洒脱和坦荡,我不希望自己的一举一动都被摄像头记录下来。

这个时候,就可以通过 SurveillanceStation、Home Assistant 和 Node-RED 来实现一些自动化的功能了。

一句话来介绍这个的原理:通过自定义规则,触发 webhook,来切换摄像头的开关和触发通知。

群晖 SurveillanceStation 支持自定义操作规则,在这里我设置了三个规则:

这个的原理也十分简单,以在家模式举例,通过设置事件源为 WebHook 外部设备,群晖暴露了一个 WebHook 接口,通过普通的网络请求触发这个接口,就可以关闭摄像头。

而动静通知模式,则是反过来的操作,当监控画面出现动静时,群晖会主动向我的第三方 Node-Red 定义的接口触发请求,Node-Red 会执行抓取当前画面、推送画面到 Telegram、Bark 等通知渠道的动作。

如果想做到更加智能和自动化,比如离开家时自动开启摄像头,这个时候就需要通过 Home Assistant 来实现了。

Home Assistant 里的「设置」>「自动化与场景」支持自己创建自动化。可以通过设置不同的 if then 逻辑来控制不同的设备和流程。

以我的需求为例,当我的手机离开家时(地理围栏),就会自动发送一条通知到我的手机,并请求我的群晖的 WebHook API 开启摄像头。

rest_command:  bark_push:    url: https://push.bark.demo/notification    method: POST    payload: '{"body": "{{ body }}","title": "{{ title }}","icon":"https://static.is26.com/share/icon-vm.png"}'    content_type: "application/json; charset=utf-8"    verify_ssl: true  camera_on:    url: http://10.0.0.5:5000/webapi/SurveillanceStation/Webhook/Incoming/v1?token=iQRJyuOYPIZODrWMRfT    method: GET  camera_off:    url: http://10.0.0.5:5000/webapi/SurveillanceStation/Webhook/Incoming/v1?token=iVufyERsmJZEM1UJ7M    method: GET

这只是我的一个简单的示例,同理,当你回到家时,希望什么时候关闭摄像头,也可以根据你的实际需求,实现更复杂的自动化功能。上面我使用到了 HA 自带的RESTful Command功能。有关Home Assistant的自动化RESTful Command配置,可以参考下面的文章。

有关「离家」和「在家」模式,可以参考下下面的讨论:

弄完上面的流程后,监控的大多需求都已经满足了,最后我们再来补全出现动静>截取摄像头画面>推送图像这个环节。

群晖本身自带的动静通知功能,也能通过 DS Cam 接受通知,这里我没有使用自动的方案,而是通过 Node-Red 来实现。

首先放流程图,整个的流程逻辑是这样的:

同理,最终的通知截图也可以使用钉钉、企业微信等其他渠道,我选择 Telegram 是因为之前已经集成了,加上 Telegram 对内容几乎完全没有限制,相对而言我更放心点。

复制下面的 JSON ,导入到 Node-Red,修正对应的账号、密码、IP、端口和 Telegram bot ChatId 即可

展开查看 Node-Red 流程配置文件

[  {    "id": "a07703f3752fc82a",    "type": "tab",    "label": "抓取群晖摄像头推送到Telegram",    "disabled": false,    "info": ""  },  {    "id": "3902801a557a738e",    "type": "www-request",    "z": "a07703f3752fc82a",    "name": "获取sid",    "method": "GET",    "ret": "obj",    "url": "",    "follow-redirects": true,    "persistent-http": true,    "tls": "",    "x": 880,    "y": 300,    "wires": [["cabd07b9c918bd13"]],    "inputLabels": ["输入"],    "outputLabels": ["输出"]  },  {    "id": "6e7a90e85b1d736b",    "type": "change",    "z": "a07703f3752fc82a",    "name": "设置群辉用户名、密码、摄像头ID",    "rules": [      {        "t": "set",        "p": "dsm_account",        "pt": "msg",        "to": "username",        "tot": "str"      },      {        "t": "set",        "p": "dsm_passwd",        "pt": "msg",        "to": "password",        "tot": "str"      },      {        "t": "set",        "p": "dsm_camera_id",        "pt": "msg",        "to": "1",        "tot": "str"      },      {        "t": "set",        "p": "dsm_domain",        "pt": "msg",        "to": "10.0.0.5",        "tot": "str"      },      {        "t": "set",        "p": "dsm_port",        "pt": "msg",        "to": "5000",        "tot": "str"      }    ],    "action": "",    "property": "",    "from": "",    "to": "",    "reg": false,    "x": 420,    "y": 240,    "wires": [["c18d4fe0633037a4"]]  },  {    "id": "833f9be33b550d84",    "type": "www-request",    "z": "a07703f3752fc82a",    "name": "GetSnapshot",    "method": "GET",    "ret": "bin",    "url": "",    "follow-redirects": true,    "persistent-http": true,    "tls": "",    "x": 890,    "y": 500,    "wires": [["45ea4313512d68a6", "1957d8308081700b"]]  },  {    "id": "c18d4fe0633037a4",    "type": "function",    "z": "a07703f3752fc82a",    "name": "url_sid",    "func": "msg.url = `http://${msg.dsm_domain}:${msg.dsm_port}/webapi/auth.cgi?api=SYNO.API.Auth&method=login&version=3&account=${msg.dsm_account}&passwd=${msg.dsm_passwd}&session=SurveillanceStation&&format=cookie`;nconsole.log(msg)nreturn msg;",    "outputs": 1,    "noerr": 0,    "initialize": "",    "finalize": "",    "libs": [],    "x": 690,    "y": 240,    "wires": [["3902801a557a738e"]]  },  {    "id": "f3df28b20f02dd3e",    "type": "function",    "z": "a07703f3752fc82a",    "name": "url_snapshot",    "func": "var sid;nsid = msg.payload.data.sid;nmsg.url = `http://${msg.dsm_domain}:${msg.dsm_port}/webapi/entry.cgi?version=9&id=${msg.dsm_camera_id}&api="SYNO.SurveillanceStation.Camera"&method="GetSnapshot"&profileType=0&_sid=${sid}`;nreturn msg;",    "outputs": 1,    "noerr": 0,    "initialize": "",    "finalize": "",    "libs": [],    "x": 690,    "y": 500,    "wires": [["833f9be33b550d84"]]  },  {    "id": "cabd07b9c918bd13",    "type": "switch",    "z": "a07703f3752fc82a",    "name": "",    "property": "payload.error.code",    "propertyType": "msg",    "rules": [      {        "t": "nnull"      },      {        "t": "else"      }    ],    "checkall": "true",    "repair": false,    "outputs": 2,    "x": 570,    "y": 400,    "wires": [["3902801a557a738e"], ["f3df28b20f02dd3e"]],    "inputLabels": ["输入"]  },  {    "id": "dcb9b48e6b3c1fee",    "type": "inject",    "z": "a07703f3752fc82a",    "name": "手动触发",    "props": [      {        "p": "payload"      },      {        "p": "topic",        "vt": "str"      }    ],    "repeat": "",    "crontab": "",    "once": false,    "onceDelay": 0.1,    "topic": "",    "payload": "",    "payloadType": "date",    "x": 140,    "y": 220,    "wires": [["6e7a90e85b1d736b"]]  },  {    "id": "45ea4313512d68a6",    "type": "image",    "z": "a07703f3752fc82a",    "name": "",    "width": "360",    "data": "payload",    "dataType": "msg",    "thumbnail": false,    "active": true,    "pass": false,    "outputs": 0,    "x": 1120,    "y": 500,    "wires": []  },  {    "id": "c8427c30a83ac775",    "type": "telegram sender",    "z": "a07703f3752fc82a",    "name": "",    "bot": "ff58d9095b6d2cda",    "haserroroutput": true,    "outputs": 2,    "x": 1420,    "y": 780,    "wires": [[], []]  },  {    "id": "1957d8308081700b",    "type": "function",    "z": "a07703f3752fc82a",    "name": "图像转TG格式",    "func": "msg.payload = {n    chatId: 10000, n    type: "photo",n    content: msg.payload,n    caption: "家里有动静,监控提醒"n};nreturn msg;n",    "outputs": 1,    "noerr": 0,    "initialize": "",    "finalize": "",    "libs": [],    "x": 880,    "y": 740,    "wires": [["c8427c30a83ac775"]]  },  {    "id": "02fe4781ca3fbb7b",    "type": "http in",    "z": "a07703f3752fc82a",    "name": "/api/camera/capture",    "url": "/api/camera/capture",    "method": "get",    "upload": false,    "swaggerDoc": "",    "x": 90,    "y": 280,    "wires": [["6e7a90e85b1d736b", "fb5663502a675986"]]  },  {    "id": "fb5663502a675986",    "type": "http response",    "z": "a07703f3752fc82a",    "name": "",    "statusCode": "200",    "headers": {      "code": "0"    },    "x": 600,    "y": 680,    "wires": []  },  {    "id": "ff58d9095b6d2cda",    "type": "telegram bot",    "botname": "拉布管家",    "usernames": "",    "chatids": "10000",    "baseapiurl": "",    "updatemode": "polling",    "pollinterval": "300",    "usesocks": false,    "sockshost": "",    "socksprotocol": "socks5",    "socksport": "6667",    "socksusername": "anonymous",    "sockspassword": "",    "bothost": "",    "botpath": "",    "localbotport": "8443",    "publicbotport": "8443",    "privatekey": "",    "certificate": "",    "useselfsignedcertificate": false,    "sslterminated": false,    "verboselogging": false  }]

有关这部分的参考文章:

配置好了之后,建议在群晖摄像头的事件检测那里再精确配置下动作检测区域和灵敏度,避免误报。

从购买摄像头到最终的监控系统搭建,整个过程大概花了我一个下午的时间。基本上所有的步骤,都能在网上找到详细的教程和资料。

除了上面所说的功能,理论上,只要想法够多,还能实现更多的功能,比如:

小小的捣鼓一番,获得一个功能基本完善,但是更安全和经济的自主监控方案,还是一次挺有趣的折腾过程。

> 下载少数派 客户端、关注 少数派公众号,了解更多的新玩意 🆒

> 特惠、好用的硬件产品,尽在 少数派 sspai 官方店铺 🛒

推荐文章
友情链接