homebridge-savanthost
Version:
386 lines • 17.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SavantHostHomebridgePlatform = void 0;
const ssh2_1 = require("ssh2");
const platformAccessory_1 = require("./platformAccessory");
const settings_1 = require("./settings");
const auth_1 = require("./auth");
class SavantHostHomebridgePlatform {
log;
config;
api;
Service;
Characteristic;
accessories = new Map();
discoveredCacheUUIDs = [];
// 修改类型定义
CustomServices = {};
CustomCharacteristics = {};
scenes = new Map();
pollTimer = null;
hubConfig = null;
isActivated = false;
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;
// 初始化为空对象,确保不会出现 undefined
this.CustomServices = {};
this.CustomCharacteristics = {};
// 使用异步 IIFE 来处理动态导入
(async () => {
try {
const { EveHomeKitTypes } = await Promise.resolve().then(() => __importStar(require('homebridge-lib/lib/EveHomeKitTypes.js')));
if (EveHomeKitTypes) {
const eve = new EveHomeKitTypes(this.api);
this.CustomServices = eve.Services;
this.CustomCharacteristics = eve.Characteristics;
this.log.debug('成功加载 EveHomeKitTypes');
}
else {
this.log.warn('EveHomeKitTypes 模块不可用');
}
}
catch (error) {
this.log.error('加载 EveHomeKitTypes 失败:', error);
}
})();
this.log.debug('初始化平台:', this.config.name);
this.api.on('didFinishLaunching', async () => {
this.log.debug('执行 didFinishLaunching 回调');
// 检查插件激活状态
await this.checkActivation();
});
}
// 检查插件激活状态
async checkActivation() {
try {
// 检查是否已激活
this.isActivated = await (0, auth_1.isPluginActivated)(this.log);
if (this.isActivated) {
this.log.info('插件已激活,开始运行...');
this.startPolling();
return;
}
// 获取配置中的授权码
const authCode = this.config.authCode;
if (!authCode) {
// 获取地址码并提示用户
const addressCode = await (0, auth_1.getAddressCode)(this.log);
this.log.warn('插件未激活!请联系开发者获取授权码');
this.log.warn(`您的设备地址码: ${addressCode}`);
this.log.warn('请在插件配置中填入授权码后重启Homebridge');
return;
}
// 尝试激活插件
const activationResult = await (0, auth_1.activatePlugin)(authCode, this.log);
if (activationResult) {
this.isActivated = true;
this.log.info('插件已成功激活,开始运行...');
this.startPolling();
}
else {
const addressCode = await (0, auth_1.getAddressCode)(this.log);
this.log.error('授权码无效,插件无法启动');
this.log.warn(`您的设备地址码: ${addressCode}`);
this.log.warn('请确认授权码正确或联系开发者获取新的授权码');
}
}
catch (error) {
this.log.error('检查授权状态出错:', error);
}
}
async createSSHConnection() {
return new Promise((resolve, reject) => {
const client = new ssh2_1.Client();
client
.on('ready', () => {
this.log.info('SSH 连接已建立');
resolve(client);
})
.on('error', (err) => {
this.log.error('SSH 连接错误:', err);
reject(err);
})
.connect({
host: this.hubConfig.ip,
port: this.hubConfig.port,
username: this.hubConfig.username,
password: this.hubConfig.password,
algorithms: {
serverHostKey: ['ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519'],
},
hostVerifier: () => true,
readyTimeout: 30000, // 连接超时时间30秒
debug: (message) => {
this.log.debug('SSH Debug:', message);
},
});
});
}
async closeSSHConnection(client) {
return new Promise((resolve) => {
client.on('close', () => {
this.log.debug('SSH 连接已关闭');
resolve();
});
client.end();
});
}
startPolling() {
// 如果未激活,不启动
if (!this.isActivated) {
this.log.warn('插件未激活,无法启动轮询');
return;
}
if (!this.config.hubs?.[0]) {
this.log.error('未找到有效的主机配置');
return;
}
this.hubConfig = this.config.hubs[0];
// 确保设置了轮询间隔,如果未设置则使用默认值
if (!this.hubConfig.statePollingInterval) {
this.hubConfig.statePollingInterval = this.config.statePollingInterval || 300;
}
// 验证轮询间隔
if (this.hubConfig.statePollingInterval < 60 || this.hubConfig.statePollingInterval > 3600) {
this.log.warn(`轮询间隔 ${this.hubConfig.statePollingInterval} 超出范围,将使用默认值 300 秒`);
this.hubConfig.statePollingInterval = 300;
}
this.log.info(`使用轮询间隔: ${this.hubConfig.statePollingInterval} 秒`);
// 立即执行第一次查询
this.log.debug('执行首次场景查询');
this.fetchScenes();
// 设置定时轮询
this.pollTimer = setInterval(() => {
this.log.debug(`执行定时场景查询 (间隔: ${this.hubConfig.statePollingInterval} 秒)`);
this.fetchScenes();
}, this.hubConfig.statePollingInterval * 1000);
// 确保定时器不会阻止进程退出
this.pollTimer.unref();
}
getScliPath() {
return this.hubConfig.hostType === 'ProHost'
? '/Users/rpm/Applications/RacePointMedia/sclibridge'
: '/usr/local/bin/sclibridge';
}
async executeCommand(command) {
let client = null;
try {
this.log.debug('建立新的 SSH 连接');
client = await this.createSSHConnection();
const scliPath = this.getScliPath();
const fullCommand = `${scliPath} ${command}`;
this.log.debug('准备执行命令:', fullCommand);
const result = await new Promise((resolve, reject) => {
// 根据主机类型设置不同的环境变量和路径
const setupCommands = this.hubConfig.hostType === 'ProHost'
? [
'export PATH="/Users/rpm/Applications/RacePointMedia:$PATH"',
'cd /Users/rpm/Applications/RacePointMedia',
]
: [
'export PATH="/usr/local/bin:$PATH"',
'export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"',
'cd /usr/local/bin',
];
// 组合所有命令
const wrappedCommand = [...setupCommands, fullCommand].join(' && ');
this.log.debug('完整命令:', wrappedCommand);
client.exec(wrappedCommand, (err, stream) => {
if (err) {
this.log.error('执行命令失败:', err);
reject(err);
return;
}
let output = '';
let errorOutput = '';
stream.on('data', (data) => {
const str = data.toString();
this.log.debug('收到命令输出:', str);
output += str;
});
stream.stderr.on('data', (data) => {
const str = data.toString();
this.log.debug('收到错误输出:', str);
errorOutput += str;
});
stream.on('close', (code) => {
this.log.debug('命令执行完成,退出码:', code);
this.log.debug('清理后的输出:', output.trim());
this.log.debug('错误输出:', errorOutput || '无错误输出');
if (code !== 0) {
this.log.error(`命令执行失败,退出码: ${code}`);
this.log.error(`错误输出: ${errorOutput || '无错误输出'}`);
resolve({ stdout: '', stderr: errorOutput });
return;
}
resolve({ stdout: output.trim(), stderr: errorOutput });
});
});
});
return result;
}
finally {
if (client) {
this.log.debug('命令执行完成,关闭 SSH 连接');
await this.closeSSHConnection(client);
}
}
}
async fetchScenes() {
try {
this.log.debug('尝试获取场景列表');
const command = 'getSceneNames';
this.log.debug('执行场景查询命令:', `${this.getScliPath()} ${command}`);
const { stdout } = await this.executeCommand(command);
this.log.debug('收到原始场景数据:', stdout);
const scenes = this.parseScenes(stdout);
this.log.info(`成功解析 ${scenes.length} 个场景`);
scenes.forEach(scene => {
this.log.debug(`场景信息: 名称=${scene.sceneName}, ID=${scene.sceneId}, 用户=${scene.sceneUser}`);
});
this.updateAccessories(scenes);
return scenes;
}
catch (error) {
this.log.error('执行场景查询命令时出错:', error);
return [];
}
}
parseScenes(output) {
if (!output) {
this.log.warn('没有收到场景数据');
return [];
}
const lines = output.split('\n').filter(line => line.trim());
this.log.debug('场景数据行数:', lines.length);
return lines.map(line => {
this.log.debug('处理场景数据行:', line);
const [sceneName, sceneId, sceneUser] = line.split(',').map(item => item.trim());
if (!sceneName || !sceneId || !sceneUser) {
this.log.warn('无效的场景数据行:', line);
return null;
}
return { sceneName, sceneId, sceneUser };
}).filter((scene) => scene !== null);
}
updateAccessories(scenes) {
this.log.debug('开始更新配件列表');
this.log.debug('当前场景数量:', scenes.length);
// 重置已发现的配件列表
this.discoveredCacheUUIDs = [];
this.scenes.clear(); // 清空现有场景列表
// 创建一个 Map 来跟踪场景 ID 和对应的用户列表
const sceneIdUsers = new Map();
// 首先收集所有相同 ID 的场景的用户
for (const scene of scenes) {
const users = sceneIdUsers.get(scene.sceneId) || new Set();
users.add(scene.sceneUser);
sceneIdUsers.set(scene.sceneId, users);
}
for (const scene of scenes) {
this.log.debug('处理场景:', scene.sceneName);
const uuid = this.api.hap.uuid.generate(scene.sceneId);
this.log.debug('场景UUID:', uuid);
// 获取此场景 ID 的所有用户
const users = sceneIdUsers.get(scene.sceneId);
if (users && users.size > 1) {
this.log.info(`场景 "${scene.sceneName}" (ID: ${scene.sceneId}) 有多个用户: ${Array.from(users).join(', ')}`);
}
// 添加到发现列表
this.discoveredCacheUUIDs.push(uuid);
this.scenes.set(scene.sceneName, scene);
const existingAccessory = this.accessories.get(uuid);
if (existingAccessory) {
this.log.debug('更新现有配件:', scene.sceneName);
// 检查是否需要更新用户
if (existingAccessory.context.scene.sceneUser !== scene.sceneUser) {
this.log.info(`更新场景 "${scene.sceneName}" 的用户从 "${existingAccessory.context.scene.sceneUser}" 到 "${scene.sceneUser}"`);
}
existingAccessory.context.scene = scene;
this.api.updatePlatformAccessories([existingAccessory]);
new platformAccessory_1.SavantHostPlatformAccessory(this, existingAccessory);
}
else {
this.log.debug('创建新配件:', scene.sceneName);
const accessory = new this.api.platformAccessory(scene.sceneName, uuid);
accessory.context.scene = scene;
new platformAccessory_1.SavantHostPlatformAccessory(this, accessory);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.accessories.set(uuid, accessory);
}
}
// 移除不存在的配件
const accessoriesToRemove = [];
for (const [uuid, accessory] of this.accessories) {
if (!this.discoveredCacheUUIDs.includes(uuid)) {
this.log.info('从缓存中移除配件:', accessory.displayName);
accessoriesToRemove.push(accessory);
this.accessories.delete(uuid);
}
}
if (accessoriesToRemove.length > 0) {
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, accessoriesToRemove);
}
this.log.debug('配件更新完成');
this.log.debug('当前配件数量:', this.accessories.size);
this.log.debug('当前场景数量:', this.scenes.size);
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to set up event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.info('从缓存加载配件:', accessory.displayName);
this.accessories.set(accessory.UUID, accessory);
}
async activateScene(sceneName, sceneId, sceneUser) {
try {
const command = `activateScene '${sceneName}' '${sceneId}' '${sceneUser}'`;
this.log.debug('执行场景激活命令:', `${this.getScliPath()} ${command}`);
const { stdout } = await this.executeCommand(command);
this.log.debug('场景激活结果:', stdout);
}
catch (error) {
this.log.error('执行场景激活命令时出错:', error);
}
}
}
exports.SavantHostHomebridgePlatform = SavantHostHomebridgePlatform;
//# sourceMappingURL=platform.js.map