ha-coding
v1.0.4
Published
ha-coding
Readme
面向Home Assistant,使用TypeScript编写全平台自动化!更自由,更灵活!
用法风格概览
// 监听卫生间人在传感器有人无人变化
onChange(
() => bathroom.occupySensor.occupied, // 有人无人状态
(occupied) => {
if (occupied) {
// 如果有人则开灯
bathroom.lamp.on = true;
} else {
// 如果无人则关灯
bathroom.lamp.on = false;
}
}
);快速上手
前提:
- 已经安装好 Home Assistant 以及相关集成。
- 已经安装好 Nodejs 环境。
- 已经安装好 VS Code 开发工具。
下载项目模版: 下载页面 (进入页面后点击下载按钮下载)
安装: 解压项目模板文件并在终端中打开,执行以下命令:
cd my-home
npm install
npm install ha-coding配置项目: 用VS Code打开项目,打开根目录下的 config.js 文件,并配置成你的 Home Assistant 的相关配置
export default {
/** HomeAssistant后台ip地址:端口号,如:192.168.31.156:8123 */
IP_ADDRESS_PORT: '',
/** HomeAssistant用户名 */
HA_USER_NAME: '',
/** HomeAssistant密码 */
HA_PASSWORD: ''
};启动项目: 在终端中执行以下命令:
npm start等待几秒后控制台打印 “HA Coding 启动成功!”,则证明启动成功。如果控制台报错,则为启动失败。
使用说明
定义设备
定义设备是为了告知系统每个设备是如何与 Home Assistant 交互的,推荐在项目的 devices-def 文件夹下定义
@Device()
export class MiLight implements DeviceDef {
$entityIds: { light: string };
@State(function (this: MiLight, value: MiLight['on']) {
return { service: value ? 'turn_on' : 'turn_off', entityId: this.$entityIds.light };
})
on: boolean;
@State(function (this: MiLight, value: MiLight['brightness']) {
return {
service: 'turn_on',
serviceData: {
brightness_pct: value
},
entityId: this.$entityIds.light
};
})
brightness: number;
@State(function (this: MiLight, value: MiLight['colorTemperature']) {
return {
service: 'turn_on',
serviceData: {
color_temp_kelvin: value
},
entityId: this.$entityIds.light
};
})
colorTemperature: number;
$onEvent({ a, s }: MiLightEvent, entityId: string): void {
if (entityId !== this.$entityIds.light) return;
if (s === 'on') {
this.on = true;
} else if (s === 'off') {
this.on = false;
}
if (a.brightness !== undefined && a.brightness !== null) {
this.brightness = Math.round((a.brightness * 100) / 255);
}
if (a.color_temp_kelvin !== undefined && a.color_temp_kelvin !== null) {
this.colorTemperature = a.color_temp_kelvin;
}
}
}如上定义了一个米家智能灯设备:
- 创建一个 MiLight 类并实现 DeviceDef 接口,并使用 @Device() 装饰器装饰 MiLight 类。
- 创建 $entityIds 成员变量,用于存放该设备下的所有实体id。
- 定义设备的属性 on(开关状态)、brightness(亮度)、colorTemperature(色温),并使用 @State() 装饰器装饰。@State() 装饰器中的参数回调方法返回发送信息来告知系统当该属性发生变化时,如何发送到Home Assistant上。
- 定义 $onEvent 方法,当 Home Assistant 产生事件时会调用该方法,将事件信息映射到设备的属性上。
事件信息和发送信息可以在 Home Assistant 网页上使用 F12 查看 WebSocket 记录查询到。
创建设备实例
在定义好设备后,我们需要创建这些设备的实例,以便在后续为这些设备编写自动化,推荐在项目的devices文件夹下创建
export const bathroom = {
lamp: createDevice(MiLight, { light: 'your entity id' }),
occupySensor: createDevice(MiTrio, {
allArea: 'your entity id',
illumination: 'your entity id',
area1: 'your entity id',
area2: 'your entity id'
})
};如上使用 createDevice() 方法创建了卫生间的一个米家智能灯设备和一个Trio人在传感器设备,createDevice() 方法的第一个参数为设备的定义类,第二个参数为该设备下的所有实体id。
编写自动化
在创建好设备后,我们就可以为这些设备编写自动化了,推荐在项目的automation文件夹下创建
// 监听卫生间人在传感器区域1有人无人变化
onChange(
() => bathroom.occupySensor.area1Occupied, // 区域1有人无人状态
(area1Occupied) => {
if (area1Occupied) {
// 如果有人则开灯
bathroom.lamp.on = true;
} else {
// 如果无人则关灯
bathroom.lamp.on = false;
}
}
);如上使用onChange()方法,监听区域1有人无人状态,如果区域1有人则开灯,无人则关灯。更多的使用说明可以在下面的 API 中查询。
API
onChange()
function onChange<T>(
statesGetter: () => T,
cb: (states: CbStates<T>, oldStates: CbStates<T>) => void,
onChangeOptions?: {
immediate?: boolean;
}
): {
pause: () => void;
resume: () => void;
};onChange() 方法用于监听设备的状态变化,执行相关的逻辑。
参数:
- statesGetter - 用于指定需要监听的设备状态,可以同时监听多个设备的多个状态。
- cb - 状态变化后调用的回调方法。
- onChangeOptions - 指定 onChange() 方法的监听选项:
- immediate - 是否在调用 onChange() 方法后,立即执行一次回调方法。
返回值:
- pause - 调用该方法以暂停监听
- resume - 调用该方法以恢复监听
onSwitch()
function onSwitch<T>(
statesGetter: () => T,
from: T | '*' | '**',
to: T | '*' | '**',
cb: (states: CbStates<T>, oldStates: CbStates<T>) => void
): {
pause: () => void;
resume: () => void;
};onSwitch() 方法用于监听设备状态从特定值变为另一个特定值时,执行相关逻辑。支持通配符匹配。
参数:
- statesGetter - 用于指定需要监听的设备状态。
- from - 变化前的值。可以是具体值、
'*'(匹配任意值,但排除to的值)或'**'(匹配任意值,包括to的值)。 - to - 变化后的值。可以是具体值、
'*'(匹配任意值,但排除from的值)或'**'(匹配任意值,包括from的值)。 - cb - 匹配到指定转换后调用的回调方法。
返回值:
- pause - 调用该方法以暂停监听
- resume - 调用该方法以恢复监听
示例:
// 精确匹配:有人 → 无人
onSwitch(() => sensor.occupied, true, false, () => {
bathroom.lamp.on = false;
});
// 通配符:任意值 → 关闭(排除 关闭→关闭)
onSwitch(() => light.on, '*', false, () => {
console.log('灯被关闭了');
});
// 从特定值 → 任意值
onSwitch(() => light.colorTemperature, 4000, '*', () => {
console.log('色温从4000变为了其他值');
});onKeep()
function onKeep(
statesPredicate: () => boolean,
cb: () => void,
keepTime?: number,
lifecycle?: { onMatch?: () => void; onBreak?: () => void }
): {
stop: () => void;
resume: () => void;
skip: () => void;
hit: () => void;
};onKeep() 方法用于在设备状态维持了一段时间后,执行相关逻辑。
参数:
- statesPredicate - 状态是否符合预期。
- cb - 状态符合预期并维持了指定时长后的回调方法。
- keepTime - 指定设备状态维持的时长(单位:毫秒)。
- lifecycle - 生命周期:onMatch - 每次符合条件时的回调函数。 onBreak - 每次不符合条件时的回调函数。
返回值:
- stop - 调用该方法以停止监听,并将维持时间清零
- resume - 调用该方法以恢复监听
- skip - 调用该方法以舍弃本次状态符合预期的延时任务
- hit - 调用该方法以舍弃本次状态符合预期的延时任务,并直接调用回调方法
stage()
function stage<T extends [ReturnType<typeof step<any>>, ReturnType<typeof step<any>>, ...ReturnType<typeof step<any>>[]]>(
...steps: T
): {
next: (waitingTime?: number) => void;
prev: (waitingTime?: number) => void;
goto: (stepIndex: ArrayIndexes<T>) => void;
reset: () => void;
pause: () => void;
resume: () => void;
}stage() 方法用于在事件或状态先后发生后,执行相关逻辑。
参数:
- steps - 事件的阶段
返回值:
- next - 进入下一个阶段。 waitingTime - 下一个阶段等待的时长(单位:毫秒),超时则重置到第一个阶段。
- prev - 进入上一个阶段。 waitingTime - 上一个阶段等待的时长(单位:毫秒),超时则重置到第一个阶段。
- goto - 进入指定阶段。 waitingTime - 指定阶段等待的时长(单位:毫秒),超时则重置到第一个阶段。
- reset - 重置到第一个阶段。
- pause - 暂停当前阶段的监听。
- resume - 恢复当前阶段的监听。
step()
function step<T>(
statesGetter: () => T,
cb: (states: CbStates<T>, oldStates: CbStates<T>) => void,
onChangeOptions?: {
immediate?: boolean;
}
): Parameters<typeof onChange<T>>;step() 方法用于定义 stage() 中的每个阶段,参数与 onChange() 一致。
Timer
class Timer {
constructor();
after: (cb: () => void, time: number) => () => void;
cancel: () => void;
}Timer类用于延时执行某段逻辑,需要实例化后使用。
方法:
- after() - 延时执行某段逻辑,再次调用会取消上一次的延时执行逻辑。 cb - 要执行逻辑的回调方法。 time - 延时的时间(单位:毫秒)。 返回值 - 取消延时执行这段逻辑。
- cancel() - 取消延时执行某段逻辑。
delay()
function delay(cb: () => void, time: number): () => void;delay() 方法用于延时执行某段逻辑,与 Timer 不同的是,再次调用不会取消上一次的延时执行逻辑。
参数:
- cb - 要执行逻辑的回调方法。
- time - 延时的时间(单位:毫秒)。
schedule()
function schedule(
time: TimeStr | TimeStr[] | ((date: DateStr, week: number) => TimeStr | TimeStr[]),
cb: () => void,
repeatType?: RepeatType
): {
pause: () => void;
resume: () => void;
};schedule() 方法用于在当天定时执行某段逻辑。
参数:
- time - 定时的时间。格式为时间字符串,如:"16:21:00",也支持传入时间字符串数组来设置多个定时时间。也可以通过回调方法返回时间字符串或时间字符串数组,回调方法参数 date - 当天的日期,week - 当天是周几(0是周日,1是周一,2是周二 ...以此类推)。
- cb - 到设定的定时时间的回调方法。
- repeatType - 重复类型,默认为 'EVERY_DAY'。可选的值如下:
export type RepeatType =
| 'EVERY_DAY' // 每天(默认)
| 'WEEK_DAY' // 周一到周五
| 'WEEKEND' // 周六日
| 'WORK_DAY' // 工作日(算上调休日)
| 'NON_WORK_DAY' // 周六日和法定节假日
| WEEK[] // 指定星期几
| ((date: DateStr, week: number) => boolean); // 返回值为真则当日执行,为假当日不执行。 date - 当天的日期,week - 当天是周几(0是周日,1是周一,2是周二 ...以此类推)。返回值:
- pause - 调用该方法以暂停定时任务
- resume - 调用该方法以恢复定时任务
onDetect()
function onDetect<T>(
statesGetter: () => T,
cb: (states: CbStates<T>, historyStates: CbStates<T>[]) => void,
period: number
): {
pause: () => void;
resume: () => void;
reset: () => void;
};onDetect() 方法用于记录一段时间内的状态,供用户判断,并执行相关逻辑。如:温度在3小时内变化超过5度,执行某段逻辑。
参数:
- statesGetter - 用于指定需要监听的设备状态,可以同时监听多个设备的多个状态。
- cb - 状态变化后调用的回调方法。
- period - 指定时间段(单位:毫秒)。
返回值:
- pause - 调用该方法以暂停监听
- resume - 调用该方法以恢复监听
- reset - 重置(清除历史状态)
@Device()
function Device(): ClassDecorator;@Device() 装饰器用于装饰设备定义类,设备定义类被其装饰才会有本篇说明的一系列效果。
@State()
function State(): PropertyDecorator;
function State(callInfoGetter: CallInfoGetter): PropertyDecorator;
function State(stateOptions: StateOptions): PropertyDecorator;
function State(callInfoGetter: CallInfoGetter, stateOptions: StateOptions): PropertyDecorator;@State() 装饰器装饰的变量会变为响应式变量,可以被 onChange()、onSwitch()、onKeep() 等方法监听。 参数:
- callInfoGetter - 返回CallInfo对象,告知系统如何向 Home Assistant 发送报文以更新设备状态。
- stateOptions - 目前内部仅有一个属性 persistentKeyGetter,该属性是方法类型,如果你需要持久化这个 state 成员变量,可以设置该方法,并返回一个唯一的key,每次值变化时,系统将用这个 key 作为键将其持久化 (保存到磁盘上)。下次系统启动时,系统会根据这个 key,取回持久化的值赋值给该成员变量。取回的值的优先级大于为其赋的初始值。
@Action()
function Action(): MethodDecorator;@Action() 装饰器用于装饰设备的无状态事件(如:无线开关的单击事件)的方法,该方法被调用时,可以被 onChange()、onSwitch()、onKeep() 等方法监听到。
ref()
function ref<T>(value?: T): Ref<T>;ref() 方法用于创建响应式变量,与被State()装饰器装饰的变量一样,同样可以被 onChange()、onSwitch()、onKeep() 等方法监听。
参数:
- value - 变量的初始值。
返回值: 创建的响应式变量。该变量有3个属性:
- value - 响应式变量的值。
- trigger - 调用 trigger 方法以立即触发监听该 ref 对象的所有回调方法。
- asPersistent - 调用此方法可以将这个 ref 对象的值进行持久化(保存到磁盘上),该方法需要向其传递一个 key,并保证其唯一性,每次这个 ref 对象的值变化时,系统将用这个 key 作为键存储这个值。下次系统启动时,系统会根据这个 key,取回持久化的值赋值给 ref 对象 (ref.value)。取回的值的优先级大于传入 ref 方法的参数值,如:const r = ref(123).asPersistent('my_key'); r.value = 456; 下次进入系统时,r.value 的值为 456。
createDevice()
createDevice<T extends Class<DeviceDef>>(
deviceDef: T,
entityIds: InstanceType<T>['$entityIds'],
...cps: ConstructorParameters<T>
): InstanceType<T>;createDevice() 方法用于创建设备的实例。具体用法可以参考创建设备实例章节。
onStartup()
function onStartup(cb: () => void): void;onStartup() 方法用于在 HA Coding 启动时添加一个回调函数,HA Coding 启动时会调用这个函数。
call()
export interface CallInfo {
entityId: string;
service: string;
serviceData?: Record<string, any>;
unmergeable?: boolean;
}
call(callInfo: CallInfo): void;call() 方法用于下发要对某一设备执行的操作。
sendNotification()
export interface NotificationInfo {
entityId: string;
content: string;
}
sendNotification(notificationInfo: NotificationInfo): void;sendNotification() 方法用于为某一设备实体发送通知。
customSubscribe()
customSubscribe(cb: (msgData: ObjectType) => boolean): numbercustomSubscribe() 方法用于自定义订阅Home assistant Websocket返回的消息。
removeCustomSubscribe()
removeCustomSubscribe(customSubscribeId: number): void;removeCustomSubscribe() 方法用于移除自定义订阅。
sendMessage()
sendMessage(msg: string | ObjectType): void;sendMessage() 方法用于向Home assistant Websocket发送自定义消息。
getUnavailableEntities()
function getUnavailableEntities(): Ref<string[]>;getUnavailableEntities() 方法用于获取在 Home Assistant 中不可用的设备(大概率是离线设备)。返回值是Ref包装的当前不可用的设备下的实体ID数组,可以被 onChange、onSwitch、onKeep 等监听。
logger
import { logger } from 'ha-coding';
logger.info(...args: any[]): void;
logger.warn(...args: any[]): void;
logger.error(...args: any[]): void;
logger.mark(desc: string): void;
logger.print(...args: any[]): void;
logger.printWarn(...args: any[]): void;
logger.printError(...args: any[]): void;logger 对象用于记录日志。所有日志会自动写入项目根目录下的 logs/ 文件夹,日志文件按天自动轮转(文件名格式:YYYY-MM-DD.log)。
方法:
- info(...args) - 记录 INFO 级别日志。
- warn(...args) - 记录 WARN 级别日志。
- error(...args) - 记录 ERROR 级别日志。
- mark(desc) - 在 onChange、onSwitch、onKeep、onDetect、stage、schedule、delay、timer 的回调方法中调用,标记本次回调需要记录日志。框架会自动根据所在 action 的类型格式化输出日志。只在 cb 同步执行期间有效。
- print(...args) - 记录 INFO 级别日志,并始终输出到控制台。
- printWarn(...args) - 记录 WARN 级别日志,并始终输出到控制台。
- printError(...args) - 记录 ERROR 级别日志,并始终输出到控制台。
各 action 中的日志输出格式:
[2026-03-15 12:00:00] [INFO] 你的日志内容
[2026-03-15 12:00:01] [WARN] 警告信息
[2026-03-15 12:00:02] [ERROR] 错误信息
// onChange / onSwitch / onDetect - 带状态变化
[2026-03-15 12:00:03] [INFO] [onChange:卫生间人在感应] false → true
[2026-03-15 12:00:03] [INFO] [onSwitch:灯开关状态] true → false
[2026-03-15 12:00:03] [INFO] [onDetect:温度检测] 22 → 28
// stage - 自动带步骤序号
[2026-03-15 12:00:04] [INFO] [stage(0):按钮点击] undefined → true
[2026-03-15 12:00:05] [INFO] [stage(1):人在检测] false → true
// onKeep - 区分 cb / hit / onMatch / onBreak
[2026-03-15 12:00:06] [INFO] [onKeep:设置台灯]
[2026-03-15 12:00:06] [INFO] [onKeep(hit):设置台灯]
[2026-03-15 12:00:06] [INFO] [onKeep(onMatch):条件匹配]
[2026-03-15 12:00:06] [INFO] [onKeep(onBreak):条件中断]
// schedule / delay / timer - 无状态变化
[2026-03-15 12:00:07] [INFO] [schedule:开灯]
[2026-03-15 12:00:08] [INFO] [delay:延时操作]
[2026-03-15 12:00:09] [INFO] [timer:定时操作]示例:
import { onChange, onSwitch, onKeep, schedule, delay, logger } from 'ha-coding';
// 手动记录日志
logger.info('卫生间灯已开启');
logger.warn('传感器信号不稳定', sensorId);
logger.error('设备控制失败', error);
// 配合 onChange:每次状态变化都记录
onChange(
() => bathroom.occupySensor.occupied,
(occupied) => {
logger.mark('卫生间人在感应');
bathroom.lamp.on = occupied;
}
);
// 配合 onChange:条件内调用 mark,只在匹配时记录
onChange(
() => gateway.virtualEventOccur,
(eventName) => {
if (eventName === 'v-查询温度') {
logger.mark('查询温度');
xiaoaiPlayText(`当前温度${sensor.temperature}度`);
}
}
);
// 配合 onSwitch
onSwitch(
() => light.on,
true,
false,
() => {
logger.mark('灯开关状态');
logger.info('灯被关闭,执行后续操作');
}
);
// 配合 onKeep
onKeep(
() => bathroom.occupySensor.occupied,
() => {
logger.mark('关灯');
bathroom.lamp.on = false;
},
5 * 60 * 1000,
{
onMatch: () => { logger.mark('无人开始计时'); },
onBreak: () => { logger.mark('有人取消计时'); }
}
);
// 配合 schedule
schedule('08:00:00', () => {
logger.mark('早间开灯');
livingRoom.lamp.on = true;
});其他
/** 深拷贝一个对象 */
export function cloneDeep<T>(value: T): T;
/** 判断两个对象中的属性是否全部相等 */
export function isEqual(value: any, other: any): boolean;
/** 判断是否是周一到周五 */
export function isWeekday(date?: DateStr): boolean;
/** 判断是否是周六日 */
export function isWeekend(date?: DateStr): boolean;
/** 判断是否是工作日(算上调休日) */
export function isWorkDay(date?: DateStr): boolean;
/** 判断是否是周六日或法定节假日 */
export function isNotWorkDay(date?: DateStr): boolean;
/** 获取太阳相关信息 */
export interface SunInfo {
nadir: TimeStr; // 正午夜时间
nightEnd: TimeStr; // 夜晚结束时间 (天即将开始亮)
nauticalDawn: TimeStr; // 航海黎明时间 (天开始蒙蒙亮)
dawn: TimeStr; // 黎明时间
sunrise: TimeStr; // 日出时间 (太阳的顶部边缘和地平线相切)
sunriseEnd: TimeStr; // 日出结束时间 (太阳的底部边缘和地平线相切)
goldenHourEnd: TimeStr; // 清晨金色太阳结束时间
solarNoon: TimeStr; // 正午时间
goldenHour: TimeStr; // 傍晚太阳开始变成金色时间
sunsetStart: TimeStr; // 日落开始时间 (太阳的底部边缘和地平线相切)
sunset: TimeStr; // 日落时间 (太阳的顶部边缘和地平线相切)
dusk: TimeStr; // 黄昏时间
nauticalDusk: TimeStr; // 航海黄昏时间 (天基本上黑了)
night: TimeStr; // 夜晚开始时间 (天已经足够暗)
}
export function getSunInfo(date?: DateStr): SunInfo;
/** 获取日出时间 */
export function getSunriseTime(date?: DateStr): TimeStr;
/** 获取日落时间 */
export function getSunsetTime(date?: DateStr): TimeStr;
/** 判断是否在某一时间范围内 */
export function inTimeRange(startTime: TimeStr, endTime: TimeStr): boolean;
/** 获取在 Home Assistant 中设置的地理位置 [纬度, 经度, 海拔] */
export function getGeographicLocation(): [number, number, number];
/** 判断当前实体在 Home Assistant 中的状态是否为 "不可用" */
export function isUnavailableEntity(entityId: string): boolean;
/** 获取指定实体所属的设备对象 */
export function getBelongingDevice(entityId: string): DeviceDef;