码桶
发现社区成员的开源项目
index.js29.4 KB
/**
* 插件:Home Assistant 智能家居
* --------------------------------------------------
* 对接 Home Assistant REST API,通过微信控制智能家居设备。
*
* 核心指令(直接文本触发):
* 设备 / 设备 列表 → 列出所有实体(按域分组)
* 状态 <实体ID> → 查询指定实体状态
* 开灯 / 关灯 → 开关所有灯
* 打开 <设备名> → 打开指定设备
* 关闭 <设备名> → 关闭指定设备
* 传感器 → 列出传感器数据
* 温度 → 列出温度传感器
* 遥控器 [客厅/卧室/...] → 弹出常用设备快捷面板(需配置)
*
* 智能助手(自然语言):
* 「把客厅灯关了」「卧室空调开到26度」「现在家里温度多少」
* 由 smart 插件通过 function calling 自动调用。
*/
const axios = require('axios');
const db = require('../../lib/db');
// ==================== 常量 ====================
const PLUGIN_ID = 'home-assistant';
const HTTP_TIMEOUT = 10000; // HA 请求超时(ms)
const ENTITY_CACHE_TTL = 30000; // 实体列表缓存(30s)
const ICON_MAP = {
light: '[ON] ', switch: '[Switch] ', climate: '[Climate] ', sensor: '[Sensor] ',
binary_sensor: '[Alert] ', cover: '[Cover] ', lock: '[Lock] ', media_player: '[Media] ',
fan: '[Fan] ', vacuum: '[Vacuum] ', camera: '[Camera] ', scene: '[Scene] ',
automation: '[Auto] ', script: '[Script] ', input_boolean: '[Toggle] ',
person: '[Person] ', zone: '[Zone] ', sun: '[Sun] ', weather: '[Weather] ',
button: '[Btn] ', number: '[Num] ', select: '[Sel] ', text: '[Text] ',
device_tracker: '[Tracker] ', update: '[Update] ', alarm_control_panel: '[Alarm] ',
};
// ==================== 实体列表缓存 ====================
let entityCache = { data: null, botId: null, ts: 0 };
function getDomain(entityId) {
return (entityId || '').split('.')[0] || '';
}
function getDomainLabel(domain) {
const labels = {
light: '灯具', switch: '开关', climate: '空调/温控', sensor: '传感器',
binary_sensor: '探测器', cover: '窗帘/卷帘', lock: '门锁',
media_player: '媒体', fan: '风扇', vacuum: '扫地机',
camera: '摄像头', scene: '场景', automation: '自动化',
script: '脚本', person: '人员', zone: '区域',
sun: '太阳', weather: '天气', button: '按钮',
device_tracker: '设备追踪', update: '更新', alarm_control_panel: '安防',
input_boolean: '开关', number: '数值', select: '选择器',
};
return labels[domain] || domain;
}
function getIcon(entityId) {
return ICON_MAP[getDomain(entityId)] || '';
}
function friendlyState(state) {
if (state == null || state === undefined) return '未知';
const s = String(state);
if (s === 'on') return '开';
if (s === 'off') return '关';
if (s === 'unavailable') return '[OFF] 不可用';
if (s === 'unknown') return '未知';
if (s === 'home') return '[HA] 在家';
if (s === 'not_home') return '离家';
if (s === 'locked') return '已锁';
if (s === 'unlocked') return '未锁';
if (s === 'open') return '开';
if (s === 'closed') return '关';
if (s === 'playing') return '播放中';
if (s === 'paused') return '暂停';
if (s === 'idle') return '空闲';
return s;
}
// ==================== 配置 ====================
async function loadConfig(botId) {
const rows = await db.rows(
'SELECT config_key, config_value FROM plugin_settings WHERE bot_id=? AND plugin_id=?',
[botId, PLUGIN_ID]
);
const cfg = {};
for (const r of rows) cfg[r.config_key] = r.config_value;
return cfg;
}
function validateConfig(cfg) {
const missing = [];
if (!cfg.ha_url) missing.push('HA 地址 (ha_url)');
if (!cfg.ha_token) missing.push('访问令牌 (ha_token)');
return { valid: missing.length === 0, missing };
}
// ==================== HA API 客户端 ====================
function createClient(cfg) {
const base = (cfg.ha_url || '').replace(/\/+$/, '');
const headers = {
Authorization: `Bearer ${cfg.ha_token}`,
'Content-Type': 'application/json',
};
const http = axios.create({ baseURL: base, timeout: HTTP_TIMEOUT, headers });
return {
async get(path) {
const res = await http.get(path);
return res.data;
},
async post(path, data) {
const res = await http.post(path, data);
return res.data;
},
};
}
// ==================== 获取实体列表(带缓存) ====================
async function getEntities(cfg, botId, force = false) {
if (!force && entityCache.botId === botId && entityCache.data && (Date.now() - entityCache.ts) < ENTITY_CACHE_TTL) {
return entityCache.data;
}
const ha = createClient(cfg);
const entities = await ha.get('/api/states');
entityCache = { data: entities, botId, ts: Date.now() };
return entities;
}
/** 模糊匹配实体 ID 或 friendly_name */
function findEntity(entities, query) {
const q = query.toLowerCase().trim();
if (!q) return null;
// 精确匹配 entity_id
let match = entities.find(e => e.entity_id.toLowerCase() === q);
if (match) return match;
// entity_id 包含
match = entities.find(e => e.entity_id.toLowerCase().includes(q));
if (match) return match;
// friendly_name 精确匹配
match = entities.find(e => e.attributes?.friendly_name?.toLowerCase() === q);
if (match) return match;
// friendly_name 包含
match = entities.find(e => e.attributes?.friendly_name?.toLowerCase().includes(q));
return match || null;
}
/** 在实体列表中搜索多个 */
function findEntities(entities, query, domain) {
const q = query.toLowerCase().trim();
let list = entities;
if (domain) list = list.filter(e => getDomain(e.entity_id) === domain);
if (!q) return list;
return list.filter(e =>
e.entity_id.toLowerCase().includes(q) ||
(e.attributes?.friendly_name || '').toLowerCase().includes(q)
);
}
// ==================== 构建回复 ====================
function formatEntityLine(e, showDomain = false) {
const icon = getIcon(e.entity_id);
const name = e.attributes?.friendly_name || e.entity_id;
const state = friendlyState(e.state);
const unit = e.attributes?.unit_of_measurement ? ` ${e.attributes.unit_of_measurement}` : '';
const domainTag = showDomain ? ` [${getDomainLabel(getDomain(e.entity_id))}]` : '';
return `${icon} ${name}:${state}${unit}${domainTag}`;
}
// ==================== 指令处理 ====================
async function handleList(cfg, botId, entityName) {
const entities = await getEntities(cfg, botId);
if (!entities || entities.length === 0) return '[OFF] 未获取到任何实体,请检查 HA 连接配置。';
if (entityName) {
// 搜索特定实体或域
const domainOnly = !entityName.includes('.');
let list = domainOnly
? findEntities(entities, entityName)
: [findEntity(entities, entityName)].filter(Boolean);
if (list.length === 0) return `[OFF] 未找到匹配「${entityName}」的设备。`;
if (list.length === 1) {
const e = list[0];
const lines = [formatEntityLine(e, true)];
if (e.attributes) {
const attrs = { ...e.attributes };
delete attrs.friendly_name;
delete attrs.icon;
delete attrs.supported_features;
delete attrs.entity_picture;
const keys = Object.keys(attrs);
if (keys.length > 0) {
lines.push('');
const show = keys.slice(0, 8);
for (const k of show) {
const v = typeof attrs[k] === 'object' ? JSON.stringify(attrs[k]) : attrs[k];
lines.push(` ${k}:${v}`);
}
if (keys.length > 8) lines.push(` … 还有 ${keys.length - 8} 个属性`);
}
}
return lines.join('\n');
}
// 多结果,按域分组
const grouped = {};
for (const e of list) {
const d = getDomain(e.entity_id);
if (!grouped[d]) grouped[d] = [];
grouped[d].push(e);
}
const lines = [`[Search] 搜索「${entityName}」(${list.length} 个结果):`];
for (const [domain, items] of Object.entries(grouped)) {
lines.push(`\n【${getDomainLabel(domain)}】`);
for (const e of items) lines.push(formatEntityLine(e));
}
return lines.join('\n');
}
// 全量列表,按域分组
const grouped = {};
for (const e of entities) {
const d = getDomain(e.entity_id);
if (!grouped[d]) grouped[d] = [];
grouped[d].push(e);
}
const lines = [`[HA] Home Assistant(${entities.length} 个实体):`];
// 常用域排前面
const order = ['light', 'switch', 'climate', 'sensor', 'binary_sensor', 'cover', 'lock', 'media_player', 'fan', 'vacuum'];
const ordered = [...order.filter(d => grouped[d]), ...Object.keys(grouped).filter(d => !order.includes(d)).sort()];
for (const domain of ordered) {
const items = grouped[domain];
if (items.length <= 5) {
for (const e of items) lines.push(formatEntityLine(e));
} else {
lines.push(`\n【${getDomainLabel(domain)}】(${items.length} 个)`);
for (const e of items.slice(0, 5)) lines.push(formatEntityLine(e));
lines.push(` … 还有 ${items.length - 5} 个,发送「设备 ${getDomainLabel(domain)}」查看全部`);
}
}
return lines.join('\n');
}
async function handleTurnOn(cfg, botId, query) {
let entities = await getEntities(cfg, botId);
const ha = createClient(cfg);
// 「灯 全开」→ 全部灯
if (!query || query === '所有灯' || query === '全部灯') {
const lights = entities.filter(e => getDomain(e.entity_id) === 'light');
if (lights.length === 0) return '[OFF] 未找到任何灯具。';
let done = 0;
for (const l of lights) {
try { await ha.post(`/api/services/light/turn_on`, { entity_id: l.entity_id }); done++; }
catch (e) { /* skip individual failures */ }
}
entityCache.ts = 0; // 使缓存失效
return `[ON] 已打开 ${done}/${lights.length} 个灯具`;
}
const match = findEntity(entities, query);
if (!match) return `[OFF] 未找到「${query}」对应的设备。`;
const domain = getDomain(match.entity_id);
try {
await ha.post(`/api/services/${domain}/turn_on`, { entity_id: match.entity_id });
entityCache.ts = 0;
return `[ON] 已打开 ${match.attributes?.friendly_name || match.entity_id}`;
} catch (e) {
if (e.response?.status === 404) return `[OFF] 设备「${match.entity_id}」不支持开启操作。`;
return `[OFF] 操作失败:${e.response?.data?.message || e.message}`;
}
}
async function handleTurnOff(cfg, botId, query) {
let entities = await getEntities(cfg, botId);
const ha = createClient(cfg);
// 「关灯」「灯 全关」
if (!query || query === '所有灯' || query === '全部灯') {
const lights = entities.filter(e => getDomain(e.entity_id) === 'light');
if (lights.length === 0) return '[OFF] 未找到任何灯具。';
let done = 0;
for (const l of lights) {
try { await ha.post(`/api/services/light/turn_off`, { entity_id: l.entity_id }); done++; }
catch (e) { /* skip individual failures */ }
}
entityCache.ts = 0;
return `[ON] 已关闭 ${done}/${lights.length} 个灯具`;
}
const match = findEntity(entities, query);
if (!match) return `[OFF] 未找到「${query}」对应的设备。`;
const domain = getDomain(match.entity_id);
try {
await ha.post(`/api/services/${domain}/turn_off`, { entity_id: match.entity_id });
entityCache.ts = 0;
return `[ON] 已关闭 ${match.attributes?.friendly_name || match.entity_id}`;
} catch (e) {
if (e.response?.status === 404) return `[OFF] 设备「${match.entity_id}」不支持关闭操作。`;
return `[OFF] 操作失败:${e.response?.data?.message || e.message}`;
}
}
async function handleSensor(cfg, botId, query) {
const entities = await getEntities(cfg, botId);
let sensors = entities.filter(e => getDomain(e.entity_id) === 'sensor');
if (query) {
sensors = sensors.filter(e =>
e.entity_id.toLowerCase().includes(query.toLowerCase()) ||
(e.attributes?.friendly_name || '').toLowerCase().includes(query.toLowerCase())
);
}
if (sensors.length === 0) return query ? `[OFF] 未找到匹配「${query}」的传感器` : '[OFF] 没有传感器数据。';
// 排除一些不重要的传感器
const excludeDomains = ['update.', 'sun.', 'zone.', 'person.'];
sensors = sensors.filter(e => !excludeDomains.some(p => e.entity_id.startsWith(p)));
// 优先显示数值类传感器
const lines = [`[Sensor] 传感器数据(${sensors.length} 个):`];
for (const s of sensors.slice(0, 20)) {
lines.push(formatEntityLine(s));
}
if (sensors.length > 20) lines.push(` … 还有 ${sensors.length - 20} 个传感器`);
return lines.join('\n');
}
async function handleTemperature(cfg, botId) {
const entities = await getEntities(cfg, botId);
const tempEntities = entities.filter(e =>
e.attributes?.unit_of_measurement === '°C' || e.attributes?.unit_of_measurement === '°F' ||
e.entity_id.includes('temperature') || e.entity_id.includes('temp')
);
if (tempEntities.length === 0) return '[OFF] 未找到温度传感器。';
const lines = ['[Temp] 温度数据:'];
for (const t of tempEntities) {
lines.push(formatEntityLine(t));
}
return lines.join('\n');
}
async function handleCallService(cfg, botId, domain, service, targetQuery, dataStr) {
const ha = createClient(cfg);
const serviceData = {};
// 如果有 target,查找实体
if (targetQuery) {
const entities = await getEntities(cfg, botId);
const match = findEntity(entities, targetQuery);
if (!match) return `[OFF] 未找到「${targetQuery}」对应的设备。`;
serviceData.entity_id = match.entity_id;
}
// 解析额外参数 {key:val,key2:val2}
if (dataStr) {
try {
const parsed = dataStr.startsWith('{') ? JSON.parse(dataStr) : (() => {
const obj = {};
dataStr.split(',').forEach(pair => {
const [k, v] = pair.split(':').map(s => s.trim());
obj[k] = isNaN(v) ? v : Number(v);
});
return obj;
})();
Object.assign(serviceData, parsed);
} catch (e) {
return `[OFF] 参数格式错误:${e.message}`;
}
}
try {
const result = await ha.post(`/api/services/${domain}/${service}`, serviceData);
entityCache.ts = 0;
const target = serviceData.entity_id ? ` ${serviceData.entity_id}` : '';
return `[ON] 已执行 ${domain}.${service}${target}`;
} catch (e) {
return `[OFF] 调用失败:${e.response?.data?.message || e.message}`;
}
}
// ==================== AI 工具定义(对接 smart 插件) ====================
const aiTools = [
{
type: 'function',
function: {
name: 'ha_list_entities',
description: '列出 Home Assistant 中的所有智能家居实体(设备/传感器等),可按域(light/switch/climate/sensor 等)或关键词过滤。',
parameters: {
type: 'object',
properties: {
domain: { type: 'string', description: '按域过滤,如 light(灯)、switch(开关)、climate(空调)、sensor(传感器)、cover(窗帘)、lock(锁),留空列出全部' },
search: { type: 'string', description: '按名称/ID 模糊搜索,留空不搜索' },
},
},
},
},
{
type: 'function',
function: {
name: 'ha_get_state',
description: '查询 Home Assistant 中指定实体的当前状态和详细属性。',
parameters: {
type: 'object',
properties: {
entity_id: { type: 'string', description: '实体 ID,如 light.ketint_deng、sensor.wen_du' },
},
required: ['entity_id'],
},
},
},
{
type: 'function',
function: {
name: 'ha_turn_on',
description: '打开/开启 Home Assistant 中的指定设备(灯、开关、风扇等)。实体 ID 可通过 ha_list_entities 查询。',
parameters: {
type: 'object',
properties: {
entity_id: { type: 'string', description: '要开启的设备实体 ID' },
brightness: { type: 'number', description: '亮度 0-255(仅灯具支持),可选' },
temperature: { type: 'number', description: '目标温度(仅温控支持),可选' },
},
required: ['entity_id'],
},
},
},
{
type: 'function',
function: {
name: 'ha_turn_off',
description: '关闭 Home Assistant 中的指定设备(灯、开关、风扇等)。',
parameters: {
type: 'object',
properties: {
entity_id: { type: 'string', description: '要关闭的设备实体 ID' },
},
required: ['entity_id'],
},
},
},
{
type: 'function',
function: {
name: 'ha_call_service',
description: '调用 Home Assistant 的任意服务(如设置空调模式、调节颜色等)。domain 和 service 由 ha_list_entities 结果确定。',
parameters: {
type: 'object',
properties: {
domain: { type: 'string', description: '服务域,如 light、climate、cover、media_player' },
service: { type: 'string', description: '服务名,如 turn_on、turn_off、set_temperature、set_hvac_mode' },
entity_id: { type: 'string', description: '目标实体 ID' },
data: { type: 'object', description: '额外参数,如 {"brightness":128,"rgb_color":[255,0,0]}' },
},
required: ['domain', 'service'],
},
},
},
];
// ==================== AI 工具处理入口 ====================
async function handleAiTool(name, args, ctx) {
const botId = ctx.bot?.id || ctx.bot;
if (typeof botId !== 'number') return '错误:无法获取机器人 ID';
const cfg = await loadConfig(botId);
const { valid, missing } = validateConfig(cfg);
if (!valid) return `Home Assistant 未配置完成,缺少:${missing.join('、')}。请在后台设置中填写。`;
try {
switch (name) {
case 'ha_list_entities': {
let entities = await getEntities(cfg, botId);
if (args.domain) entities = entities.filter(e => getDomain(e.entity_id) === args.domain);
if (args.search) {
const s = args.search.toLowerCase();
entities = entities.filter(e => e.entity_id.includes(s) || (e.attributes?.friendly_name || '').toLowerCase().includes(s));
}
if (entities.length === 0) return '未找到任何实体。';
return entities.slice(0, 30).map(e => ({
entity_id: e.entity_id,
name: e.attributes?.friendly_name || e.entity_id,
state: e.state,
domain: getDomain(e.entity_id),
}));
}
case 'ha_get_state': {
let entities = await getEntities(cfg, botId);
const match = findEntity(entities, args.entity_id);
if (!match) return `未找到实体「${args.entity_id}」`;
return {
entity_id: match.entity_id,
name: match.attributes?.friendly_name || match.entity_id,
state: match.state,
domain: getDomain(match.entity_id),
attributes: match.attributes,
last_changed: match.last_changed,
last_updated: match.last_updated,
};
}
case 'ha_turn_on': {
const ha = createClient(cfg);
let entities = await getEntities(cfg, botId);
const match = findEntity(entities, args.entity_id);
if (!match) return `未找到实体「${args.entity_id}」`;
const domain = getDomain(match.entity_id);
const data = { entity_id: match.entity_id };
if (args.brightness !== undefined) data.brightness = args.brightness;
if (args.temperature !== undefined) data.temperature = args.temperature;
await ha.post(`/api/services/${domain}/turn_on`, data);
entityCache.ts = 0;
return `已打开 ${match.attributes?.friendly_name || match.entity_id}`;
}
case 'ha_turn_off': {
const ha = createClient(cfg);
let entities = await getEntities(cfg, botId);
const match = findEntity(entities, args.entity_id);
if (!match) return `未找到实体「${args.entity_id}」`;
const domain = getDomain(match.entity_id);
await ha.post(`/api/services/${domain}/turn_off`, { entity_id: match.entity_id });
entityCache.ts = 0;
return `已关闭 ${match.attributes?.friendly_name || match.entity_id}`;
}
case 'ha_call_service': {
const ha = createClient(cfg);
const data = { ...(args.data || {}) };
if (args.entity_id) data.entity_id = args.entity_id;
await ha.post(`/api/services/${args.domain}/${args.service}`, data);
entityCache.ts = 0;
return `已执行 ${args.domain}.${args.service}${args.entity_id ? ' → ' + args.entity_id : ''}`;
}
default:
return `未知 HA 工具:${name}`;
}
} catch (e) {
return `HA 操作失败:${e.response?.data?.message || e.message}`;
}
}
// ==================== 指令前缀 ====================
const PREFIX_RE = /^(设备|状态|传感器|温度|执行|遥控器|ha|hass|homeassistant|智能家居)\s*/i;
// ==================== 命令匹配(无前缀直接触发) ====================
/** 「打开/开/开启 XXX」或「XXX 打开/开」(排除太短的,避免误触) */
const TURN_ON_RE = /^(?:打开|开|开启)\s*(.+)$/i;
/** 「关闭/关/关掉 XXX」(排除太短的) */
const TURN_OFF_RE = /^(?:关闭|关|关掉)\s*(.+)$/i;
/** 「开灯」/「关灯」单字词 */
const LIGHT_ON_RE = /^开灯$/;
const LIGHT_OFF_RE = /^关灯$/;
/** 「灯 全开」「灯全开」「全开灯」等 */
const LIGHT_ALL_ON_RE = /^(?:灯|灯具)?\s*全[开啟]/;
const LIGHT_ALL_OFF_RE = /^(?:灯|灯具)?\s*全(?:关|闭)/;
// ==================== onMessage ====================
async function onMessage(msg, ctx) {
const text = (msg.content || '').trim();
if (!text) return false;
// ---- 第一步:匹配前缀指令 ----
const prefixMatch = text.match(PREFIX_RE);
let matched = false;
let cmd = '';
let prefix = '';
if (prefixMatch) {
cmd = text.slice(prefixMatch[0].length).trim();
prefix = prefixMatch[1].toLowerCase();
matched = true;
}
// ---- 第二步:匹配无前缀直接指令 ----
const turnOnMatch = text.match(TURN_ON_RE);
const turnOffMatch = text.match(TURN_OFF_RE);
const isLightOn = LIGHT_ON_RE.test(text);
const isLightOff = LIGHT_OFF_RE.test(text);
const isAllOn = LIGHT_ALL_ON_RE.test(text);
const isAllOff = LIGHT_ALL_OFF_RE.test(text);
const isDirectCmd = turnOnMatch || turnOffMatch || isLightOn || isLightOff || isAllOn || isAllOff;
if (!matched && !isDirectCmd) return false;
// 确定 botId
const botId = typeof ctx.bot === 'object' ? ctx.bot.id : ctx.bot;
if (!botId) {
await ctx.sendText('[OFF] 无法识别当前机器人。');
return true;
}
// 加载配置
const cfg = await loadConfig(botId);
const { valid, missing } = validateConfig(cfg);
if (!valid) {
await ctx.sendText(`[OFF] Home Assistant 未配置完成\n缺少:${missing.join('、')}\n\n请在后台 → 插件设置 → Home Assistant 中填写 HA 地址和访问令牌。`);
return true;
}
try {
let result = '';
// ========== 前缀指令 ==========
if (matched) {
// ---- 设备 / 列表 ----
if (prefix === '设备' || prefix === 'ha' || prefix === 'hass' || prefix === 'homeassistant' || prefix === '智能家居') {
if (!cmd || cmd === '列表') {
result = await handleList(cfg, botId, null);
} else if (cmd.startsWith('执行 ')) {
const parts = cmd.replace(/^执行\s+/, '').match(/^(\S+)\s+(\S+)(?:\s+(\S+))?(?:\s*(.+))?$/);
if (!parts) {
result = '[OFF] 格式:设备 执行 <domain> <service> [entity] [参数]\n示例:设备 执行 climate set_temperature 客厅空调 temperature:26';
} else {
result = await handleCallService(cfg, botId, parts[1], parts[2], parts[3] || null, parts[4] || null);
}
} else {
result = await handleList(cfg, botId, cmd);
}
}
// ---- 状态 <entity_id> ----
else if (prefix === '状态') {
if (!cmd) { result = '[OFF] 格式:状态 <实体ID>\n示例:状态 light.ketint_deng'; }
else result = await handleList(cfg, botId, cmd);
}
// ---- 传感器 ----
else if (prefix === '传感器') {
result = await handleSensor(cfg, botId, cmd || null);
}
// ---- 温度 ----
else if (prefix === '温度') {
result = await handleTemperature(cfg, botId);
}
// ---- 遥控器 ----
else if (prefix === '遥控器') {
result = '遥控器功能开发中,请直接发送「打开/关闭 <设备名>」控制设备。';
}
// ---- 执行 ----
else if (prefix === '执行') {
const parts = cmd.match(/^(\S+)\s+(\S+)(?:\s+(\S+))?(?:\s*(.+))?$/);
if (!parts) {
result = '[OFF] 格式:执行 <domain> <service> [entity] [参数]\n示例:执行 climate set_temperature 客厅空调 temperature:26';
} else {
result = await handleCallService(cfg, botId, parts[1], parts[2], parts[3] || null, parts[4] || null);
}
}
}
// ========== 无前缀直接指令 ==========
if (!result && isDirectCmd) {
if (isLightOn) {
result = await handleTurnOn(cfg, botId, null);
} else if (isLightOff) {
result = await handleTurnOff(cfg, botId, null);
} else if (isAllOn) {
result = await handleTurnOn(cfg, botId, null);
} else if (isAllOff) {
result = await handleTurnOff(cfg, botId, null);
} else if (turnOnMatch) {
const name = (turnOnMatch[1] || turnOnMatch[2] || '').trim();
if (name.length < 2) {
result = '[OFF] 设备名太短,请写明要控制的设备\n示例:打开 客厅灯';
} else {
result = await handleTurnOn(cfg, botId, name);
}
} else if (turnOffMatch) {
const name = (turnOffMatch[1] || turnOffMatch[2] || '').trim();
if (name.length < 2) {
result = '[OFF] 设备名太短,请写明要控制的设备\n示例:关闭 客厅灯';
} else {
result = await handleTurnOff(cfg, botId, name);
}
}
}
if (result) {
await ctx.sendText(result);
return true;
}
return false;
} catch (e) {
// 连接错误友好提示
if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND' || e.code === 'ETIMEDOUT') {
await ctx.sendText(`[OFF] 无法连接到 Home Assistant(${cfg.ha_url})\n请检查 HA 服务是否运行正常。`);
return true;
}
if (e.response?.status === 401) {
await ctx.sendText('[OFF] HA 认证失败,请检查访问令牌是否正确。');
return true;
}
await ctx.sendText(`[OFF] Home Assistant 错误:${e.message}`);
return true;
}
}
// ==================== 导出 ====================
module.exports = {
meta: {
id: 'home-assistant',
name: 'Home Assistant 智能家居',
version: '1.0.0',
author: '奶狗',
category: '工具',
description: '对接 Home Assistant 智能家居平台,通过微信控制灯光、空调、窗帘等设备,查询传感器数据。支持文本指令和 AI 自然语言对话。',
entry: 'home-assistant/index.js',
commandPrefix: ['设备', '状态', '传感器', '温度', '执行', '遥控器', '打开', '关闭', '开灯', '关灯', '开启'],
configurable: true,
settingsSchema: [
{
key: 'ha_url',
label: 'Home Assistant 地址',
type: 'text',
placeholder: 'http://192.168.1.100:8123',
help: 'Home Assistant 的访问地址(局域网 IP:端口 或域名),不要带末尾斜杠',
},
{
key: 'ha_token',
label: '长期访问令牌',
type: 'password',
placeholder: 'eyJhbGciOi...',
help: '在 HA 个人资料 → 长期访问令牌 中创建。请保管好,此令牌拥有完整权限。',
},
],
usage: `【Home Assistant 智能家居控制】
基础指令(需带前缀):
设备 → 列出所有设备(按类型分组)
设备 <名称> → 搜索特定设备
状态 <实体ID> → 查看设备详细状态
传感器 → 查看所有传感器数据
温度 → 查看温度传感器
[ON] 开关控制(直接说):
开灯 / 关灯 → 开关所有灯具
打开 <设备名> → 打开指定设备
关闭 <设备名> → 关闭指定设备
[Error] 高级:
执行 <domain> <service> [entity] [参数]
示例:执行 climate set_temperature 客厅空调 temperature:26
智能助手(自然语言,无需加前缀):
「把客厅灯关了」「卧室空调开到 26 度」
「现在家里温度多少」「有没有窗没关」`,
},
aiTools,
handleAiTool,
onMessage,
};