homebridge-savanthost
Version:
377 lines • 15.9 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 (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