无论是否常(实)用、不管是虚拟的还是实体的,语音助手,无疑正在成为一股潮流。本次 WWDC 一个看点也是苹果会否推出智能音箱,或是对 Siri 叕一次升级,不过在此之前,我们依然可以做些什么,让 Siri 对你「言听计从」(目前不包括 Mac 上的 Siri,因为没有接入 HomeKit)

这不是一篇详尽的教程,而是想和大家交流一些想法,DIY 玩起来(当然干货也是有的,提供了一个 Homebridge 插件,可以在部署了 Node.js 及 Homebridge 的环境下安装)

先看一个例子

  • 对 Siri 下达「睡眠电脑」,屏幕就会进入睡眠状态

或者复杂一点的

  1. 对 Siri 下达「更新树莓派」,树莓派会执行sudo apt-get update,并且通过 Telegram 推送执行结果,如果有待更新软件包,还会新建一个 Todoist 任务,评论内容为软件包信息
  2. 对 Siri 下达「升级树莓派」,会执行sudo apt-get dist-upgrade,推送成功,并完成 Todoist 任务

Siri 命令与消息通知
Siri 命令与消息通知

Todoist 任务
Todoist 任务

通过 HomeKit 执行 Shell 脚本

熟悉 Homebridge 的朋友当然了解它的强大与灵活,我们可以利用提供的接口编写插件,将原本并不智能的设备纳入到 HomeKit 的管理中,或者做任何想做的事,这个实现也不例外

直观的方案

我们要做的,是扩展 HomeKit 的功能,使之能执行自定义的命令

其实社区不乏这样的实现,我们可以很容易地虚拟一个开关之类的设备,然后打开它的时候去执行预设的 Shell 脚本(能执行 Shell 就约等于能做任何事)

这个方法很直观,但是我们当然不希望每个 Shell 脚本都需要添加一个虚拟开关,并且在使用 Siri 的时候用「打开 / 关闭 xxx」这样的句式来下发命令

转换思路

其实想一下,我们并不是必须靠「开关」来执行命令,我们需要的是改变虚拟设备的状态,就触发相应的脚本,所以接下来就顺理成章了,HomeKit 里智能灯拥有最多的可控状态,1 个智能灯,通过改变亮度(0 - 100)就可以对应约 100 个命令

HomeKit 中智能灯采用 HSV 色彩空间,也就是说除了「亮度 V」,还有「色相 H」、「饱和度 S」可以利用,但是根据 Homebridge 接口的特点,实现各属性的乘数关系比较复杂,简单与直观起见,只用亮度通常就足够了

这样,我们就可以设定亮度为 1 时,执行某个脚本;设定亮度为 2 时,执行另一个脚本等等,极大地减少了添加虚拟设备的数量

不同的亮度对应不同的脚本
不同的亮度对应不同的脚本

HomeKit 中「场景」的用途

滑动操作的缺点

前面我们设计通过改变智能灯的亮度来执行 Shell 脚本,但是你一定不想在 UI 上滑动亮度条,鬼知道「沿途」会触发多少目标之外的脚本,而且每个亮度对应的脚本具体是什么也不容易记忆,这时候「场景」就派上用场了

场景与命令映射

我们可以设定多个场景,每个场景中智能灯的亮度不同,进而对应不同的脚本,这样去点按场景就很方便了,而且与「给每个命令都映射一个开关」的方法相比,虚拟一个设备而设定多个场景的方式显得更「优雅」一些

场景与命令映射
场景与命令映射

用语音去设定场景,就好像在「执行语音命令」

自然地,我们会给这些场景起一些有含义的名字,比如上面的例子。至此,我们「执行语音命令」的目标其实就已经达成了

最初想到可以这样做是缘于一次搞怪:

当 HomeKit 接入了一些设备,比如智能灯(真正的),我们自然会想要捉弄下 Siri,于是我说:要有光。不出意外,Siri 没那么「聪明」。不过如果这样设置,我们对《最后的问题》的致敬就可以完成:

要有光
要有光

显然,Siri 对场景名称是敏感的。正如系统建议的「出门」、「到家」、「晚安」、「早上好」那几个场景一样,我们只要用想要执行的命令名称(或任何话,只要 Siri 能正确「听写」)来创建场景,比如「睡眠电脑」、「关闭电脑」等等,然后去编写对应的 Shell 脚本就好

刚设置好的场景,可能要等一会儿 Siri 才能正确索引和识别

几个体验上的问题 & 细节

原理很简单,交流几个应该考虑的问题:

  • 屏蔽误触。毕竟是添加了一个设备,有时会不小心开关它,我们可以编写一些策略来屏蔽它,比如响应「开 / 关」操作时,异步还原它原本的开关状态;只有「修改亮度」操作才真正进行处理,同时将开关状态置为「开」
  • 忽略模糊指令。同样,毕竟是添加了一个「灯」,如果对 Siri 下达类似这样的指令:「将灯亮度调到 x」,这会改变所有智能灯的亮度,而如果 x 恰好对应了一个 Shell 的话……这一定不是我们期望的。这个问题解决起来麻烦一些,一个可行的方案是再添加一个虚拟灯,如果我们发现这两个灯「同时」收到命令的话(实际上是两条并发的命令,但中间会有一点时间差),就可以认为该命令是模糊指令,然后忽略它

插件使用说明

需要先行配置好 Node.js 及 Homebridge,请参考相关教程

下面是我写的一个 Homebridge 插件,仅供参考。安装命令:

npm install -g homebridge-command-bulb

插件配置

默认不需要配置。如果想要修改 Shell 脚本存放路径,或者需要 Telegram 推送功能,Homebridge 配置如下(如果有其它插件配置,注意合并):

{
    "platforms": [
        {
            "platform": "CommandPlatform",
            "directory": "~/.homebridge/commands",
            "tg_token": "",
            "tg_chat_id": "",
            "proxy": "http://localhost:8888"
        }
    ]
}
  • 需要自己创建 Shell 脚本目录,默认为~/.homebridge/commands
  • Telegram token 需要通过 @BotFather 申请;chat id 可以通过 @get_id_bot 获取
  • 插件连接 Telegram 通常需要 proxy,请科学解决。如果是 socks proxy 的话可以用 privoxy 转换为 http proxy

重启 Homebridge 后会添加两个灯,「Command Bulb」和「Probe Bulb」,前者用于执行命令,后者用于排除模糊指令干扰(没有其它用处,请无视它的存在)

脚本规则

脚本应具有可执行权限,约定先于配置,脚本前缀、后缀采用如下规则

  • 前缀(前两位,01 - 99)用于映射亮度,比如「01」对应亮度为 1,此外,所有前缀为「01」的脚本都会被执行,可以一次执行多个独立脚本。预留了亮度为 0 和 100 两个值用于标识「成功 / 失败」,所以不要用「00」作为前缀
  • 后缀用于辅助功能,目前是用于 Telegram 推送消息(在配置了相关参数的情况下)
    • 「.ok」表示执行结果为成功时推送「Command: xxx OK!」消息。注意,默认情况下,执行过程中存在 stderr 不会被认为是失败
    • 「.out」表示推送 stdout
    • 「.err」表示推送 stderr,并且执行过程中存在 stderr 会被认为是失败
    • 如果失败,总是会推送「Command: xxx Failed!」消息,无需后缀
    • 多个后缀可组合,如「.ok.out」,会推送成功和标准输出两条消息
    • 其它未定义的及「.sh」可有可无,会忽略

一些使用建议

  • 不要将两个虚拟灯加入个人收藏,最好新建一个房间来摆放它们,尽量减少直接操作它们的机会

独立房间
独立房间

  • 新建两个场景「成功」、「失败」(「成功」对应于「Command Bulb」关闭状态,「失败」对应于其亮度为 100 的状态),用于标识执行结果,可以添加到个人收藏充当信号灯;而其他命令的场景最好不要添加到个人收藏(太多会显得乱,而且我们倾向于用 Siri 来触发而不是点按)

成功 / 失败场景作为信号灯
成功 / 失败场景作为信号灯

  • 有一个家庭中枢会方便很多,同时也使得外出时 HomeKit 依然可用。如果用闲置的 iPad 作为中枢,将「自动锁定」关闭可以保持 HomeKit 总是可用,亮度可以调到最低

控制 Mac 的方法

通过 SSH 控制 Mac 执行命令

原理是在运行 Homebridge 及插件的服务器上,通过 SSH 连接 Mac 执行命令,类似这样:

ssh user@ip 'shell 命令'

所以需要在 Mac 打开系统偏好设置 -> 共享 -> 远程登录

其中 user 为 Mac 的用户名(在终端中运行whoami即是),ip 为 Mac 的地址,可以在路由器中绑定,或者用共享设置页面显示的类似 xxx.local 的地址,更加灵活

设置 Mac SSH 免密登录

在服务器上 SSH 到 Mac 还需要输入密码,所以要设置免密登录(只对该服务器有效)

在服务器上运行ssh-keygen -t rsa,一路回车,会在服务器的 ~/.ssh 目录下生成两个文件,把其中 id_rsa.pub 中的内容粘贴到 Mac 的 ~/.ssh/authorized_keys 文件中即可

示例

  • 睡眠电脑(屏幕)
    ssh user@ip 'pmset displaysleepnow'
    
  • 关闭电脑
    ssh user@ip 'osascript <<EOF
    tell application "Finder" to shut down
    EOF'
    

    it works 还是 it just works

整个方案是可行的,对原本 HomeKit 的「侵入性」相对比较小,就我个人体验来说还不错,不过当然也有无法克服的缺点:

  • 需要自己编写 Shell 脚本
  • 终究是显式地引入了虚拟设备,也为此不得不考虑容错机制。不过增加的设备也可以成为功能的扩展点
  • 语音与命令终究是静态映射,不能动态解析和响应,所以需要手动设定较多场景
  • 受制于 HomeKit 接口的开放 / 破解程度,未来可能会失效
  • Siri 对场景的名称并不总能正确解读。比如设置了一个「关闭电脑」命令,因为包含了 Siri 指令系统的关键字「关闭」,它就不干(不过如果说「请关闭电脑」,却能正确识别,迷之傲娇)

要有礼貌
要有礼貌

采用这个方案,我们实际上并没有让 Siri 更「聪明」,而是用一种比较 tricky 的方式,让 Siri 更「听话」,有一说一,说一不二,说二就听不懂了

这自然不是最佳的体验,不管是语音助手,还是智能家居,都应该有更加直观的界面、更加动态的实现。期待本次 WWDC 能给我们带来更多「it just works」的特性,也期待被收购的 Workflow 能早日加入 Siri 的支持