在经过上一次针对开关的小试牛刀之后,我开始进一步的探索智能家居。又拿单片机搓了远程开关和亮度传感器,还购买了智能窗帘。
上一篇文章光一个api控制灯的开关,HA就的写个好几层if嵌套的 YAML ,高亮是没有的,写起来也麻烦。
面对日益繁多的设备和关联性更强更复杂的场景,HA的控制系统是愈发力不从心了。
HA或许可以写脚本,但看了看它种类繁多的设备分类,以及从字典里掏出来不知道啥类型的用法...感觉还是不太适合我
在拿单片机搓设备时,了解了MQTT协议,这个协议简单易懂。
刚好HA可以通过MQTT集成来控制设备,于是便萌生了把HA当适配器和看板,使用MQTT协议自行构建逻辑实现。
起初我是打算魔改用于聊天Bot的Nonebot2框架来实现。听说有位搞单片机的群友,将Nonebot2部署到了他公司,据说Nonebot2的事件驱动非常适合单片机的控制。
但深入思考了智能家居和聊天Bot的区别。
聊天Bot90%的使用场景并不需要太多的状态,如.gpt 你好,基本上把触发事件里的你好拿去处理就行了。
但智能家居需要的状态是非常多的,如要控制灯的开关,它受我是否睡觉,外界亮度两个状态的影响。
明显,聊天Bot受状态影响较小,需要全局存储状态的场景并没有多少。
而智能家居受状态影响较大,基本上受多变量控制的都要做全局状态存储。
因此我觉得以事件变动为主的响应式编程更适合构建智能家居的控制。
对于经常写前端的小伙伴来说,响应式编程一定非常熟悉。
就像写Excel一样,单元格里输入=A1+B1,当A1或B1的值发生变化时,单元格的值也会自动更新。
不论是 Vue 的reactive,还是 react 的useState,都是响应式编程的体现。
更进一步说,我可以把房间当成一个网页,按钮、传感器都是响应式变量,灯、窗帘都是用户看到的DOM元素。
带着这个思路,我就回到了我熟悉的领域。
直接照抄 Vue 的设计思路,将对象分为了三类
- 输入层:将传感器消息转换为响应式变量的对象,如按钮、传感器等。对应 Vue 中的
Reactive。 - 中间层:由输入对象计算而来的响应式变量,如外界是否暗到需要开灯。对应 Vue 中的
Computed。 - 处理层:由输入对象或中间对象触发的事件处理函数,如开灯、关灯等。对应 Vue 中的
watchEffect。
这套系统在前端开发中已经被验证了无数次,完全可以照搬过来。
MQTT协议的内容非常简洁。它是基于发布-订阅模式的通信协议。订阅一个主题,当有消息发布到这个主题时,订阅者就会收到消息。
订阅主题也就是一个字符串,没有HA里那种复杂的设备分类和用法,上手非常快。
可以在下面的文档里看到详细介绍。
我使用的是GMQT。使用方法很简单,解压完执行可执行文件就行了。
由于我写的 Vue 比较多,我下意识的选择了 Vue 的响应式库reactivity。
但后端的@vue/reactivity和前端环境下的 Vue 简直是两种东西,缺少诸如watchEffect等功能,使用起来非常麻烦。
我也并没有学会使用。
在之后我找到了一个叫reactor.js的响应式库,功能非常完善,使用起来也非常简单。
| reactor.js | Vue |
|---|---|
| Reactor | reactive |
| Observer | computed |
| Observer | watchEffect |
可惜的是这个库并没有类型文件,我拿ai写了个并修订了一些我使用时遇到的问题。有需要的可以直接拿去用。
declare module 'reactorjs' {
/**
* Type definitions for reactorjs
* Project: https://github.com/fynyky/reactor.js
* Definitions by: ChzxxuanzhengFile
*/
/**
* A Reactor is an object wrapper that automatically tracks Observer functions
* that read its properties and notifies the observers when those properties are updated.
*/
export class Reactor<T extends object = any> {
/**
* Creates a new Reactor.
* @param target - Optional object to wrap with reactive behavior. Changes to the reactor are passed through to the underlying object.
*/
constructor(target?: T)
}
export interface Observer<TArgs extends any[] = [], TReturn = any> {
/**
* Executes the observer function and starts tracking dependencies.
* @param args - Arguments to pass to the observer function.
* @returns The return value of the observer function.
*/
(...args: TArgs): TReturn
}
/**
* An Observer is a function wrapper that automatically tracks dependencies on Reactor properties
* and re-executes when those properties are updated.
*/
export class Observer<TArgs extends any[] = [], TReturn = any> {
/**
* Creates a new Observer.
* @param fn - The function to wrap with reactive behavior.
*/
constructor(fn: (...args: TArgs) => TReturn)
/**
* The last return value of the observer function.
* This property is itself observable by other observers.
*/
readonly value: TReturn
/**
* Starts the observer. Re-executes the function and begins tracking dependencies.
* If already started, subsequent calls have no effect.
*/
start(): void
/**
* Stops the observer. Clears any existing dependencies and prevents triggering.
*/
stop(): void
}
/**
* Shields a block of code from creating observer dependencies.
* Any reactor properties read inside the function will not be set as dependencies.
*
* @param fn - The function to execute without creating dependencies.
* @returns The return value of the function.
*
* @example
* ```typescript
* const taskList = new Reactor(["a", "b", "c", "d"])
* new Observer(() => {
* console.log(hide(() => taskList.pop()))
* })()
* ```
*/
export function hide<T>(fn: () => T): T
/**
* Batches multiple reactor updates together and triggers observers only once at the end.
* This prevents multiple repeated triggering when updating multiple properties.
*
* @param fn - The function containing multiple updates to batch.
* @returns The return value of the function.
*
* @example
* ```typescript
* const person = new Reactor({ firstName: "John", lastName: "Doe" })
* batch(() => {
* person.firstName = "Jane"
* person.lastName = "Smith"
* }) // Triggers observers only once
* ```
*/
export function batch<T>(fn: () => T): T
/**
* Removes the Reactor wrapper and returns the underlying base object.
* This is necessary for native objects or objects with private properties that don't work with proxies.
*
* @param reactor - The reactor to unwrap, or the observer to extract the function from.
* @returns The underlying object or function.
*
* @example
* ```typescript
* const mapReactor = new Reactor(new Map())
* Map.prototype.keys.call(shuck(mapReactor)) // works fine
*
* const myFunction = () => {}
* const observer = new Observer(myFunction)
* myFunction === shuck(observer) // true
* ```
*/
export function shuck<T extends object>(reactor: Reactor<T>): T
export function shuck<TArgs extends any[], TReturn>(
observer: Observer<TArgs, TReturn>,
): (...args: TArgs) => TReturn
export function shuck(target: any): any
/**
* Type helper for creating a reactive version of an object type.
* Properties of the object become reactive and can trigger observers.
*/
export type Reactive<T> = T extends object ? Reactor<T> & T : T
}
在js环境下使用MQTT非常简单,使用mqtt.js库就可以了。
我计划一个订阅主题就是一个输入对象。由于有通配符订阅主题的存在,不同的订阅会用到相同的主题。
因此我把MQTT的原始payload存储在状态仓库,不同的输入对象去这里获取自己的payload进行转换处理。
我成功用这套控制系统写出了我满意的控制逻辑,虽然不支持异步吧...但我也用不到异步。反正感觉比HA的模板好用多了。
但静下来想想,虽然HA使用并不方便,但却是比自己再搓一个这样的框架方便多了...果然我就是喜欢重复造轮子。
其实这是我半个月前写的东西啦~不过昨完后就被研究生拉着写论文,写完了还要给博客写新样式,现在回首来看,槽点还是蛮多的。
- 向
输入对象发送通知看起来更像是状态仓库的工作,而不是收到消息的工作。都由输入对象发起就凭空的引入了优先级问题。
但我现在已经不想碰这个东西了...能跑就不碰是对的
INFO
