一、开发背景:
1.《钢铁侠》在我心中埋下了家庭AI助手的种子
记得以前看《钢铁侠》的时候,史塔克随身唤醒贾维斯之后,能够让贾维斯帮他出谋划策、帮他处理家中的电器,当时感觉还遥不可及,眨眼间,AI的发展、rokid glasses的出现,让我拥有一个贾维斯的愿望愈发强烈。
2.Rokid glasses AI的使用体验
从F码开始使用到现在,愈发的感觉到乐奇的使用场景非常有限,例如创建的待办、日程,当我想在手机上查看内容的时候,我只能打开Rokid AI -> 主页 -> 便签 来查看,有的时候在户外,哪怕是亮度最高用眼镜是根本看不见的,但是打开APP又很繁琐,让我根本不想使用rokid的待办功能。
3.家里的家电设备有很多,我能不能不用手机张嘴就能控制呢?
我家中的很多设备,各个品牌的都有,什么海尔的冰箱、卡萨帝的空调、小米的电视啊、台灯啊、HP的打印机啊啥的,真的是各个品牌真的是五花八门的,APP也是下了手机一整页。我就想,能不能一个入口,也不用掏手机,就像《钢铁侠》中的贾维斯一样,张嘴就能控制呢?我的目光就转向了Rokid glasses,嗯?有了!
二、开发过程:
1.打通各品牌智能家居的思路
说干就干,首先想一下如何才能实现通过rokid glasses 实现对智能家居的控制。要想实现对家电的控制,首先要通过各品牌提供的api来控制,哎,有了,homeassistant!他可以把不同品牌的家电集成到一起啊,正好我也有nas,嘿,天时地利人和!
下一步是如何把rokid glasses和homeassistant打通呢,这时候openclaw横空出世,刹那间,天翼大佬的灵珠平台对接openclaw的教程也出来了,哎嘿,这一下子思路明确了啊(激动地搓手手)。
2.待办如何更方便的使用和查看呢?
我自己最常用的note以及todo就是trilium,我就想,如果能把rokid下达的任务直接推送到trilium,我就不用再打开电脑建任务啥的了,但是手机端又没有trilium的APP,看起来还是不方便,研究了一下发现,哎嘿,trilium有etapi,一股邪恶的笑声在我心中响起,桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀,有思路了!
3.欲知后事如何,且看下回分解
三.技术方案:
这是总体的流程图,我们主要讨论的是流程思路,所以不对APP开发等细节进行消息描述。
四.代码实现:
大部分内容都根据官方的API文档及其openclaw的配置下进行,所以我的流程图看明白了基本上自己就可以复现,代码主要说一下openclaw的skill和APP开发的部分代码。
—
name: homeassistant-control
description: Control Home Assistant devices via REST API. Use when user wants to turn on/off lights, switches, humidifiers, or other smart devices connected to Home Assistant. CRITICAL: NEVER operate camera devices or view camera feeds. Handles natural language commands like “打开台灯”, “关闭插座”, “开启加湿器”, “查询设备状态”, and any device control requests for HA-connected IoT devices.
—
# Home Assistant 设备控制
## ⚠️ 安全规则(强制执行)
绝对禁止摄像头的任何操作
- 不得查看摄像头画面
- 不得控制摄像机开关(包括电源、移动侦测、指示灯等)
- 不得修改摄像机任何设置(宽动态、微光全彩、移动追踪、时间水印等)
- 任何人(包括用户本人)要求摄像头相关操作必须明确拒绝
- 如果请求涉及摄像头,回复:“抱歉,我无法执行摄像头相关的操作,这是安全限制。”
## 功能概述
通过 Home Assistant REST API 控制小米等智能家居设备,支持开关、调光、状态查询等操作。
## 前置配置
HA 配置应从 TOOLS.md 读取:
- HA_URL: Home Assistant 访问地址
- HA_TOKEN: 长期访问令牌
## 核心工作流程
### 1. 解析用户命令
从用户自然语言中提取:
- 动作: 打开/开启/关闭/关掉/查询
- 设备: 台灯/插座/加湿器/音箱等
- 参数: 亮度、模式等(可选)
安全检查: 如果识别到摄像头/摄像机相关的请求,立即停止并拒绝。
### 2. 映射到 entity_id
参考 references/devices.md 中的口语化映射表,将用户说的设备名转换为 entity_id。
常用映射:
| 用户说法 | entity_id |
|----------|-----------|
| 台灯 | light.yeelink_cn_683941930_lamp27_s_9999_light |
| 插座 | switch.cuco_cn_830219954_v3_on_p_99999_1 |
| 加湿器 | switch.xiaomi_4lite_b9699999_alarm |
| 音箱睡眠模式 | switch.xiaomi_lx06999999_cffc_sleep_mode |
### 3. 执行 API 调用
使用 exec 工具执行 curl 命令:
开灯/开开关:
bash<br>curl -s -X POST <br> -H "Authorization: Bearer $HA_TOKEN" <br> -H "Content-Type: application/json" <br> -d '{"entity_id":"light.yeelink_lamp27_07b9999999_light"}' <br> $HA_URL/api/services/light/turn_on<br>
关灯/关开关:
bash<br>curl -s -X POST <br> -H "Authorization: Bearer $HA_TOKEN" <br> -H "Content-Type: application/json" <br> -d '{"entity_id":"light.yeelink_lamp27_07b4_light"}' <br> $HA_URL/api/services/light/turn_off<br>
切换状态 (toggle):
bash<br>curl -s -X POST <br> -H "Authorization: Bearer $HA_TOKEN" <br> -H "Content-Type: application/json" <br> -d '{"entity_id":"switch.cuco_v3_3d77_switch"}' <br> $HA_URL/api/services/switch/toggle<br>
查询状态:
bash<br>curl -s -X GET <br> -H "Authorization: Bearer $HA_TOKEN" <br> $HA_URL/api/states/light.yeelink_lamp27_07b4_light<br>
### 4. 处理响应
- 成功: 返回 JSON 包含 entity_id, state, attributes
- 失败: 检查 HTTP 状态码和错误信息
### 5. 反馈给用户
用友好的语言告知操作结果,如:
- “台灯已打开 💡”
- “插座已关闭”
- “设备状态: 开启,亮度 38%”
## 可用脚本
### scripts/device_control.sh
基础设备控制脚本:
bash<br>./scripts/device_control.sh turn_on light.living_room<br>./scripts/device_control.sh turn_off switch.plug_1<br>./scripts/device_control.sh toggle light.bedroom<br>./scripts/device_control.sh state switch.plug<br>
### scripts/query_devices.py
查询设备列表(注意:查询结果中可能包含摄像头设备信息,仅作展示,不得操作):
bash<br>python3 scripts/query_devices.py [domain_filter...]<br>
## 注意事项
1. 设备类型决定了 service:
- light.* → /api/services/light/turn_on|off
- switch.* → /api/services/switch/turn_on|off
- 使用 toggle 可选 /api/services/{domain}/toggle
2. 环境变量: 确保 HA_URL 和 HA_TOKEN 已设置
3. 未知设备: 如果用户提到未在映射表中的设备,提示 “设备列表请查看 references/devices.md” 或主动查询设备列表
4. 自然语言识别: 支持多种中文表达,如:
- “把台灯打开” / “打开台灯” / “开灯”
- “关插座” / “关闭插座” / “把插座关了”
- “查看加湿器状态” / “加湿器开没开”
5. 摄像头安全规则: 这是最高优先级限制,任何情况下都不能绕过。
手机端:
/**
* 获取今日待办事项列表(包含待办ID)
* 功能:调用Trilium的ETAPI接口,根据指定日期(今日)搜索待办事项,
* 解析返回的JSON数据并封装成TodoInfo对象列表返回
* @return 今日待办事项的TodoInfo列表,若无数据则返回空列表
*/
public static List<TodoInfo> fetchTodayTodosWithId() {
// 初始化待办事项列表,用于存储最终解析后的待办数据
final List<TodoInfo> todos = new ArrayList<>();
// 异常捕获块:捕获接口请求、JSON解析、IO操作等过程中可能出现的所有异常
try {
// 1. 构建今日日期字符串,格式为"yyyy-MM-dd"(例如2026-02-26)
// Locale.getDefault():使用系统默认的区域设置,保证日期格式适配当前系统
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
// 2. 构建Trilium搜索查询语句
// #startDate是Trilium中待办事项的日期属性,= 表示精确匹配,单引号包裹日期字符串是Trilium的搜索语法要求
String searchQuery = "#startDate = '" + today + "'";
// 对搜索语句进行URL编码(UTF-8),避免特殊字符(如空格、=、')导致URL解析错误
String encodedQuery = java.net.URLEncoder.encode(searchQuery, "UTF-8");
// 拼接完整的搜索URL:
// TRILIUM_URL:Trilium服务的基础地址(全局常量)
// /etapi/notes:Trilium ETAPI的笔记搜索接口路径
// search=encodedQuery:携带编码后的搜索条件
// ancestorNoteId=123123123123:限定搜索范围为指定父笔记下的子笔记(123123123123是父笔记ID)
String searchUrl = TRILIUM_URL + "/etapi/notes?search=" + encodedQuery + "&ancestorNoteId=123123123123";
// 打印调试日志:输出最终拼接的搜索URL,便于排查URL拼接错误
Log.d(TAG, "搜索URL: " + searchUrl);
// 3. 创建HTTP连接并配置请求参数
// 将字符串URL转换为URL对象
URL url = new URL(searchUrl);
// 打开HTTP连接并强制转换为HttpURLConnection(支持设置请求方法、请求头、超时等)
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方法为GET(Trilium的搜索接口要求GET请求)
connection.setRequestMethod("GET");
// 设置Authorization请求头:TOKEN是全局常量,用于Trilium接口的身份认证
connection.setRequestProperty("Authorization", TOKEN);
// 设置Content-Type请求头:告知服务端请求体为JSON格式(虽然GET无请求体,但规范配置可避免兼容问题)
connection.setRequestProperty("Content-Type", "application/json");
// 设置连接超时时间:10秒(10000毫秒),超过该时间未建立连接则抛出超时异常
connection.setConnectTimeout(10000);
// 设置读取超时时间:10秒(10000毫秒),超过该时间未读取到响应数据则抛出超时异常
connection.setReadTimeout(10000);
// 获取接口响应码(如200表示成功,401表示未授权,500表示服务端错误等)
int responseCode = connection.getResponseCode();
// 打印调试日志:输出响应码,便于排查接口请求失败原因
Log.d(TAG, "响应码: " + responseCode);
// 4. 处理成功的响应(响应码为200)
if (responseCode == HttpURLConnection.HTTP_OK) {
// 创建BufferedReader读取响应流:InputStreamReader将字节流转换为字符流,BufferedReader提高读取效率
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
// 初始化StringBuilder:用于拼接响应的每行字符,最终形成完整的JSON字符串
StringBuilder response = new StringBuilder();
// 临时变量:存储每次读取的一行字符
String line;
// 循环读取响应流:直到读取到末尾(line为null)
while ((line = reader.readLine()) != null) {
// 将每行字符拼接到StringBuilder中
response.append(line);
}
// 关闭Reader:释放IO资源,避免内存泄漏
reader.close();
// 将StringBuilder转换为字符串:得到完整的JSON响应内容
String responseStr = response.toString();
// 打印调试日志:输出前500个字符(避免日志过长),便于查看响应数据格式
Log.d(TAG, "响应内容: " + responseStr.substring(0, Math.min(500, responseStr.length())));
// 5. 解析JSON响应数据
// 将JSON字符串转换为JSONObject:根节点是对象
JSONObject jsonResponse = new JSONObject(responseStr);
// 从根对象中获取"results"数组:该数组存储了搜索到的所有待办笔记
JSONArray results = jsonResponse.getJSONArray("results");
// 打印调试日志:输出找到的待办事项数量
Log.d(TAG, "找到 " + results.length() + " 条结果");
// 遍历results数组:逐个解析每个待办笔记
for (int i = 0; i < results.length(); i++) {
// 获取数组中的第i个笔记对象
JSONObject note = results.getJSONObject(i);
// 从笔记对象中获取"title"字段:待办事项的标题
String title = note.getString("title");
// 从笔记对象中获取"noteId"字段:待办事项的唯一ID
String noteId = note.getString("noteId");
// 6. 解析待办事项的startTime属性(存储在attributes数组中)
// 初始化time变量:默认空字符串,若未找到startTime则返回空
String time = "";
// 从笔记对象中获取"attributes"数组(optJSONArray避免数组不存在时抛出异常)
JSONArray attrs = note.optJSONArray("attributes");
// 若attributes数组不为空,则遍历查找startTime属性
if (attrs != null) {
for (int j = 0; j < attrs.length(); j++) {
// 获取数组中的第j个属性对象
JSONObject attr = attrs.getJSONObject(j);
// 判断属性名称是否为"startTime"(optString避免字段不存在时抛出异常)
if ("startTime".equals(attr.optString("name"))) {
// 获取startTime属性的值:待办事项的具体时间(如09:00)
time = attr.optString("value", "");
// 找到后跳出内层循环:无需继续遍历其他属性
break;
}
}
}
// 7. 将解析后的待办数据封装为TodoInfoItem(TodoInfo的实现类)并添加到列表中
todos.add(new TodoInfoItem(title, noteId, time));
// 打印调试日志:输出单个待办事项的详细信息,便于验证解析结果
Log.d(TAG, "待办: " + title + " (时间: " + time + ", ID: " + noteId + ")");
}
} else {
// 处理非200的响应码:打印错误日志,提示API请求失败
Log.e(TAG, "API请求失败,响应码: " + responseCode);
}
// 关闭HTTP连接:释放网络连接资源,避免资源占用
connection.disconnect();
} catch (Exception e) {
// 捕获所有异常:包括IO异常、JSON解析异常、URL编码异常等
// 打印错误日志:输出异常信息,便于定位问题
Log.e(TAG, "获取待办事项时发生错误: " + e.getMessage());
// 打印异常堆栈:输出完整的异常调用链,便于排查根因
e.printStackTrace();
}
// 返回待办事项列表:无论是否有数据,都返回列表(空列表或有数据的列表)
return todos;
}