惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
罗磊的独立博客
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
Apple Machine Learning Research
Apple Machine Learning Research
The Register - Security
The Register - Security
J
Java Code Geeks
V2EX - 技术
V2EX - 技术
Vercel News
Vercel News
N
News and Events Feed by Topic
腾讯CDC
P
Proofpoint News Feed
N
News | PayPal Newsroom
www.infosecurity-magazine.com
www.infosecurity-magazine.com
爱范儿
爱范儿
O
OpenAI News
酷 壳 – CoolShell
酷 壳 – CoolShell
月光博客
月光博客
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
D
Docker
Y
Y Combinator Blog
博客园 - 聂微东
G
Google Developers Blog
S
Security @ Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
S
Schneier on Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
阮一峰的网络日志
阮一峰的网络日志
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
CERT Recently Published Vulnerability Notes
I
Intezer
G
GRAHAM CLULEY
有赞技术团队
有赞技术团队
Attack and Defense Labs
Attack and Defense Labs
V
Visual Studio Blog
博客园 - Franky
博客园 - 三生石上(FineUI控件)
W
WeLiveSecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
Scott Helme
Scott Helme
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
LINUX DO - 最新话题
C
Cybersecurity and Infrastructure Security Agency CISA

夜行人

回家路上 第一期的直播演示项目 震动检测器 正能量 在线参观CodeLab Neverland 发布 CodeLab Adapter 3.3.1 DynamicTable 之 纸糊方向盘 CodeLab DynamicTable: 一个可实施的技术方案 CodeLab Insight 发布 Alpha 版 情人节 Home Assistant 周报 && IoT 周报 (02) Joplin: 关注隐私的 Evernote 开源替代软件 浏览器的未来与 Web 传感器 Home Assistant 周报 && IoT 周报 (01) 百宝箱(01) 论自由 介绍 WebThings Home Assistant 周报 && iot 周报 (00) 百宝箱(00) 毛姆读书心得 传世之作 周末徒步 CodeLab Adapter ❤️ Jupyter/Python 航班 躲雨 夏令营途中 [译]思想--作为一种技术 The future of coding 美国之行 三门问题的程序模拟 从Python转向Pharo https://blog.just4fun.site/post/iot/iot-open-source-projects/ Python异步编程笔记 https://blog.just4fun.site/post/iot/iot-open-source-hardware-community/ 万物积木化开发者社区 CodeLab ❤️ Blender Scratch3技术分析之云变量 API(第7篇) [译]对管道(Pipes)的偏爱 [译]提出正确的问题比得到正确答案更重要 蓝牙设备与Scratch3.0 创建你的第一个Scratch3.0 Extension Scratch3技术分析之项目内部数据(第6篇) Scratch3技术分析之社区 API(第5篇) Scratch3技术分析之User API(第4篇) Scratch3技术分析之项目主页API(第3篇) Scratch3技术分析之静态资源API(第2篇) Scratch3.0、micro:bit与Windows7 https://blog.just4fun.site/post/iot/zerynth-vs-micropython/ 核聚变、方所与半宅空间 可视化编程为何是个糟糕的主意 codelab.club周末聚会 关于codelab.club '下一件大事'是一个房间 Hungry Robot - Eat everything 编程作为一种思考方式 今日简史 史蒂夫·乔布斯传 罗素自选文集 https://blog.just4fun.site/post/edx/tianjin-scratch-ai/ https://blog.just4fun.site/post/edx/richie-cms-openedx/ 徒步武功山 WebUSB与micro:bit 积木化编程与3D场景 夜宿武功山顶 scratch3-adapter接入优必选Alpha系列机器人 https://blog.just4fun.site/post/edx/video-migration-note/ scratch3-adapter重构笔记 https://blog.just4fun.site/post/edx/edx-community-members/ 两种硬件编程风格的比较 使用micro:bit自制PPT翻页笔 柏拉图对话集 scratch3.0 + micro:bit 七月电影放映计划 非营利组织的管理 Screenly--用树莓派让任何屏幕变为可编程的数字标牌 以最佳实践开始你的Django项目 micro:bit与事件驱动 为Scratch3.0设计的插件系统(上篇) OCR应用一例 近两年读过的一些好书 blockly开发之使用python驱动浏览器中的turtle(2) 牛顿新传 文学理论入门 逻辑的引擎 人生的意义 blockly开发之生成并运行js代码(1) blockly开发之hello world(0) micro:bit使用笔记 神器之Termux https://blog.just4fun.site/post/iot/micropython-notes/ Cozmo what is this Scratch的前世今生 下段旅程 我行在远方 爆裂 途中杂记 https://blog.just4fun.site/post/edx/open-edx-startup/ cozmo系列之入门 - 有性格且可编程的机器人 PaperWeekly开发笔记 创业二三事
Home Assistant 与 Yeelight彩光灯泡的通信过程分析
种瓜 · 2019-06-12 · via 夜行人

Awaken your home.

前言

Home Assistant是一个隐私优先的智慧家庭系统,支持本地化部署,甚至允许在没有断网的情况下使用。

我是Home Assistant的铁杆粉,用它构建了一些好玩的东西,正打算基于它创建一些更有想象力的项目。

近期基本读完了Home Assistant所有的文档,喜欢它的架构设计。在架构上,codelab-adapter和它有许多相似之处。

Home Assistant的核心源码此前也大体上读了一遍,代码写得非常漂亮,堪称Python Asyncio的典范案例。

Home Assistant近期发布了0.94版本, 为1.0版本做好了准备,预计今年发布1.0版本。

考虑到项目已经基本上稳定下来,API不会再有大的变动。于是我近期准备重新审视一遍Home Assistant,在源码和架构层面对其做一些解读和梳理。

本文焦点

将硬件接入Home Assistant有许多种方式。正如大多数物联网项目一样,接入一款硬件是很容易的。MQTT这类的基础协议提供了充分的自由度。

但我们要知道此类系统往往会随着业务而发展,不断生长,如果没有精心设计的架构,很容易被后期的复杂度压垮。在操作系统的发展史上,太多这样的先烈了。

Home Assistant已经展示出其架构的高度灵活性(至今已经接入了1400+设备),也许有望成为物联网领域的linux。

往系统中硬编码接入一款硬件总是容易的,但随着系统的生长,这种生态十分容易溃败。所以本文关注Home Assistant社区是如何考虑设备接入这件事的,如何应对组件增长带来的复杂度升高。

我们将分析Home Assistant接入Yeelight彩光灯泡的细节,以此为例,对系统做深入了解。

本文将通过实验来跟踪通信的细节。

Yeelight彩光灯泡搭载了wifi芯片。为了将其接入WiFi,你需要使用官方APP来为它配网。

由于我们准备用Home Assistant控制它,所以需要在APP中做些设置,使其接受来自局域网的控制指令。

实验

实验环境

  • MacOS 10.13.5
  • Python 3.7.2
  • homeassistant==0.94.2

运行 homeassistant

我在虚拟环境里安装homeassistant,为了不与系统中的homeassistant冲突,我选择在当前的虚拟python环境下运行homeassistant: python -m homeassistant

配置目录

第一次运行,会创建配置目录

Config directory: /Users/wuwenjie/.homeassistant

目录内容为:

1
2
3
➜  ~ ls /Users/wuwenjie/.homeassistant
automations.yaml     customize.yaml       groups.yaml          home-assistant_v2.db secrets.yaml
configuration.yaml   deps                 home-assistant.log   scripts.yaml         tts

从命名可以看出configuration.yaml是配置文件。

值得一提的是,如果你采用hass.io安装homeassistant,那么你永远不需要在命令行里进入配置目录,可以直接在网页上编辑,这对非技术用户很友好。

由于防火墙的存在,在国内安装hass.io非常痛苦,由于防火墙的存在,技术/科研人员的日常大多时候都是痛苦的。

fuck the GFW.

其他依赖

运行python -m homeassistant的时候,还将安装其他依赖,一些值得留意的依赖包括netdisco==2.6.0.

将灯泡信息添加到configuration.yaml

重启时,发现安装了yeelight==0.5.0

在Yeelight APP找到Yeelight彩光灯泡的IP地址,将其配置到configuration.yaml

1
2
3
4
yeelight:
  devices:
    192.168.31.30:
      name: test light

重启home-assistant服务,即可看到灯泡已经出现在用户面板里。控制开关和调节色温一切正常。

开始分析

在接下来的分析里,我们关注

  • 彩灯设备的发现和加载
  • 控制指令如何生效(通信细节)

意外发现

Yeelight是home-assistant支持的components之一,home-assistant目前支持1400+设备。

技术层面,components的代码见components。在探索如何接入新的components时,意外发现Integration Services已经完美解决了我们前头提到的home-assistant如何接入新设备的问题。

但我们仍然准备继续分析,深入到代码层面的细节。

分析工具

我使用sourcegraphchrome插件来阅读和跟踪源码,这个工具让我们在代码森林中穿梭变得自如。

程序入口

setup.py中可以看到程序的入口点: hass = homeassistant.__main__:main

其中ensure_config_file值得关注,这个函数的指责是确保配置文件的正常。如果不存在配置文件则帮助我们创建所需的配置文件(默认),后文提及的configuration.yaml就是由它创建的,如果你对初始化的配置文件感兴趣,从它入手可能是个好主意。更多细节:

此外,try_to_restart的机制怪有趣的,home-assistant是生产级的代码,很多细节考虑得非常周到,很有借鉴价值。

彩灯设备的发现和加载

我们进入到前头提到的分析目标之一:彩灯设备的发现和加载

components/yeelight/init.py中发现了Integration Services文档描述的内容:def setup(hass, config), 由此可知官方默认支持的components,与用户临时接入的组件,都遵循同一套接入机制。系统的同构性有利于健壮性和灵活性。

还记得我们在configuration.yaml做的设置吗?

1
2
3
4
yeelight:
  devices:
    192.168.31.30:
      name: test light

这个设置是如何被源码使用的呢?在def setup(hass, config)中看得很清楚:

 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
def setup(hass, config):
    """Set up the Yeelight bulbs."""
    conf = config.get(DOMAIN, {})
    yeelight_data = hass.data[DATA_YEELIGHT] = {}

    def device_discovered(service, info):
        _LOGGER.debug("Adding autodetected %s", info['hostname'])

        device_type = info['device_type']

        name = "yeelight_%s_%s" % (device_type,
                                   info['properties']['mac'])
        ipaddr = info[CONF_HOST]
        device_config = DEVICE_SCHEMA({
            CONF_NAME: name,
            CONF_MODEL: device_type
        })

        _setup_device(hass, config, ipaddr, device_config)

    discovery.listen(hass, SERVICE_YEELIGHT, device_discovered)

    def update(event):
        for device in list(yeelight_data.values()):
            device.update()

    track_time_interval(
        hass, update, conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
    )

    if DOMAIN in config:
        for ipaddr, device_config in conf[CONF_DEVICES].items():
            _LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
            _setup_device(hass, config, ipaddr, device_config)

    return True

彩灯设备的发现和加载机制,这段代码已经很清晰展示了。

def setup(hass, config)中的细节(如track_time_interval)值得深入学习,留待之后有空进一步追踪。

控制指令如何生效(通信细节)

为了理解控制指令如何生效,我们可以跟踪用户操作过程中发生的交互。

从控制台可以看出开关等过程中并不发生http(ajax)交互,于是可以断定是使用websocket交互。

通过观察过程中发生的交互,我们可以使用其他客户端来模拟。下边使用Pharo来替代网页,与Yeelight进行交互。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
webSocket := ZnWebSocket to: <URI>.

[ webSocket
   sendMessage: '{"type":"auth","access_token":"<access_token>"}'.] value.

[ (webSocket
   readMessage) logCr ]  schedule.

[ webSocket
   sendMessage: '{"type":"call_service","domain":"light","service":"turn_on","service_data":{"entity_id":"light.test_light"},"id":27}'.] value.

让我们进一步跟踪websocket消息是如何生效的。

追踪代码可以发现websocket_api竟是作为components在homeassistant中运行!homeassistant的同构性太惊人了,我非常喜欢这种高度一致的设计。

我们前头发送了开灯命令:

1
2
3
4
5
6
7
{
  "type": "call_service",
  "domain": "light",
  "service": "turn_on",
  "service_data": { "entity_id": "light.test_light" },
  "id": 27
}

handle这份命令的代码为:

 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
@decorators.async_response
@decorators.websocket_command({
    vol.Required('type'): 'call_service',
    vol.Required('domain'): str,
    vol.Required('service'): str,
    vol.Optional('service_data'): dict
})
async def handle_call_service(hass, connection, msg):
    """Handle call service command.
    Async friendly.
    """
    blocking = True
    if (msg['domain'] == HASS_DOMAIN and
            msg['service'] in ['restart', 'stop']):
        blocking = False

    try:
        await hass.services.async_call(
            msg['domain'], msg['service'], msg.get('service_data'), blocking,
            connection.context(msg))
        connection.send_message(messages.result_message(msg['id']))
    except ServiceNotFound as err:
        if err.domain == msg['domain'] and err.service == msg['service']:
            connection.send_message(messages.error_message(
                msg['id'], const.ERR_NOT_FOUND, 'Service not found.'))
        else:
            connection.send_message(messages.error_message(
                msg['id'], const.ERR_HOME_ASSISTANT_ERROR, str(err)))
    except HomeAssistantError as err:
        connection.logger.exception(err)
        connection.send_message(messages.error_message(
            msg['id'], const.ERR_HOME_ASSISTANT_ERROR, str(err)))
    except Exception as err:  # pylint: disable=broad-except
        connection.logger.exception(err)
        connection.send_message(messages.error_message(
            msg['id'], const.ERR_UNKNOWN_ERROR, str(err)))

至此我们弄清楚了本文感兴趣的所有问题。

todo

  • 对多个组件的并行机制做了解

参考