教程:让你的不智能蓝牙灯接入 HomeKit 智能家居

引言 如果你有一个蓝牙灯,并且它能够被以下几个手机应用控制: LampSmart Pro Lamp Smart Pro - Soft Lighting / Smart Lighting FanLamp Pro Zhi Jia Zhi Guang ApplianceSmart Vmax smart Zhi Mei Deng Kong 那么恭喜你,你可以跟随本篇教程,一步一步将它接入 HomeKit 智能家居。 来实现通过 iPhone 家庭应用控制以及 Siri 语音控制。 对于实现后的效果,可以看我另一篇博文:折腾:HomeKit 接入蓝牙吸顶灯 非常感谢 https://github.com/NicoIIT/ha-ble-adv/,本篇教程实际是从零接入该项目。 开始之前 在开始之前,需要一点点基础的计算机知识,确保满足以下条件: 熟悉 Home Assistant 概念和一些基本操作,可以阅读官方文档快速熟悉; 能够使用 Terminal 命令行工具; 已安装 Docker 并且可用; 有一个 GitHub 帐号; 关键:一块 ESP32 开发板; 主要用于蓝牙通信,如果没有的话,可以网购一块,25 元左右的价格; (未来会研究如何直接使用系统自带的蓝牙); 教程方式 本教程以 MacOS 为例。如果你使用 Linux 或 Windows,部分操作可能需要稍作调整。 教程的每个步骤会说明:要做什么、怎么做、以及验证结果。请确保完成验证后再进行下一步。遇到问题时,可以参考自查攻略(部分步骤有)或联系我。 如何实现 对于实现不感兴趣的,可以跳转到下一部分 组件列表 Home Assistant (HA) Home Assistant Community Store (HACS) ha-ble-adv 缩写是 Home Assistant Bluetooth Low Energy Advertise 翻译为 Home Assistant 蓝牙低功耗广播 ESP Home esphome-ble-adv-proxy ESP32 组件交互 核心是使用 ha-ble-adv 通过蓝牙与设备交互,控制开关和亮度 通过 Home Assistant 提供的 HomeKit Bridge 连接到 iPhone 家庭应用,来实现控制 要让 Home Assistant 成功使用上 ha-ble-adv 需要具备以下条件: 通过 HACS 安装组件 ha-ble-adv 通过 ESP Home 在 ESP32 上面安装 esphome-ble-adv-proxy 以供 ha-ble-adv 发送和接收蓝牙信息 以下是整体架构图 ...

四月 27, 2025 · 6 分钟 · Lex Cao

折腾:蓝牙吸顶灯接入 HomeKit

一、缘起:一个"基础版"吸顶灯的智能化挑战 最近给家里客厅换了个新吸顶灯。这灯不仅能用遥控器,还能用手机 App 控制,看起来挺智能的。然而,美中不足的是,它竟然不支持接入米家或 HomeKit。 搜索后才发现,原来同品牌更贵版本是支持米家控制的,而我买的这个基础版不支持,大概价差 100 元左右。 作为一个喜欢折腾的程序员,我开始从第一性原理思考:既然它能通过手机 App 控制,通过打开 App 时申请蓝牙权限可以推断是通过蓝牙进行通信。既然是蓝牙控制,那理论上就能通过别的蓝牙程序来控制它,进而接入智能家居生态。对,没错,肯定行! 【注意】想要着手操作一番的朋友,可以参考我的另一篇博文:教程:让你的不智能蓝牙灯接入 HomeKit 智能家居 问题现状演示 想要直接看到接入后的效果,可以 点击这里跳转。 二、探索:Home Assistant 与蓝牙控制方案 于是乎,我兴冲冲地开始了我的折腾之旅,进行了大量的搜索尝试。既然直接接入米家此路不通,那曲线救国呢?(注:这里指通过其他方式间接实现目标)我灵机一动,开始搜索如何将米家设备接入 HomeKit。 这次搜索让我打开了一个新世界的大门 —— Home Assistant (HA)! Home Assistant Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. 以本地控制和隐私为先的开源家庭自动化。由全球工匠和 DIY 爱好者社区提供支持。 顺藤摸瓜,我又发现 Home Assistant 竟然有个叫 HomeKit Bridge 的组件,能把各种非 HomeKit 设备桥接到 HomeKit。看到这里,我仿佛看到了曙光! 继续在 Home Assistant 的社区里搜索,感谢万能的网友,挖到了一个宝藏项目:ha-ble-adv!看名字就知道,这玩意儿跟蓝牙有关,简直是为我量身定做的! ...

四月 26, 2025 · 3 分钟 · Lex Cao

介绍 genapi:一个 Golang HTTP Client 生成代码库

本文将为大家介绍 genapi,一个用于自动生成 Golang HTTP Client 的代码库。如果你对这个项目感兴趣,可以访问 genapi 官网 或 GitHub 仓库 获取更多技术细节。 从手工到自动:Golang HTTP Client 的演进之路 在 Golang 开发中,调用 HTTP API 是一个非常常见的需求。本文将通过一个天气 API 的示例,介绍 HTTP Client 代码是如何从手工编写演进到自动生成的。让我们看看这个简单的天气 API: GET /api/weather?city=shanghai Response: { "temperature": 25, "humidity": 60, "condition": "sunny" } 原始手工编写 最初,我们可能会直接编写如下代码: func getWeather(city string) (*Weather, error) { resp, err := http.Get("https://api.weather.com/api/weather?city=" + city) if err != nil { return nil, err } defer resp.Body.Close() var weather Weather if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { return nil, err } return &weather, nil } 这种方式简单直接,但存在以下问题: URL 硬编码在代码中 参数拼接容易产生错误 错误处理逻辑重复 响应解析代码重复 模板化请求 为了解决上述问题,我们开始对代码进行抽象和模板化改造: type Client struct { baseURL string client *http.Client } func (c *Client) doRequest(method, path string, query url.Values, result interface{}) error { u, _ := url.Parse(c.baseURL + path) u.RawQuery = query.Encode() req, err := http.NewRequest(method, u.String(), nil) if err != nil { return err } resp, err := c.client.Do(req) if err != nil { return err } defer resp.Body.Close() return json.NewDecoder(resp.Body).Decode(result) } func (c *Client) GetWeather(city string) (*Weather, error) { query := url.Values{} query.Set("city", city) var weather Weather err := c.doRequest("GET", "/api/weather", query, &weather) return &weather, err } 这样的改进带来了以下好处: ...

三月 3, 2025 · 2 分钟 · Lex Cao

「翻译」复式记账法 (The Double-Entry Counting Method)

翻译自 The Double-Entry Counting Method 介绍 本文是一份关于复式记账的简要介绍,从计算机科学家的角度撰写。它试图以尽可能简单的方法解释基础记账,简化会计中通常涉及到的某些特殊性。它也代表了 Beancount 的工作方式,并且对所有使用纯文本记账的用户都应该适用。 请注意,我不是会计师,在编写此文档过程中,我可能使用与传统会计培训教授略有不同或不常见的术语。我给自己授权创造一些新的、甚至是不寻常的东西,以便将这些想法尽可能简单明了地解释给那些对它们不熟悉的人。 我认为每个高中生都应该在高中阶段学习复式记账法,因为这是一项极其有用的组织技能,并且我希望这篇文章可以帮助将其知识传播到专业圈以外的领域。 复式记账的基础 复式记账法只是一种简单的计数方法,只有一些简单的规则。 让我们从定义账户的概念开始。账户是一种可以容纳物品的东西,就像一个袋子。它用于计算和累积物品。让我们画一条水平箭头来直观地表示随着时间推移账户中不断变化的内容: 左侧,是描述过去,而右侧则是不断增长的时间:现在、未来等。 现在,让我们假设账户只能包含一种东西,例如美元。所有的账户都以零美元的空内容开始。我们将称账户中单位数量为账户的 余额 (Balance)。请注意,它代表了特定时间点上其内容的情况。我会使用一个数字在帐户时间轴上方绘制余额: 账户的内容会随着时间而变化。为了改变账户的内容,我们必须向其添加一些东西。我们将这个添加称为对账户的记账,我会在该账户的时间轴上画一个带圈数字来表示这种变化,例如:向该账户中添加 100 美元: 现在,我们可以在记账后绘制更新后的账户余额,并在其后面加上另一个小数字: 账户加上 100 美元后,余额现在为 100 美元。 我们也可以从账户中减去一定金额。例如,我们可以减去 25 美元,这样账户余额就变成了 75 美元: 如果我们减去的金额超过账户余额,账户余额也可能变为负数。例如,如果我们从该账户中取出 200 美元,则余额现在变为 -125 美元: 账户中包含负数是完全正常的。请记住,我们所做的只是计数。很快我们会看到,有些账户在它们的时间轴上将保持负余额。 报表 (Statement) 值得注意的是,我在前一节中写下的时间线记账与机构为每个客户维护并通常通过邮件发送的纸质账户报表类似: 时间 描述 金额 余额 2016-10-02 … 100.00 1100.00 2016-10-05 .. -25.00 1075.00 2016-10-06 .. -200.00 875.00 最终结余 875.00 有时候金额栏会被分成两个,一个显示正数,另一个显示负数: ...

七月 4, 2023 · 4 分钟 · Lex Cao

Spring Data JPA 多条件连表查询最佳实践

背景 本文是 Spring Data JPA 多条件连表查询 文章的最佳实践总结。 解决什么问题? 使用 Spring Data JPA 需要针对多条件进行连表查询的场景不使用原生 SQL 或者 HQL 的时候,仅仅通过 JpaSpecificationExecutor 构造 Specification 动态条件语句来实现类型安全的多条件查询。 说明 相关上下文背景请前往 前文 了解。 这里再提一下接下来示例会用到的场景: 三个实体:作者、书、书评。其中,作者与书是一对多的关系,书与书评是一对一的关系(当然书评与读者的评价是一对多的关系,这里省去,仅用一对一来进行演示即可)。 假设有这样的后台查询条件:作者名称、书的发布时间、书评的评分。(这里每个实体取一个字段进行连表查询演示,其他字段同理)。返回书籍列表以及相关表字段。 【本文所有代码在此】 最佳实践 需要 SELECT 查询的字段,通过单独的 Java Bean 进行映射 利用 JPA 的自动实体映射结果集 @EntityGraph 注解标注返回实体需要 Fetch 的字段 无需再手动针对连表进行 fetch,解决 N+1 问题 JOIN ON 查询条件使用 join().on() 拼接 Join<Object, Object> author = root.join("author"); author.on(cb.equal(author.get("name"), param.getAuthorName())); WHERE 查询条件使用 query.where() 拼接 query.where(cb.equal(root.get("publishTime"), param.getBookPublishTime())); 代码示例 针对 Repository 需要 override 已有 findAll 方法,使用 @EntityGraph 注解 使用 @EntityGraph 注解,标注额外属性需要 fetch // BookJoinRepository.java @Repository public interface BookJoinRepository extends JpaRepository<BookJoin, String>, JpaSpecificationExecutor<BookJoin> { @Override @EntityGraph(attributePaths = { "author", "review" }) Page<BookJoin> findAll(Specification<BookJoin> spec, Pageable pageable); } 针对 Specification WHERE 查询条件使用 query.where() 拼接 JOIN ON 查询条件使用 join().on() 拼接 static Specification<BookJoin> multiQuery_04(BookJoinQuery param) { return (root, query, cb) -> { if (null != param.getBookPublishTime()) { query.where(cb.equal(root.get("publishTime"), param.getBookPublishTime())); } if (null != param.getAuthorName()) { Join<Object, Object> author = root.join("author"); author.on(cb.equal(author.get("name"), param.getAuthorName())); } if (null != param.getReviewScore()) { Join<Object, Object> review = root.join("review"); review.on(cb.equal(review.get("score"), param.getReviewScore())); } return query.getRestriction(); }; } 结果 SQL 语句 select bookjoin0_.id as id1_1_0_, bookjoin_a1_.id as id1_0_1_, bookjoin_r2_.id as id1_2_2_, bookjoin0_.author_id as author_i3_1_0_, bookjoin0_.publish_time as publish_2_1_0_, bookjoin0_.review_id as review_i4_1_0_, bookjoin_a1_.name as name2_0_1_, bookjoin_r2_.score as score2_2_2_ from book bookjoin0_ inner join author bookjoin_a1_ on bookjoin0_.author_id = bookjoin_a1_.id and (bookjoin_a1_.name = ?) inner join review bookjoin_r2_ on bookjoin0_.review_id = bookjoin_r2_.id and (bookjoin_r2_.score = ?) where bookjoin0_.publish_time = ? limit ? 当然,这里案例使用的是 INNER JOIN,对于 LEFT JOIN 也是生效的。 ...

九月 25, 2022 · 2 分钟 · Lex Cao