UNPKG

homebridge-savanthost

Version:
377 lines 15.9 kB
"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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SavantHostHomebridgePlatform = void 0; const axios_1 = __importDefault(require("axios")); const bonjour_service_1 = require("bonjour-service"); const https_1 = __importDefault(require("https")); const net_1 = __importDefault(require("net")); const os_1 = __importDefault(require("os")); 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; isActivated = false; bonjour; savantHost = null; constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.Service = api.hap.Service; this.Characteristic = api.hap.Characteristic; this.bonjour = new bonjour_service_1.Bonjour(); // 初始化为空对象,确保不会出现 undefined this.CustomServices = {}; this.CustomCharacteristics = {}; // 使用异步 IIFE 来处理动态导入 (async () => { try { const { EveHomeKitTypes } = await Promise.resolve().then(() => __importStar(require('homebridge-lib/EveHomeKitTypes'))); 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); } } startPolling() { // 如果未激活,不启动 if (!this.isActivated) { this.log.warn('插件未激活,无法启动轮询'); return; } // 确保设置了轮询间隔 let interval = this.config.statePollingInterval || 300; // 验证轮询间隔 if (interval < 60 || interval > 3600) { this.log.warn(`轮询间隔 ${interval} 超出范围,将使用默认值 300 秒`); interval = 300; } this.log.info(`使用轮询间隔: ${interval} 秒`); // 启动发现和同步 this.discoverAndSync(); // 设置定时轮询 this.pollTimer = setInterval(() => { this.log.debug(`执行定时场景查询 (间隔: ${interval} 秒)`); this.fetchScenes(); }, interval * 1000); // 确保定时器不会阻止进程退出 this.pollTimer.unref(); } async discoverAndSync() { if (!this.isActivated) { return; } await this.discoverHost(); if (this.savantHost) { await this.fetchScenes(); } } async discoverHost() { if (!this.isActivated) { return; } this.log.info('正在搜索 Savant 主机 (OpenAPI)...'); return new Promise((resolve) => { let resolved = false; // Bonjour 搜索 const browser = this.bonjour.find({ type: 'soapi_sdo', protocol: 'tcp' }); // 设置发现超时 const timeout = setTimeout(() => { if (!resolved) { this.log.warn('搜索主机超时,未找到 Savant 主机。将在下一次轮询时重试。'); browser.stop(); resolve(); } }, 5000); // Bonjour 服务发现处理 browser.on('up', (service) => { this.log.debug(`发现服务: ${service.name} (${service.type}) IP:${service.addresses} Port:${service.port}`); if (service.addresses && service.addresses.length > 0 && !resolved) { // 优先使用 IPv4 const ip = service.addresses.find((addr) => addr.includes('.')) || service.addresses[0]; this.log.info(`找到主机: ${service.name} (${ip}:${service.port})`); this.savantHost = { ip: ip, port: service.port, hostname: service.host || service.name, }; resolved = true; clearTimeout(timeout); browser.stop(); resolve(); } }); // 并行执行 3060 端口扫描 this.scanForPort3060().then((foundIp) => { if (foundIp && !resolved) { this.log.info(`通过 3060 端口找到 Savant 主机: ${foundIp}:3060`); this.savantHost = { ip: foundIp, port: 3060, hostname: `Savant-Host-${foundIp.replace(/\./g, '-')}`, }; resolved = true; clearTimeout(timeout); browser.stop(); resolve(); } }); }); } // 扫描局域网内开放 3060 端口的设备 async scanForPort3060() { try { // 获取本地网络接口 const interfaces = os_1.default.networkInterfaces(); const localIps = []; // 收集所有本地 IPv4 地址 for (const iface of Object.values(interfaces)) { for (const addr of iface || []) { if (addr.family === 'IPv4' && !addr.internal) { localIps.push(addr.address); } } } this.log.debug(`本地 IP 地址: ${localIps.join(', ')}`); // 生成要扫描的 IP 范围 const ipRanges = []; for (const ip of localIps) { const parts = ip.split('.'); if (parts.length === 4) { const networkPrefix = `${parts[0]}.${parts[1]}.${parts[2]}.`; ipRanges.push(networkPrefix); } } // 去重 IP 范围 const uniqueIpRanges = [...new Set(ipRanges)]; this.log.debug(`扫描 IP 范围: ${uniqueIpRanges.join(', ')}`); // 扫描每个 IP 范围 for (const networkPrefix of uniqueIpRanges) { // 扫描 1-254 网段 for (let i = 1; i <= 254; i++) { const ip = `${networkPrefix}${i}`; // 测试 3060 端口 const isOpen = await this.isPortOpen(ip, 3060, 500); if (isOpen) { return ip; } } } return null; } catch (error) { this.log.error('3060 端口扫描错误:', error); return null; } } // 检查端口是否开放 async isPortOpen(ip, port, timeout) { return new Promise((resolve) => { const socket = new net_1.default.Socket(); socket.setTimeout(timeout); socket.on('connect', () => { socket.destroy(); resolve(true); }); socket.on('timeout', () => { socket.destroy(); resolve(false); }); socket.on('error', () => { socket.destroy(); resolve(false); }); socket.connect(port, ip); }); } async fetchScenes() { // 如果没有主机信息,尝试重新发现 if (!this.savantHost) { await this.discoverHost(); } // 如果仍然没有主机信息,返回空 if (!this.savantHost) { this.log.error('无法获取场景:未连接到主机 (请检查主机是否在线以及是否开启了 OpenAPI)'); return []; } try { const url = `http://${this.savantHost.ip}:${this.savantHost.port}/config/v1/scenes`; this.log.debug('获取场景:', url); const response = await axios_1.default.get(url, { timeout: 5000, httpsAgent: new https_1.default.Agent({ rejectUnauthorized: false }), }); const data = response.data; if (!Array.isArray(data)) { this.log.warn('场景数据格式错误: 期望数组'); return []; } // 映射并过滤场景 const scenes = data.map((s) => ({ sceneName: s.alias || s.name || 'Unknown', sceneId: s.id, })).filter(s => s.sceneId); // 确保有 ID this.log.info(`成功获取 ${scenes.length} 个场景`); // 只有成功获取到场景列表(即使为空列表,只要是正常的空)才更新配件 // 这样可以避免因连接错误导致配件被错误删除 this.updateAccessories(scenes); return scenes; } catch (error) { this.log.error('获取场景失败:', error instanceof Error ? error.message : String(error)); // 如果发生错误,可能是 IP 变了或服务不可用 // 清除当前主机信息,以便下次轮询时重新发现 this.log.info('清除当前主机缓存,将在下次轮询时重新搜索主机...'); this.savantHost = null; return []; } } updateAccessories(scenes) { const activeSceneIds = new Set(); for (const scene of scenes) { // 使用 UUID 库生成基于 sceneId 的 UUID const uuid = this.api.hap.uuid.generate(scene.sceneId); activeSceneIds.add(uuid); const existingAccessory = this.accessories.get(uuid); if (existingAccessory) { this.log.debug('恢复现有配件:', existingAccessory.displayName); // 更新场景名称 existingAccessory.context.scene = scene; // 确保缓存的配件也更新 this.api.updatePlatformAccessories([existingAccessory]); new platformAccessory_1.SavantHostPlatformAccessory(this, existingAccessory); } else { this.log.info('添加新配件:', 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); } } // 移除已删除的场景 for (const [uuid, accessory] of this.accessories) { if (!activeSceneIds.has(uuid)) { this.log.info('移除已删除的配件:', accessory.displayName); this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); this.accessories.delete(uuid); } } } async activateScene(sceneName, sceneId) { if (!this.savantHost) { this.log.error('无法激活场景:未连接到主机'); return; } try { const url = `http://${this.savantHost.ip}:${this.savantHost.port}/control/v1/scenes/${sceneId}/apply`; this.log.info(`正在激活场景: ${sceneName} (${sceneId})`); await axios_1.default.post(url, {}, { timeout: 5000, httpsAgent: new https_1.default.Agent({ rejectUnauthorized: false }), }); this.log.info(`场景激活成功: ${sceneName}`); } catch (error) { this.log.error('激活场景失败:', error instanceof Error ? error.message : String(error)); } } configureAccessory(accessory) { this.log.info('加载缓存的配件:', accessory.displayName); this.accessories.set(accessory.UUID, accessory); } } exports.SavantHostHomebridgePlatform = SavantHostHomebridgePlatform; //# sourceMappingURL=platform.js.map