编注:本文是「我的 2018 年度关键词」年度征文活动的第 5 篇入围文章,本文仅代表作者本人观点,少数派对标题和排版略作调整。
想了解如何参与本次征文,赢取各种丰厚奖品,你可以 点此查看 活动规则和奖品清单。
前言
在2017年年底,我给自己的2018年定了一个挑战,那就是尝试学习一门编程语言。在之前,这对我是一片空白。这个个人挑战,既不追求有什么具体的目标,也不会对我的工作有什么直接的帮助,纯粹是要逼自己锻炼自学的能力,拓展一下 scope。
18年年底逐渐来临,一个 idea 逐渐孕育。
作为一个晚睡癌者,虽然我已经特地租到离公司只有15分钟步行时间的地方,但随着我睡过闹钟的病症越来越严重,我有了一个想法:
能不能有这样一个闹钟?设定在每天的起床 deadline 上,到点就能够智能检测我是不是在赖床;一旦发现我又要迟到,就放摇滚音乐把我叫醒。
在2018年的 Black Friday,我托朋友海淘了一个 Amazon Echo Dot,燃起了我要 hack 一下这个小东西的热情。在18年最后的两个星期,我决定抡起袖子开干。
中途一度觉得搞不定,但熬了一轮夜、掉了一轮头发之后,搞出来的效果居然还不错,效果请戳:
今天就和广大晚睡癌病友们分享下这个项目。
方案研究
一开始的方案设想很简单:
但是,经过一番研究,我还是没有找到方法 hack 进 Alexa。不过,作为一名灵活的产品经理,我决定 work around 一下,改成下面这样子依然能上线跑噢:
经过一番研究,我确定了这个项目所需的软硬件方案。基于 MVP 原则(简单来说就是万一搞不成了不要浪费太多钱和时间的原则),我从床底下拿出了尘封已久的安卓机皇:oneplus one
下面就是初步列出的方案:
模块 | 实现方式 | 所需硬件/软件 |
---|---|---|
整体流程控制 | Home Assistant | 一台能够运行 Linux 的机器,例如一台安卓手机 |
视觉识别 | 摄像头(local)+深度学习模型接口(server) | 一台有摄像头的机器,例如一台安卓手机;一台能够部署模型并暴露接口的VPS |
音频 | Text-to-speech | 带麦克风的机器,例如一台安卓手机 |
于是流程图,又可以继续细化。
这个在2018年年尾才确定的项目,我觉得是一个很好的选择。因为麻雀虽小,但贯穿了过去一年的所有重要知识点。
模块 | 知识点 | 技能点 |
---|---|---|
部署 Home Assistant | Linux 基础知识 | 开源项目理解,以及文档查阅能力 |
Home Assistant 自动化开发 | Python + 硬件基础理解 | 业务流程设计能力 |
图像识别 | 深度学习模型训练 | 机器学习的基础理解,以及开源的优秀框架运用 |
Client-Server 交互 | 深度学习模型部署API接口 | 对API接口交互的理解 |
像很多新手一样,2017年底刚开始学习时,我模糊地以为我的学习重点是 Python 这门语言。但慢慢走下去,才发现具体的语言本身毫不重要,像上面所展示的知识点,完全可以把语言抽离出来,更多是编程利于的框架性理解。一年过后更重要的收获,是我对互联网在实现层面的理解更深了。
分享科技相关资讯的网站有不少,但我一直觉得少数派还是一个小小的极客社区:对比较 hardcore 而相对枯燥的内容很宽容,社区内也很鼓励动手能力。我不相信编程是一种囿于职业、专业的技能。只要愿意学习,每个人都几乎可以学习任何事情,编程自然也绝不例外。不过,虽然编程不是少数人的特权,但肯学习和动手实践的人总是少数派。
这次项目的分享,会分为两篇,可能还是会比较 hardcore 和枯燥:
- 第一篇围绕 Home Assistant 的搭建;
- 第二篇围绕用深度学习模型完成图像分类预测的实现。
本文为第一篇,大部分的篇幅会和 Home Assistant 相关。我会在前半部分介绍比较浅显和有趣的内容,尽量面向所有读者,感兴趣的朋友看完前半部分,有了大体的了解就差不多了。而后半部分则会比较复杂,涉及 Home Assitant 的开发部分,代码也会出现比较多。普通读者可能会感到头疼,这不要紧,因为只是供打算动手做设计的朋友参考的。
什么是 Home Assistant?
Home Assistant 是一个开源的智能家居自动化平台。它特点是运行在局域网内的设备上,相对比较注重隐私。
智能家居的最大问题之一,就是标准无法统一。在开源社区的努力下,已经支持1000多种硬件和软件接入。接入的软硬件,可以统一串联起来,设置自动化流程。包括我手头上零零碎碎的硬件也是各个品牌的,基于这个平台的特点,我选择使用该开源项目作为主要的开发平台。
Home Assistant 核心概念理解:组件、服务、状态
在 Home Assistant 设计自动化中,我们总是在和这几个关键概念打交道:
- 组件:可以理解为 HA 允许接入的硬件或者软件服务,例如小米的灯(硬件)、摄像头(硬件)、麦克风(硬件)、脚本传感器(软件)、甚至自己编写的 custom component(软件,这个也是我们这次项目中主要使用到的能串起整个流程的核心组件)
- 服务:一个组件,会对外提供服务,例如一盏灯可以提供开关灯的服务,一个麦克风可以提供说话的服务。自己编写的一些脚本组件,也可以对外提供触发的服务
- 状态:一盏灯在当前是否开启着的,这是一个状态;
智能家居是一个非常复杂的场景,Home Assistant 作为一个智能家居平台,在设计之初就要考虑到开放性和复杂性。组件、服务、状态,这三个概念相当简洁,又全面,确实是非常优雅的设计。
HA 模块拆解
承接上面对核心概念的概念,我们终于可以把项目中设计到的功能点,在 Home Assistant 的框架中进行拆解。
模块 | 组件(component) | 涉及服务 |
---|---|---|
在面板上可以输入时间的地方 | Input Datetime(输入时间) | 不涉及 |
判断当前时间是否为工作日&当前时间 | Shell Command(命令行) | 暴露一个检查当前时间的服务 |
判断&闹钟主体(视觉部分) | Android IP webcam(摄像头) | snapshot 拍照服务 |
判断&闹钟主体(闹钟部分) | TTS (text-to-speech) + Kodi(麦克风) | google.say 用上google 文字转音频服务 |
实战篇
现在开始进入到复杂的部分,会设计到 Home Assistant 的开发模块,也会涉及比较多的代码。抛砖引玉,供想要动手调教的朋友参考。
如何部署 Home Assistant?
本质上来讲,可以部署在任何 Linux 的环境中,包括树莓派、Macbook、以及任意可以运行 Linux 的设备上。如果你使用树莓派,建议参阅 @墨澜 之前分享的一系列教程,特别是 Home Assistant + 树莓派:强大的智能家居系统 · 安装篇
我手上刚好有一台旧的安卓手机,就想办法直接在上面部署了,参考分享帖 Ubuntu安装HomeAssistant教程 步骤如下:
- 安装 Linux Deploy APP,在家里的 Wifi 下启动
- 使用你的电脑 ssh 连入安卓终端,安装 Python3、pip 等工具
- 激活一个 Python3 的虚拟环境
- 安装 Home Assistant
pip install homeassistant
- 启动 Home Assistant
hass --open-ui
然后,用你的浏览器访问
<存管服务器的IP>:8123
应该就可以正常访问。
文档研究基础篇:如何查询、添加组件
当 HA 成功运行后,要添加组件都在主目录的 configuration.yaml
中进行编辑和添加。
朋友们,让我们打开文档官方文档:组件,可以在这里搜索上千样软硬件的接入方法。
例如,Text-to-speech,只需要在configuration.yaml
中添加下面这些代码:
# yaml 文件的关键点在于缩进要正确
# Text to speech
tts:
- platform: google
配置我的麦克风,只需要这样添加:
# Host 是 IP, 要看你的设备当前在局域网内是啥 IP
# Media player
media_player:
- platform: kodi
name: speaker
host: 192.168.0.106
文档中还会介绍如何接入小米、Echo等设备,大家可以多多探索。
文档研究高级篇:如何用 script 写稍微复杂的自动化
一般的自动化,我们可以使用 HA 自带的自动化模块进行设计,这里也可以参考文档 官方文档:自动化
但在我们这个场景下,自动化流程过于复杂了,所以我们要开始了解如何用编写脚本的方式完成自动化。
- Shell command 命令行
这个组件,可以把命令行封装成一个服务。命令行可以执行脚本,这样一来,我们就可以执行复杂一点的流程了。而且,更重要的是,我们可以通过这个形式,突破 HA 的沙盒模型,例如导入第三方 Python 包,以实现各种可能性。
命令行相关的组件以及其功能
组件 | 作用 |
---|---|
Shell Command | 简单地提供一个 Service,调用之后可以执行输入的命令行 |
Command Line Binary Sensor | 本质上是一个二元传感器(就是说只有On/Off两种状态),根据命令行返回的结果,设置传感器的状态 |
Command Line Cover | 本质上应该是一个类似车库门这样的可以上下左右控制的东西,但没有怎么了解,因为我首先还没有车… |
Command Line Notify | 本质上是一个消息服务,不过可以使用命令行 |
Command Line Sensor | 本质上是一个传感器,可以通过命令行返回的内容定制各种各样的东西 |
Command Line Switch | 本质上是一个开关,当它开启或者关闭的时候,可以触发不同的命令行 |
关于 Shell command 的高级用法还可以参考:Home Assistant + 树莓派:强大的智能家居系统 · 高级篇一
这里顺带提一下 Python Scripts 这个组件,它其实和 Shell Command 比较像,就是直接把使用 Python 写的脚本封装成一个可以调用的服务,对于习惯写 Python 的朋友应该稍微有好点。但请注意,这里是在 沙盒环境(sandbox environment) 中,不能直接调用第三方包。
- Custom component 定制化组件
使用 Custom component 可以按照自己的流程,去定制流程。
以这个项目为例,我将会定制一个叫做 Smart Alarm 的组件,并接入系统,它如果被触发:
- 首先,会开启摄像头拍一张照片
- 其次,会分析这张照片中,我是否还在床上
- 如果是,则让麦克风说话;如果不是,就保持沉默
这样一段流程,用定制化的方式来写,就非常非常简单了。不过,它的缺点也很明显:
- 语言是基于 Python 的,对 Python 的要求比较高
- 位于沙盒模型中,只能使用 HA 内部的 API
因此,不作赘述,感兴趣的同学可以自己阅读文档:官方文档:自己动手创建一个虚拟组件
文档研究超级复杂篇:什么是沙盒环境,以及脚本如何跨设备联动
这一小节的内容非常 hardcore 和复杂,仅仅是描述下自己踩过的坑,感兴趣的同学可以留心下。在使用 Python 编写脚本的时候,非常重要的一点,是要知道现在是否位于沙盒环境中。这是一种安全策略,在这个环境内,基本上意味着你只能使用 HA 内部定义的 API,不能导入第三方。
组件 | 环境 | 局限性 | 跨设备联动方案 |
---|---|---|---|
Shell Command(以及上面列出的相关的组件) | 无限制,当前 Linux 所在的环境 | 脚本执行是一次性的,没有什么逻辑判断在里面,无法定义复杂的自动化 | 利用 HA 对外提供的 REST API 服务 |
Python Scripts | 沙盒环境 | 只能使用 HA 自己的 API | 利用 HA 对内的 API 服务 |
Custom component | 沙盒环境 | 只能使用 HA 自己的 API | 利用 HA 对内的 API 服务 |
划重点:而在这个智能闹钟项目中,判断当前是否在工作日会需要导入 chinese_calendar 这个包,以及触发照片判断会用到 requests,都一定会使用到第三方包,所以一定要把他们用 Shell Command 封装成两个单独的服务!
# command line sensor,工作日和时间判断
binary_sensor:
- platform: command_line
command: 'python3 /home/android/.homeassistant/python_scripts/working_time_check.py'
name: if_worktime_check
payload_on: 'Working time'
payload_off: 'Resting time'
# command line script,触发照片判断
shell_command:
alarm_check: 'python3 /home/android/.homeassistant/python_scripts/alarm_check.py '
HA 内部 API
该内部方法,可以允许 HA 内沙盒环境内的脚本与 HA 的设备进行交互。
HA 外部 REST API
这个方法,允许 HA 体系外的脚本,能够与 HA 体系内的设备进行交互。但是,这里一定要提前获取令牌,不然请求会失败。实际上,这也是一种安全的措施。你肯定不想自己的设备被其他人入侵。查看官方文档:发出请求前要先授权。
首先是授权,获取令牌
请求时,该令牌会需要放置在请求头中,参数名为 Authorization
# 官方样例,这里的 ABCDEFGH 就是令牌内容,Bearer 请保留。创建你个人令牌后,请如法炮制。
import requests
url = "https://your.awesome.home/api/error/all"
headers = {
'Authorization': "Bearer ABCDEFGH",
}
response = requests.request('GET', url, headers=headers)
print(response.text)
因为我也是新手,对文档的理解也很浅。所以只能在这里简单分享下我整理的信息,以及常见的几种场景,感兴趣的同学可以再去钻研文档:
HA 内部 API | HA 外部 REST API | |
---|---|---|
使用方法 | 使用 hass 对象 | 用 requests 的 GET/POST 处理 |
注意事项 | 除了 hass 对象以外,还有一堆帮助方法,例如监听某个状态是否有变更 | 请求头中,要带上提前获取的 Token |
文档地址 | 官方文档:Hass对象入门 | 官方文档:外部REST API |
-
获取状态
-
内部 API:
hass.states.get(entity_id)
# 例如:获取前端设置的时间值 TIMING = hass.states.get('input_datetime.smart_alarm_timing')
-
外部 REST API:用
get
请求http://localhost:8123/api/states/entity_id
# 例如:获取前端设置的时间值 headers = { 'Accept': '*/*', 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer 后面接着前面所说的Token', } response = requests.get('http://localhost:8123/api/states/input_datetime.smart_alarm_timing', headers=headers)
-
-
改变状态
-
内部 API:
hass.states.set(entity_id, state)
# 例如:恢复传感器 am_I_still_in_bed 状态为 'empty' hass.states.set('sensor.am_I_still_in_bed', 'empty')
-
外部 REST API:用
post
带上要设置的状态参数请求http://localhost:8123/api/states/entity_id
# 例如:设置传感器 am_I_still_in_bed 状态为 'occupied' headers = { 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer 后面接着前面所说的Token', } data = '{"state": "occupied"}' response = requests.post( 'http://localhost:8123/api/states/sensor.am_I_still_in_bed', headers=headers, data=data)
我还要特别推荐两个官方用例:
例子一、用 Python 编写虚拟组件
例子二、有人侵入时触发警报灯
-
HA 部分代码分享
我还得先想想,这里怎么分享。
感谢
作为面向 Google 编程的选手,我必须要感谢下社区内分享过大量 HA 经验的作者 @墨澜 ,我在文中频繁地引用了她的文章,就是因为她的一系列文章都非常全面,还翻译了 HA 的中文文档。Hats off to her!
下一篇,会围绕项目中另一块硬骨头:视觉识别模型。
欢迎在评论留下你的看法,谢谢大家。
关联阅读:下篇•用深度学习为闹钟提供视觉能力
> 下载少数派 客户端、关注 少数派公众号,找到数字时代更好的生活方式 🥳
> 特惠、好用的硬件产品,尽在 少数派sspai官方店铺🛒
今年,你除了可以参加年度征文活动,赢取 iPad Pro、Kindle Oasis、戴森吸尘器、HomePod 等丰厚奖品,也可以去微博参与「我的 2018 年度瞬间」有奖摄影活动,从今年拍摄的照片里,选出令你印象最深刻的一张。我们同样准备了 GoPro Hero 7 Black、富士 instax 照片打印机等奖品等你来拿。