UNPKG

@homebridge-plugins/homebridge-plugin-update-check

Version:
421 lines (420 loc) 19.4 kB
/* eslint-disable style/operator-linebreak */ /* eslint-disable object-shorthand */ /* eslint-disable perfectionist/sort-imports */ /* eslint-disable antfu/if-newline */ import fs from 'node:fs'; import { hostname } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Cron } from 'croner'; // eslint-disable-next-line ts/consistent-type-imports import { UiApi } from './ui-api.js'; // ESM equivalent of __dirname const __filename = fileURLToPath(import.meta.url); // eslint-disable-next-line unused-imports/no-unused-vars const __dirname = path.dirname(__filename); let hap; let Accessory; const PLUGIN_NAME = '@homebridge-plugins/homebridge-plugin-update-check'; const PLATFORM_NAME = 'PluginUpdate'; class PluginUpdatePlatform { log; api; config; uiApi; isDocker; sensorInfo; checkHB; checkHBUI; checkPlugins; checkDocker; initialCheckDelay; autoUpdateHB; autoUpdateHBUI; autoUpdatePlugins; allowDirectNpmUpdates; autoRestartAfterUpdates; respectDisabledPlugins; service; cronJob; firstDailyRun = true; hbUpdates = []; hbUIUpdates = []; pluginUpdates = []; dockerUpdates = []; constructor(log, config, api) { hap = api.hap; Accessory = api.platformAccessory; this.log = log; this.config = config; this.api = api; this.uiApi = new UiApi(this.api.user.storagePath(), this.log); this.isDocker = fs.existsSync('/homebridge/package.json'); this.sensorInfo = this.getSensorInfo(this.config.sensorType); this.checkHB = this.config.checkHomebridgeUpdates ?? false; this.checkHBUI = this.config.checkHomebridgeUIUpdates ?? false; this.checkPlugins = this.config.checkPluginUpdates ?? false; this.checkDocker = this.config.checkDockerUpdates ?? false; this.initialCheckDelay = this.config.initialCheckDelay ?? 10; this.autoUpdateHB = this.config.autoUpdateHomebridge ?? false; this.autoUpdateHBUI = this.config.autoUpdateHomebridgeUI ?? false; this.autoUpdatePlugins = this.config.autoUpdatePlugins ?? false; this.allowDirectNpmUpdates = this.config.allowDirectNpmUpdates ?? false; this.autoRestartAfterUpdates = this.config.autoRestartAfterUpdates ?? false; this.respectDisabledPlugins = this.config.respectDisabledPlugins ?? true; api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, this.addUpdateAccessory.bind(this)); } addUpdateAccessory() { if (!this.service) { const uuid = hap.uuid.generate(PLATFORM_NAME); const newAccessory = new Accessory('Plugin Update Check', uuid); newAccessory.addService(this.sensorInfo.serviceType); this.configureAccessory(newAccessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [newAccessory]); } setTimeout(() => { this.doCheck(); this.firstDailyRun = false; }, this.initialCheckDelay * 1000); const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; this.setupFirstDailyRunResetCron(timezone); this.setupUpdatesCron(timezone); } setupFirstDailyRunResetCron(timezone) { const cronScheduleAtMidnight = '0 0 * * *'; this.cronJob = new Cron(cronScheduleAtMidnight, { name: `First Daily Run Reset Cron Job`, timezone: timezone, }, async () => { this.firstDailyRun = true; this.log.debug(`Reset "firstDailyRun" to ${this.firstDailyRun}`); }); } setupUpdatesCron(timezone) { const cronScheduleFiveAfterTheHour = '5 * * * *'; this.cronJob = new Cron(cronScheduleFiveAfterTheHour, { name: `Updates Available Cron Job`, timezone: timezone, }, async () => { this.log.debug(`Is first daily run: ${this.firstDailyRun}`); this.doCheck(); this.firstDailyRun = false; this.log.debug(`Cleared "firstDailyRun" to ${this.firstDailyRun}`); }); } async checkUi() { this.log.debug('Searching for available updates ...'); let logLevel = (this.firstDailyRun === true) ? "info" /* LogLevel.INFO */ : "debug" /* LogLevel.DEBUG */; const updatesAvailable = []; // Get ignored plugins from API if respectDisabledPlugins is enabled let ignoredPlugins = []; if (this.respectDisabledPlugins) { try { ignoredPlugins = await this.uiApi.getIgnoredPlugins(); this.log.debug(`Retrieved ${ignoredPlugins.length} ignored plugin(s) from homebridge-config-ui-x: ${ignoredPlugins.join(', ')}`); } catch (error) { this.log.warn(`Failed to retrieve ignored plugins list, filtering disabled: ${error}`); ignoredPlugins = []; } } else { this.log.debug('respectDisabledPlugins is disabled, skipping plugin filtering'); } if (this.checkHB) { const homebridge = await this.uiApi.getHomebridge(); if (homebridge.updateAvailable) { // Check if homebridge core updates are ignored const isIgnored = this.respectDisabledPlugins && ignoredPlugins.includes('homebridge'); if (!isIgnored) { updatesAvailable.push(homebridge); const version = homebridge.latestVersion; if (this.hbUpdates.length === 0 || !this.hbUpdates.includes(version)) logLevel = "info" /* LogLevel.INFO */; this.log.log(logLevel, `Homebridge update available: ${version}`); this.hbUpdates = [version]; } else { this.log.debug(`Ignoring Homebridge core update: ${homebridge.latestVersion} (update notifications disabled in homebridge-config-ui-x)`); } } } if (this.checkHBUI || this.checkPlugins) { const plugins = await this.uiApi.getPlugins(); if (this.checkHBUI) { const homebridgeUiPlugins = plugins.filter(plugin => plugin.name === 'homebridge-config-ui-x'); // Only one plugin is returned homebridgeUiPlugins.forEach((homebridgeUI) => { if (homebridgeUI.updateAvailable) { // Check if homebridge-config-ui-x updates are ignored const isIgnored = this.respectDisabledPlugins && ignoredPlugins.includes('homebridge-config-ui-x'); if (!isIgnored) { updatesAvailable.push(homebridgeUI); const version = homebridgeUI.latestVersion; if (this.hbUIUpdates.length === 0 || !this.hbUIUpdates.includes(version)) logLevel = "info" /* LogLevel.INFO */; this.log.log(logLevel, `Homebridge UI update available: ${version}`); this.hbUIUpdates = [version]; } else { this.log.debug(`Ignoring Homebridge UI update: ${homebridgeUI.latestVersion} (update notifications disabled in homebridge-config-ui-x)`); } } }); } if (this.checkPlugins) { this.log.debug(`Checking ${plugins.length} plugins for updates (respectDisabledPlugins: ${this.respectDisabledPlugins})`); const filteredPlugins = plugins.filter((plugin) => { // Always exclude homebridge-config-ui-x if (plugin.name === 'homebridge-config-ui-x') { return false; } // If respectDisabledPlugins is enabled, check API ignored list if (this.respectDisabledPlugins) { if (ignoredPlugins.includes(plugin.name)) { this.log.debug(`Filtering out plugin ${plugin.name} (ignored in homebridge-config-ui-x)`); return false; } } return true; }); this.log.debug(`After filtering: ${filteredPlugins.length} plugins to check for updates`); filteredPlugins.forEach((plugin) => { if (plugin.updateAvailable) { updatesAvailable.push(plugin); const version = plugin.latestVersion; if (this.pluginUpdates.length === 0 || !this.pluginUpdates.includes(version)) logLevel = "info" /* LogLevel.INFO */; this.log.log(logLevel, `Homebridge plugin update available: ${plugin.name} ${plugin.latestVersion}`); this.pluginUpdates.push(version); } }); // Log ignored plugins if any updates are available for them (only when respectDisabledPlugins is enabled) if (this.respectDisabledPlugins) { const ignoredWithUpdates = plugins.filter(plugin => plugin.name !== 'homebridge-config-ui-x' && plugin.updateAvailable && ignoredPlugins.includes(plugin.name)); if (ignoredWithUpdates.length > 0) { this.log.info(`Ignoring updates for ${ignoredWithUpdates.length} plugin(s): ${ignoredWithUpdates.map(p => p.name).join(', ')}`); } } } } if (this.isDocker && this.checkDocker) { const docker = await this.uiApi.getDocker(); if (docker.updateAvailable) { updatesAvailable.push(docker); const version = docker.latestVersion; if (this.dockerUpdates.length === 0 || !this.dockerUpdates.includes(version)) logLevel = "info" /* LogLevel.INFO */; this.log.log(logLevel, `Docker update available: ${version}`); this.dockerUpdates = [version]; } } this.log.log(logLevel, `Found ${updatesAvailable.length} available update(s)`); // Provide additional diagnostic information in debug mode if (this.respectDisabledPlugins && ignoredPlugins.length > 0) { this.log.debug(`Filtering enabled with ${ignoredPlugins.length} ignored plugins: ${ignoredPlugins.join(', ')}`); } else if (this.respectDisabledPlugins) { this.log.debug('Filtering enabled but no ignored plugins found'); } else { this.log.debug('Plugin filtering is disabled (respectDisabledPlugins: false)'); } return updatesAvailable.length; } doCheck() { this.checkUi() .then((updates) => { this.service?.setCharacteristic(this.sensorInfo.characteristicType, updates ? this.sensorInfo.trippedValue : this.sensorInfo.untrippedValue); }) .catch((ex) => { this.log.error(ex); }) .finally(() => { this.log.debug('Check complete'); }); } checkService(accessory, serviceType) { const service = accessory.getService(serviceType); if (this.sensorInfo.serviceType === serviceType) { if (service) { this.service = service; } else { this.service = accessory.addService(serviceType); } return true; } else { if (service) { accessory.removeService(service); } return false; } } configureAccessory(accessory) { accessory.on("identify" /* PlatformAccessoryEvent.IDENTIFY */, () => { this.log(`${accessory.displayName} identify requested!`); }); const accInfo = accessory.getService(hap.Service.AccessoryInformation); if (accInfo) { accInfo .setCharacteristic(hap.Characteristic.Manufacturer, 'Homebridge') .setCharacteristic(hap.Characteristic.Model, 'Plugin Update Check') .setCharacteristic(hap.Characteristic.SerialNumber, hostname()); } this.checkService(accessory, hap.Service.MotionSensor); this.checkService(accessory, hap.Service.ContactSensor); this.checkService(accessory, hap.Service.OccupancySensor); this.checkService(accessory, hap.Service.SmokeSensor); this.checkService(accessory, hap.Service.LeakSensor); this.checkService(accessory, hap.Service.LightSensor); this.checkService(accessory, hap.Service.HumiditySensor); this.checkService(accessory, hap.Service.CarbonMonoxideSensor); this.checkService(accessory, hap.Service.CarbonDioxideSensor); this.checkService(accessory, hap.Service.AirQualitySensor); /* const motionService = accessory.getService(hap.Service.MotionSensor); const contactService = accessory.getService(hap.Service.ContactSensor); const occupancyService = accessory.getService(hap.Service.OccupancySensor); const smokeService = accessory.getService(hap.Service.SmokeSensor); const leakService = accessory.getService(hap.Service.LeakSensor); const lightService = accessory.getService(hap.Service.LightSensor); const humidityService = accessory.getService(hap.Service.HumiditySensor); const monoxideService = accessory.getService(hap.Service.CarbonMonoxideSensor); const dioxideService = accessory.getService(hap.Service.CarbonDioxideSensor); const airService = accessory.getService(hap.Service.AirQualitySensor); if (this.sensorInfo.serviceType == hap.Service.MotionSensor) { this.service = motionService; } else if (motionService) { accessory.removeService(motionService); } if (this.sensorInfo.serviceType == hap.Service.ContactSensor) { this.service = contactService; } else if (contactService) { accessory.removeService(contactService); } if (this.sensorInfo.serviceType == hap.Service.OccupancySensor) { this.service = occupancyService; } else if (occupancyService) { accessory.removeService(occupancyService); } if (this.sensorInfo.serviceType == hap.Service.SmokeSensor) { this.service = smokeService; } else if (smokeService) { accessory.removeService(smokeService); } if (this.sensorInfo.serviceType == hap.Service.LeakSensor) { this.service = leakService; } else if (leakService) { accessory.removeService(leakService); } if (this.sensorInfo.serviceType == hap.Service.LightSensor) { this.service = lightService; } else if (lightService) { accessory.removeService(lightService); } if (this.sensorInfo.serviceType == hap.Service.HumiditySensor) { this.service = humidityService; } else if (humidityService) { accessory.removeService(humidityService); } if (this.sensorInfo.serviceType == hap.Service.CarbonMonoxideSensor) { this.service = monoxideService; } else if (monoxideService) { accessory.removeService(monoxideService); } if (this.sensorInfo.serviceType == hap.Service.CarbonDioxideSensor) { this.service = dioxideService; } else if (dioxideService) { accessory.removeService(dioxideService); } if (this.sensorInfo.serviceType == hap.Service.AirQualitySensor) { this.service = airService; } else if (airService) { accessory.removeService(airService); } */ this.service?.setCharacteristic(this.sensorInfo.characteristicType, this.sensorInfo.untrippedValue); } getSensorInfo(sensorType) { switch (sensorType?.toLowerCase()) { case 'contact': return { serviceType: hap.Service.ContactSensor, characteristicType: hap.Characteristic.ContactSensorState, untrippedValue: 0, trippedValue: 1, }; case 'occupancy': return { serviceType: hap.Service.OccupancySensor, characteristicType: hap.Characteristic.OccupancyDetected, untrippedValue: 0, trippedValue: 1, }; case 'smoke': return { serviceType: hap.Service.SmokeSensor, characteristicType: hap.Characteristic.SmokeDetected, untrippedValue: 0, trippedValue: 1, }; case 'leak': return { serviceType: hap.Service.LeakSensor, characteristicType: hap.Characteristic.LeakDetected, untrippedValue: 0, trippedValue: 1, }; case 'light': return { serviceType: hap.Service.LightSensor, characteristicType: hap.Characteristic.CurrentAmbientLightLevel, untrippedValue: 0.0001, trippedValue: 100000, }; case 'humidity': return { serviceType: hap.Service.HumiditySensor, characteristicType: hap.Characteristic.CurrentRelativeHumidity, untrippedValue: 0, trippedValue: 100, }; case 'monoxide': return { serviceType: hap.Service.CarbonMonoxideSensor, characteristicType: hap.Characteristic.CarbonMonoxideDetected, untrippedValue: 0, trippedValue: 1, }; case 'dioxide': return { serviceType: hap.Service.CarbonDioxideSensor, characteristicType: hap.Characteristic.CarbonDioxideDetected, untrippedValue: 0, trippedValue: 1, }; case 'air': return { serviceType: hap.Service.AirQualitySensor, characteristicType: hap.Characteristic.AirQuality, untrippedValue: 1, trippedValue: 5, }; case 'motion': default: return { serviceType: hap.Service.MotionSensor, characteristicType: hap.Characteristic.MotionDetected, untrippedValue: false, trippedValue: true, }; } } } // Register our platform with homebridge. export default (api) => { api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, PluginUpdatePlatform); }; //# sourceMappingURL=index.js.map