前言

Home Assistant 折腾笔记一文中,我们曾提及Home Assistant Cloud

Home Assistant Cloud是什么

Home Assistant Cloud提供安全的远程连接,允许你远程控制设备(在你外出的时候),也允许Amazon Alexa and Google Assistant控制Home Assistant实例.

目前该服务由Home Assistant的合作伙伴Nabu Casa, Inc提供。Nabu Casa, Inc由Home Assistant 和 Hass.io的联合创始人创建。


Home Assistant Cloud目前由Nabu Casa, Inc提供, nabucasa client是开源的:nabucasa(我当前本地安装的版本是0.14),对于hass_nabucasa的交互逻辑,可以从测试代码中学习:test_cloud_api.py

但在国内使用,延迟比较严重,哪天被墙了也难说。本文试图对Home Assistant Cloud 进行分析,为今后自行构建Home Assistant Cloud server提供参考,这个工作颇似我们此前对Scratch做的分析

思路

为了理解Home Assistant Cloud的工作原理,我们将从前端开始,观察通信过程,之后跟踪到Home Assistant后端源码里(nabucasa组件是核心部分),最后对Home Assistant Cloud server进行推断。

前端分析(通信过程)

首先打开Home Assistant Cloud: http://127.0.0.1:8123/config/cloud/account

我们从登陆开始:

登陆

POST: http://127.0.0.1:8123/api/cloud/login

Request Payload: {"email":"EMAIL","password":"PASSWORD"}

Response: {"success": true}

登陆完成之后, websocket发送message:{"type":"cloud/status","id":18}

收到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
  "success": true,
  "id": 18,
  "result": {
    "logged_in": true,
    "prefs": {
      "alexa_enabled": false,
      "google_secure_devices_pin": null,
      "google_enabled": false,
      "remote_enabled": false,
      "cloudhooks": {
        "xxx": {
          "cloudhook_id": "xxx",
          "managed": true,
          "cloudhook_url": "https://hooks.nabu.casa/xxxxxx",
          "webhook_id": "xxx"
        }
      },
      "cloud_user": "xx",
      "google_entity_configs": {}
    },
    "email": "EMAIL",
    "remote_certificate": null,
    "alexa_entities": {
      "include_entities": [],
      "exclude_domains": [],
      "include_domains": [],
      "exclude_entities": []
    },
    "cloud": "connecting",
    "remote_connected": false,
    "alexa_domains": [
      "fan",
      "automation",
      "group",
      "alert",
      "cover",
      "lock",
      "script",
      "sensor",
      "binary_sensor",
      "climate",
      "scene",
      "input_boolean",
      "switch",
      "media_player",
      "light"
    ],
    "remote_domain": null,
    "google_entities": {
      "include_entities": [],
      "exclude_domains": [],
      "include_domains": [],
      "exclude_entities": []
    }
  },
  "type": "result"
}

之后前端继续发送两条websocket消息:

{"type":"webhook/list","id":19}, 返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "success": true,
  "id": 19,
  "result": [
    {
      "domain": "locative",
      "name": "Locative",
      "webhook_id": "xxxx"
    }
  ],
  "type": "result"
}

{"type":"cloud/subscription","id":20}, 返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "success": true,
  "id": 20,
  "result": {
    "customer_exists": true,
    "renewal_active": false,
    "status": 200,
    "subscription": {
      "canceled_at": 1560481412,
      "cancel_at_period_end": true,
      "trial_end": 1563148800,
      "status": "trialing",
      "current_period_end": 1563148800
    },
    "human_description": "Trial user. Trial expires Jul 15, 2019.",
    "delete_requested": false,
    "provider": null,
    "source": null,
    "plan_renewal_date": 1563148800,
    "billing_plan_type": "trial"
  },
  "type": "result"
}

开启Remote Control

点击Remote Control 右边的按钮,将打开远程控制模式。点击之后前端发送websocket消息:

{"type":"cloud/remote/connect","id":20}, 返回内容同前头{"type":"cloud/status","id":18}相似

{"type":"cloud/status","id":21}, 返回内容同前头{"type":"cloud/status","id":18}相似

关闭Remote Control

{"type":"cloud/remote/disconnect","id":22}, 返回内容同前头{"type":"cloud/status","id":18}相似

打开显示在页面的链接:https://xxx.ui.nabu.casa, 可以进行远程控制了!控制界面完全相同,目前猜测是建立了一个透明管道,就像ngrok那样

开启alax

{"type":"cloud/update_prefs","alexa_enabled":true,"id":24}, 返回{"success": true, "id": 24, "result": null, "type": "result"}

后端分析

根据前端的websocket message,逆向找到对应的后端源码是比较简单的一件事。

我们重点关注一下handle{"type":"cloud/remote/connect","id":20}消息的后端源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
    'type': 'cloud/remote/connect'
})
async def websocket_remote_connect(hass, connection, msg):
    """Handle request for connect remote."""
    cloud = hass.data[DOMAIN] # 
    await cloud.client.prefs.async_update(remote_enabled=True)
    await cloud.remote.connect()
    connection.send_result(msg['id'], _account_data(cloud))

nabucasa client部分对应的源码

阅读RemoteUI class可以发现cloud非常值得关注,cloud作为RemoteUI初始化参数传入,我们跟踪RemoteUI的初始化过程,可以追溯到:self.remote = RemoteUI(self), nabucasa client与云相关的所有秘密都可以从这儿找到。

云端分析

从前头的讨论我们感觉Home Assistant Cloud有些像ngrok server。使用ngrok好像也完全做得到这些事。 本质是把局域网服务暴露到外网。

远程页面如何通信

Remote Control现实的公网控制页面https://xxx.ui.nabu.casa/ 是如何控制我们的设备的呢,可以发现,它使用的websocket通道为 wss://xxx.ui.nabu.casa/api/websocket,采用的message和本地完全一样。

看起来是个透明代理。

从证书信息和后端源码可知证书使用 letsencrypt.org(有效期为3个月,会自动更新)

隐藏目录

登陆之后,观察.homeassistant/.cloud目录, 有以下文件:

  • acme_account.pem
  • acme_reg.json
  • production_auth.json
  • remote_fullchain.pem
  • remote_private.pem

.storage/cloud内容为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "data": {
        "alexa_enabled": false,
        "cloud_user": "xxx",
        "cloudhooks": {
            "xxx": {
                "cloudhook_id": "xxx",
                "cloudhook_url": "https://hooks.nabu.casa/xxx",
                "managed": true,
                "webhook_id": "xxx"
            }
        },
        "google_enabled": false,
        "google_secure_devices_pin": null,
        "remote_enabled": true
    },
    "key": "cloud",
    "version": 1
}

参考