├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── FUNDING.yml ├── dependabot.yml ├── workflows │ ├── stale.yml │ ├── beta-release.yml │ └── release.yml └── copilot-instructions.md ├── .DS_Store ├── docs ├── assets │ ├── hierarchy.js │ ├── navigation.js │ ├── search.js │ ├── highlight.css │ ├── icons.svg │ ├── icons.js │ └── main.js ├── .nojekyll ├── hierarchy.html ├── variables │ └── default.html ├── modules.html └── index.html ├── src ├── index.test.ts ├── configTypes.ts ├── homebridge-ui │ ├── server.ts │ └── public │ │ └── index.html ├── configTypes.test.ts ├── schema.test.ts ├── ignore-plugins.test.ts ├── failureSensor.ts ├── ui-api.ts └── index.ts ├── vitest.config.ts ├── typedoc.json ├── tsconfig.json ├── LICENSE ├── eslint.config.js ├── package.json ├── README.md ├── config.schema.json └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge-plugins/homebridge-plugin-update-check/HEAD/.DS_Store -------------------------------------------------------------------------------- /docs/assets/hierarchy.js: -------------------------------------------------------------------------------- 1 | window.hierarchyData = "eJyrVirKzy8pVrKKjtVRKkpNy0lNLsnMzytWsqqurQUAmx4Kpg==" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Sunoo 2 | ko_fi: sunookitsune 3 | liberapay: Sunoo 4 | custom: ["https://paypal.me/sunoo"] 5 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "eJyLrlYqSa0oUbJSSklNSyzNKVHSUSpILMlQslIqSyzKTEzKSS3Wh0rpZZTk5ijpKGVn5qUoWRkb1cYCAKAYFR0=" -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | describe('Plugin Configuration', () => { 4 | it('should be able to import without errors', () => { 5 | // Basic test to ensure the module can be imported 6 | expect(true).toBe(true) 7 | }) 8 | }) -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 0 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '45 11 * * *' 7 | 8 | jobs: 9 | stale: 10 | uses: homebridge/.github/.github/workflows/stale.yml@latest 11 | secrets: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "eJxNj81qxDAMhN9lziIbsodt9QZ9gV6MKW6spaaOvdhJWjB+9+L8sL2J0UjzTUGKPxmsCr5dsODrQAhmEjCs3M3iZxCW5MFYTXLm00u+HJvua548CKM3OUsGA1UTXLDyCy5YJWUXAxhDd+1eQbg78bbF7RmEMU6ThJZh47hsoz5s7zLOMTXz7r70INXT0L3cblqTOm83fRPOF4eyoaySZrFvO5JSz1IFHwdofzYu6MGl1icWl/qPrO3a14d7iHdBwErX+gdRUmn6"; -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['src/**/*.{test,spec}.ts'], 6 | exclude: [ 7 | 'dist/**', 8 | 'node_modules/**', 9 | '**/coverage/**', 10 | '.git/**', 11 | 'docs/**', 12 | ], 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "out": "docs", 3 | "exclude": ["src/**/*.spec.ts"], 4 | "entryPoints": [ 5 | "src/index.ts" 6 | ], 7 | "excludePrivate": true, 8 | "excludeProtected": true, 9 | "excludeExternals": true, 10 | "hideGenerator": true, 11 | "includeVersion": false, 12 | "validation": { 13 | "invalidLink": true, 14 | "notExported": false 15 | }, 16 | "inlineTags": ["@link", "@see"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": [ 5 | "DOM", 6 | "ES2022" 7 | ], 8 | "rootDir": "src", 9 | "module": "ES2022", 10 | "moduleResolution": "bundler", 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "outDir": "dist", 16 | "sourceMap": true, 17 | "allowSyntheticDefaultImports": true, 18 | "esModuleInterop": true, 19 | "forceConsistentCasingInFileNames": true 20 | }, 21 | "include": [ 22 | "src" 23 | ], 24 | "exclude": [ 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/configTypes.ts: -------------------------------------------------------------------------------- 1 | import type { PlatformIdentifier, PlatformName } from 'homebridge' 2 | 3 | export interface PluginUpdatePlatformConfig { 4 | platform: PlatformName | PlatformIdentifier 5 | sensorType?: string 6 | checkHomebridgeUpdates?: boolean 7 | checkHomebridgeUIUpdates?: boolean 8 | checkPluginUpdates?: boolean 9 | checkDockerUpdates?: boolean 10 | initialCheckDelay?: number 11 | autoUpdateHomebridge?: boolean 12 | autoUpdateHomebridgeUI?: boolean 13 | autoUpdatePlugins?: boolean 14 | allowDirectNpmUpdates?: boolean 15 | autoRestartAfterUpdates?: boolean 16 | failureSensorType?: string 17 | respectDisabledPlugins?: boolean 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021-2024, David Maher 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #A31515; 3 | --dark-hl-0: #CE9178; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #0451A5; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-code-background: #FFFFFF; 11 | --dark-code-background: #1E1E1E; 12 | } 13 | 14 | @media (prefers-color-scheme: light) { :root { 15 | --hl-0: var(--light-hl-0); 16 | --hl-1: var(--light-hl-1); 17 | --hl-2: var(--light-hl-2); 18 | --hl-3: var(--light-hl-3); 19 | --code-background: var(--light-code-background); 20 | } } 21 | 22 | @media (prefers-color-scheme: dark) { :root { 23 | --hl-0: var(--dark-hl-0); 24 | --hl-1: var(--dark-hl-1); 25 | --hl-2: var(--dark-hl-2); 26 | --hl-3: var(--dark-hl-3); 27 | --code-background: var(--dark-code-background); 28 | } } 29 | 30 | :root[data-theme='light'] { 31 | --hl-0: var(--light-hl-0); 32 | --hl-1: var(--light-hl-1); 33 | --hl-2: var(--light-hl-2); 34 | --hl-3: var(--light-hl-3); 35 | --code-background: var(--light-code-background); 36 | } 37 | 38 | :root[data-theme='dark'] { 39 | --hl-0: var(--dark-hl-0); 40 | --hl-1: var(--dark-hl-1); 41 | --hl-2: var(--dark-hl-2); 42 | --hl-3: var(--dark-hl-3); 43 | --code-background: var(--dark-code-background); 44 | } 45 | 46 | .hl-0 { color: var(--hl-0); } 47 | .hl-1 { color: var(--hl-1); } 48 | .hl-2 { color: var(--hl-2); } 49 | .hl-3 { color: var(--hl-3); } 50 | pre, code { background: var(--code-background); } 51 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | ignores: ['dist', 'docs'], 6 | jsx: false, 7 | typescript: true, 8 | formatters: { 9 | markdown: true, 10 | }, 11 | rules: { 12 | 'curly': ['error', 'multi-line'], 13 | 'import/order': 0, 14 | 'jsdoc/check-alignment': 'error', 15 | 'jsdoc/check-line-alignment': 'error', 16 | 'perfectionist/sort-exports': 'error', 17 | 'perfectionist/sort-imports': [ 18 | 'error', 19 | { 20 | groups: [ 21 | 'builtin-type', 22 | 'external-type', 23 | 'internal-type', 24 | ['parent-type', 'sibling-type', 'index-type'], 25 | 'builtin', 26 | 'external', 27 | 'internal', 28 | ['parent', 'sibling', 'index'], 29 | 'object', 30 | 'unknown', 31 | ], 32 | order: 'asc', 33 | type: 'natural', 34 | }, 35 | ], 36 | 'perfectionist/sort-named-exports': 'error', 37 | 'perfectionist/sort-named-imports': 'error', 38 | 'sort-imports': 0, 39 | 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 40 | 'style/quote-props': ['error', 'consistent-as-needed'], 41 | 'test/no-only-tests': 'error', 42 | 'unicorn/no-useless-spread': 'error', 43 | 'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }], 44 | 'no-new': 0, // Disable the no-new rule 45 | 'new-cap': 0, // Disable the new-cap rule 46 | 'no-undef': 0, // Disable the no-undef rule 47 | }, 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /src/homebridge-ui/server.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | 3 | /* Copyright(C) 2021-2024, David Maher (https://github.com/sunoo). All rights reserved. 4 | * 5 | * server.ts: homebridge-plugin-update-check. 6 | */ 7 | import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils' 8 | 9 | class PluginUiServer extends HomebridgePluginUiServer { 10 | constructor() { 11 | super() 12 | /* 13 | A native method getCachedAccessories() was introduced in config-ui-x v4.37.0 14 | The following is for users who have a lower version of config-ui-x 15 | */ 16 | this.onRequest('getCachedAccessories', () => { 17 | try { 18 | const plugin = '@homebridge-plugins/homebridge-plugin-update-check' 19 | const devicesToReturn = [] 20 | 21 | // The path and file of the cached accessories 22 | const accFile = `${this.homebridgeStoragePath}/accessories/cachedAccessories` 23 | 24 | // Check the file exists 25 | if (fs.existsSync(accFile)) { 26 | // read the cached accessories file 27 | const cachedAccessories: any[] = JSON.parse(fs.readFileSync(accFile, 'utf8')) 28 | 29 | cachedAccessories.forEach((accessory: any) => { 30 | // Check the accessory is from this plugin 31 | if (accessory.plugin === plugin) { 32 | // Add the cached accessory to the array 33 | devicesToReturn.push(accessory.accessory as never) 34 | } 35 | }) 36 | } 37 | // Return the array 38 | return devicesToReturn 39 | } catch { 40 | // Just return an empty accessory list in case of any errors 41 | return [] 42 | } 43 | }) 44 | this.ready() 45 | } 46 | } 47 | 48 | function startPluginUiServer(): PluginUiServer { 49 | return new PluginUiServer() 50 | } 51 | 52 | startPluginUiServer() 53 | -------------------------------------------------------------------------------- /.github/workflows/beta-release.yml: -------------------------------------------------------------------------------- 1 | name: Beta Release 2 | 3 | on: 4 | push: 5 | branches: [beta-*.*.*, beta] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build_and_test: 10 | uses: homebridge/.github/.github/workflows/nodejs-build-and-test.yml@latest 11 | with: 12 | enable_coverage: false 13 | secrets: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | lint: 16 | needs: build_and_test 17 | uses: homebridge/.github/.github/workflows/eslint.yml@latest 18 | 19 | publish: 20 | needs: lint 21 | if: ${{ github.repository == 'homebridge-plugins/homebridge-plugin-update-check' }} 22 | permissions: 23 | id-token: write 24 | uses: homebridge/.github/.github/workflows/npm-publish-esm.yml@latest 25 | with: 26 | tag: 'beta' 27 | dynamically_adjust_version: true 28 | npm_version_command: 'pre' 29 | pre_id: 'beta' 30 | secrets: 31 | npm_auth_token: ${{ secrets.npm_token }} 32 | 33 | pre-release: 34 | needs: publish 35 | if: ${{ github.repository == 'homebridge-plugins/homebridge-plugin-update-check' }} 36 | uses: homebridge/.github/.github/workflows/pre-release.yml@latest 37 | with: 38 | npm_version: ${{ needs.publish.outputs.NPM_VERSION }} 39 | body: | 40 | **Beta Release** 41 | **Version**: v${{ needs.publish.outputs.NPM_VERSION }} 42 | [How To Test Beta Releases](https://github.com/homebridge-plugins/homebridge-plugin-update-check/wiki/Beta-Version) 43 | 44 | github-releases-to-discord: 45 | name: Discord Webhooks 46 | needs: [build_and_test,publish] 47 | if: ${{ github.repository == 'homebridge-plugins/homebridge-plugin-update-check' }} 48 | uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest 49 | with: 50 | title: "Plugin Update Check Beta Release" 51 | description: | 52 | Version `v${{ needs.publish.outputs.NPM_VERSION }}` 53 | url: "https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}" 54 | secrets: 55 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_BETA || secrets.DISCORD_WEBHOOK_URL_LATEST }} -------------------------------------------------------------------------------- /src/configTypes.test.ts: -------------------------------------------------------------------------------- 1 | import type { PluginUpdatePlatformConfig } from './configTypes.js' 2 | 3 | import { describe, expect, it } from 'vitest' 4 | 5 | describe('pluginUpdatePlatformConfig', () => { 6 | it('should allow valid platform name and identifier', () => { 7 | const config: PluginUpdatePlatformConfig = { 8 | platform: 'ExamplePlatform', 9 | } 10 | expect(config.platform).toBe('ExamplePlatform') 11 | }) 12 | 13 | it('should allow optional sensorType property', () => { 14 | const config: PluginUpdatePlatformConfig = { 15 | platform: 'ExamplePlatform', 16 | sensorType: 'temperature', 17 | } 18 | expect(config.sensorType).toBe('temperature') 19 | }) 20 | 21 | it('should allow optional initialCheckDelay property', () => { 22 | const config: PluginUpdatePlatformConfig = { 23 | platform: 'ExamplePlatform', 24 | initialCheckDelay: 30, 25 | } 26 | expect(config.initialCheckDelay).toBe(30) 27 | }) 28 | 29 | it('should allow all properties to be set', () => { 30 | const config: PluginUpdatePlatformConfig = { 31 | platform: 'ExamplePlatform', 32 | sensorType: 'humidity', 33 | checkHomebridgeUpdates: true, 34 | checkHomebridgeUIUpdates: true, 35 | checkPluginUpdates: true, 36 | checkDockerUpdates: true, 37 | initialCheckDelay: 15, 38 | autoUpdateHomebridge: true, 39 | autoUpdateHomebridgeUI: false, 40 | autoUpdatePlugins: true, 41 | allowDirectNpmUpdates: false, 42 | } 43 | expect(config.platform).toBe('ExamplePlatform') 44 | expect(config.sensorType).toBe('humidity') 45 | expect(config.checkHomebridgeUpdates).toBe(true) 46 | expect(config.checkHomebridgeUIUpdates).toBe(true) 47 | expect(config.checkPluginUpdates).toBe(true) 48 | expect(config.checkDockerUpdates).toBe(true) 49 | expect(config.initialCheckDelay).toBe(15) 50 | expect(config.autoUpdateHomebridge).toBe(true) 51 | expect(config.autoUpdateHomebridgeUI).toBe(false) 52 | expect(config.autoUpdatePlugins).toBe(true) 53 | expect(config.allowDirectNpmUpdates).toBe(false) 54 | }) 55 | 56 | it('should allow auto-update properties to be set independently', () => { 57 | const config: PluginUpdatePlatformConfig = { 58 | platform: 'ExamplePlatform', 59 | autoUpdateHomebridge: true, 60 | autoUpdateHomebridgeUI: false, 61 | autoUpdatePlugins: true, 62 | } 63 | expect(config.autoUpdateHomebridge).toBe(true) 64 | expect(config.autoUpdateHomebridgeUI).toBe(false) 65 | expect(config.autoUpdatePlugins).toBe(true) 66 | }) 67 | 68 | it('should allow allowDirectNpmUpdates property to be set', () => { 69 | const config: PluginUpdatePlatformConfig = { 70 | platform: 'ExamplePlatform', 71 | allowDirectNpmUpdates: true, 72 | } 73 | expect(config.allowDirectNpmUpdates).toBe(true) 74 | }) 75 | 76 | it('should allow autoRestartAfterUpdates property to be set', () => { 77 | const config: PluginUpdatePlatformConfig = { 78 | platform: 'ExamplePlatform', 79 | autoRestartAfterUpdates: true, 80 | } 81 | expect(config.autoRestartAfterUpdates).toBe(true) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Unified Release 2 | 3 | on: 4 | push: 5 | branches: 6 | #- "alpha-*" 7 | #- "beta-*" 8 | - latest 9 | workflow_dispatch: 10 | 11 | jobs: 12 | # 1️⃣ Determine release type, ESM status, and branch name 13 | determine-release-type: 14 | uses: homebridge/.github/.github/workflows/determine-release-type.yml@latest 15 | with: 16 | ref_name: ${{ github.ref_name }} 17 | 18 | # 2️⃣ Update version and changelog using the scripts 19 | update-version: 20 | needs: determine-release-type 21 | uses: homebridge/.github/.github/workflows/update-version.yml@latest 22 | with: 23 | release_type: ${{ needs.determine-release-type.outputs.release_type }} 24 | is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }} 25 | secrets: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | 28 | # 3️⃣ Publish to NPM and create GitHub release 29 | publish-release: 30 | needs: [determine-release-type, update-version] 31 | permissions: 32 | id-token: write 33 | contents: write 34 | uses: homebridge/.github/.github/workflows/publish-release.yml@latest 35 | with: 36 | release_type: ${{ needs.determine-release-type.outputs.release_type }} 37 | version: ${{ needs.update-version.outputs.version }} 38 | is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }} 39 | secrets: 40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | 42 | # 4️⃣ Promote branch if this is a prerelease (alpha/beta) 43 | promote-branch: 44 | needs: [determine-release-type, publish-release] 45 | if: ${{ needs.determine-release-type.outputs.release_type != 'latest' && needs.determine-release-type.outputs.release_type != 'skip' }} 46 | uses: homebridge/.github/.github/workflows/promote-branch.yml@latest 47 | with: 48 | branch_name: ${{ needs.determine-release-type.outputs.branch_name }} 49 | release_type: ${{ needs.determine-release-type.outputs.release_type }} 50 | is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }} 51 | secrets: 52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | 54 | # 5️⃣ Notify if any previous job fails 55 | workflow-failure: 56 | if: ${{ failure() }} 57 | needs: [determine-release-type, update-version, publish-release, promote-branch] 58 | uses: homebridge/.github/.github/workflows/report-failure.yml@latest 59 | with: 60 | workflow_name: ${{ github.workflow }} 61 | job_name: ${{ github.job }} 62 | run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 63 | 64 | # 6️⃣ Post to Discord 65 | github-releases-to-discord: 66 | name: Discord Webhooks 67 | needs: [determine-release-type, update-version, publish-release] 68 | uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest 69 | with: 70 | title: "Plugin Update Check Release" 71 | description: | 72 | Version `v${{ needs.update-version.outputs.version }}` 73 | url: "https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v${{ needs.update-version.outputs.version }}" 74 | secrets: 75 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@homebridge-plugins/homebridge-plugin-update-check", 3 | "displayName": "Homebridge Plugin Update Check", 4 | "type": "module", 5 | "version": "2.3.7", 6 | "description": "A Homebridge plugin for checking for updates to Homebridge and plugins", 7 | "author": "David Maher", 8 | "license": "BSD-2-Clause", 9 | "funding": [ 10 | { 11 | "type": "kofi", 12 | "url": "https://ko-fi.com/sunookitsune" 13 | }, 14 | { 15 | "type": "paypal", 16 | "url": "https://paypal.me/sunoo" 17 | }, 18 | { 19 | "type": "github", 20 | "url": "https://github.com/Sunoo" 21 | }, 22 | { 23 | "type": "liberapay", 24 | "url": "https://liberapay.com/Sunoo" 25 | } 26 | ], 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "homepage": "https://github.com/homebridge-plugins/homebridge-plugin-update-check#readme", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/homebridge-plugins/homebridge-plugin-update-check.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/homebridge-plugins/homebridge-plugin-update-check/issues" 37 | }, 38 | "keywords": [ 39 | "homebridge-plugin", 40 | "plugin", 41 | "update", 42 | "updates", 43 | "upgrade", 44 | "upgrades", 45 | "version", 46 | "versions" 47 | ], 48 | "main": "dist/index.js", 49 | "files": [ 50 | "LICENSE", 51 | "README.md", 52 | "config.schema.json", 53 | "dist/**/*", 54 | "package.json" 55 | ], 56 | "engines": { 57 | "homebridge": "^1.8.5 || ^2.0.0 || ^2.0.0-beta.29 || ^2.0.0-alpha.38", 58 | "node": "^20 || ^22 || ^24" 59 | }, 60 | "scripts": { 61 | "check": "npm install && npm outdated", 62 | "lint": "eslint src/**/*.ts", 63 | "lint:fix": "eslint src/**/*.ts --fix", 64 | "watch": "npm run build && npm run plugin-ui && npm link && nodemon", 65 | "plugin-ui": "rsync ./src/homebridge-ui/public/index.html ./dist/homebridge-ui/public/", 66 | "build": "npm run clean && tsc && npm run plugin-ui", 67 | "prepublishOnly": "npm run lint && npm run build && npm run plugin-ui && npm run docs && npm run docs:lint", 68 | "postpublish": "npm run clean && npm ci", 69 | "clean": "shx rm -rf ./dist", 70 | "test": "vitest run", 71 | "test:watch": "vitest watch", 72 | "test-coverage": "npm run test -- --coverage", 73 | "docs": "typedoc", 74 | "docs:lint": "typedoc --emit none --treatWarningsAsErrors" 75 | }, 76 | "dependencies": { 77 | "@homebridge/plugin-ui-utils": "^2.1.0", 78 | "axios": "^1.13.1", 79 | "axios-retry": "^4.5.0", 80 | "cacheable-lookup": "^7.0.0", 81 | "croner": "^9.1.0", 82 | "jsonwebtoken": "^9.0.2" 83 | }, 84 | "devDependencies": { 85 | "@antfu/eslint-config": "^6.2.0", 86 | "@types/aes-js": "^3.1.4", 87 | "@types/debug": "^4.1.12", 88 | "@types/fs-extra": "^11.0.4", 89 | "@types/mdast": "^4.0.4", 90 | "@types/node": "^24.9.2", 91 | "@types/semver": "^7.7.1", 92 | "@types/source-map-support": "^0.5.10", 93 | "@vitest/coverage-v8": "^4.0.6", 94 | "ajv": "^8.17.1", 95 | "eslint": "^9.39.0", 96 | "eslint-plugin-format": "^1.0.2", 97 | "eslint-plugin-import": "^2.32.0", 98 | "homebridge": "^1.11.1", 99 | "homebridge-config-ui-x": "^5.8.0", 100 | "nodemon": "^3.1.10", 101 | "shx": "^0.4.0", 102 | "ts-node": "^10.9.2", 103 | "typedoc": "^0.28.14", 104 | "typescript": "^5.9.3", 105 | "vitest": "^4.0.6" 106 | } 107 | } -------------------------------------------------------------------------------- /docs/hierarchy.html: -------------------------------------------------------------------------------- 1 | @homebridge-plugins/homebridge-plugin-update-check
@homebridge-plugins/homebridge-plugin-update-check
    Preparing search index...

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

    Hierarchy Summary

    2 | -------------------------------------------------------------------------------- /src/schema.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import Ajv from 'ajv' 6 | import { beforeAll, describe, expect, it } from 'vitest' 7 | 8 | // ESM equivalent of __dirname 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = path.dirname(__filename) 11 | 12 | describe('config schema validation', () => { 13 | let schema: any 14 | let ajv: Ajv 15 | 16 | beforeAll(() => { 17 | // Load the config schema 18 | const schemaPath = path.resolve(__dirname, '../config.schema.json') 19 | const schemaContent = fs.readFileSync(schemaPath, 'utf8') 20 | const fullSchema = JSON.parse(schemaContent) 21 | schema = fullSchema.schema 22 | 23 | ajv = new Ajv() 24 | }) 25 | 26 | describe('name field validation', () => { 27 | it('should accept valid names with letters, numbers, spaces, hyphens, and underscores', () => { 28 | const validNames = [ 29 | 'Plugin Update', 30 | 'PluginUpdate V2', 31 | 'Plugin-Update', 32 | 'Plugin_Update', 33 | 'Update123', 34 | 'My Plugin Name', 35 | 'Plugin-Update_V2 Test', 36 | ] 37 | 38 | validNames.forEach((name) => { 39 | const config = { 40 | name, 41 | sensorType: 'motion', 42 | platform: 'PluginUpdate', 43 | } 44 | 45 | const validate = ajv.compile(schema) 46 | const valid = validate(config) 47 | 48 | expect(valid, `Expected "${name}" to be valid, but got errors: ${JSON.stringify(validate.errors)}`).toBe(true) 49 | }) 50 | }) 51 | 52 | it('should reject names with periods and other invalid characters', () => { 53 | const invalidNames = [ 54 | 'Plugin Update V2.0', // Period - the reported issue 55 | 'Plugin.Update', // Period 56 | 'Plugin@Update', // @ symbol 57 | 'Plugin#Update', // # symbol 58 | 'Plugin$Update', // $ symbol 59 | 'Plugin%Update', // % symbol 60 | 'Plugin&Update', // & symbol 61 | 'Plugin*Update', // * symbol 62 | 'Plugin+Update', // + symbol 63 | 'Plugin=Update', // = symbol 64 | 'Plugin/Update', // / slash 65 | 'Plugin\\Update', // \ backslash 66 | 'Plugin|Update', // | pipe 67 | 'Plugin', // < > brackets 68 | 'Plugin[Update]', // [ ] square brackets 69 | 'Plugin{Update}', // { } curly brackets 70 | 'Plugin(Update)', // ( ) parentheses 71 | ] 72 | 73 | invalidNames.forEach((name) => { 74 | const config = { 75 | name, 76 | sensorType: 'motion', 77 | platform: 'PluginUpdate', 78 | } 79 | 80 | const validate = ajv.compile(schema) 81 | const valid = validate(config) 82 | 83 | expect(valid, `Expected "${name}" to be invalid, but validation passed`).toBe(false) 84 | }) 85 | }) 86 | 87 | it('should reject empty names', () => { 88 | const config = { 89 | name: '', 90 | sensorType: 'motion', 91 | platform: 'PluginUpdate', 92 | } 93 | 94 | const validate = ajv.compile(schema) 95 | const valid = validate(config) 96 | 97 | expect(valid).toBe(false) 98 | }) 99 | 100 | it('should reject names that are too long', () => { 101 | const longName = 'A'.repeat(65) // 65 characters, exceeds maxLength of 64 102 | const config = { 103 | name: longName, 104 | sensorType: 'motion', 105 | platform: 'PluginUpdate', 106 | } 107 | 108 | const validate = ajv.compile(schema) 109 | const valid = validate(config) 110 | 111 | expect(valid).toBe(false) 112 | }) 113 | 114 | it('should accept names at the maximum length', () => { 115 | const maxLengthName = 'A'.repeat(64) // Exactly 64 characters 116 | const config = { 117 | name: maxLengthName, 118 | sensorType: 'motion', 119 | platform: 'PluginUpdate', 120 | } 121 | 122 | const validate = ajv.compile(schema) 123 | const valid = validate(config) 124 | 125 | expect(valid).toBe(true) 126 | }) 127 | }) 128 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-plugin-update-check 2 | 3 | [![npm](https://img.shields.io/npm/v/@homebridge-plugins/homebridge-plugin-update-check) ![npm](https://img.shields.io/npm/dt/@homebridge-plugins/homebridge-plugin-update-check)](https://www.npmjs.com/package/@homebridge-plugins/homebridge-plugin-update-check) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 4 | 5 | A [Homebridge](https://github.com/nfarina/homebridge) plugin for checking for updates to Homebridge and plugins. 6 | 7 | ## Installation 8 | 9 | 1. Install Homebridge using the [official instructions](https://github.com/homebridge/homebridge/wiki). 10 | 2. Install this plugin using: `sudo npm install -g homebridge-plugin-update-check`. 11 | 3. Update your configuration file. See sample config.json snippet below. 12 | 13 | ### Configuration 14 | 15 | Configuration sample: 16 | 17 | ```json 18 | "platforms": [ 19 | { 20 | "name": "PluginUpdate", 21 | "sensorType": "contact", 22 | "checkHomebridgeUpdates": false, 23 | "checkHomebridgeUIUpdates": false, 24 | "checkPluginUpdates": true, 25 | "checkDockerUpdates": true, 26 | "autoUpdateHomebridge": false, 27 | "autoUpdateHomebridgeUI": false, 28 | "autoUpdatePlugins": false, 29 | "allowDirectNpmUpdates": false, 30 | "autoRestartAfterUpdates": false, 31 | "failureSensorType": "motion", 32 | "platform": "PluginUpdate" 33 | } 34 | ] 35 | ``` 36 | 37 | #### Fields 38 | 39 | * "platform": Must always be "PluginUpdate" (required) 40 | * "sensorType": What type of sensor will be exposed to HomeKit. Can be `motion`, `contact`, `occupancy`, `humidity`, `light`, `air`, `leak`, `smoke`, `dioxide`, or `monoxide` (Default: `motion`) 41 | * "checkHomebridgeUpdates": Check if an update is available for the Homebridge server 42 | * "checkHomebridgeUIUpdates: Check if an update is available for the Homebridge UI 43 | * "checkPluginUpdates": Check if updates are available for any installed plugins 44 | * "checkDockerUpdates": If running in Docker, check if newer Docker versions are available. If not running in Docker, does nothing 45 | * "autoUpdateHomebridge": Automatically install Homebridge updates when available (Default: `false`) 46 | * "autoUpdateHomebridgeUI": Automatically install Homebridge Config UI updates when available (Default: `false`) 47 | * "autoUpdatePlugins": Automatically install plugin updates when available (Default: `false`) 48 | * "allowDirectNpmUpdates": Allow automatic updates using direct npm commands even when homebridge-config-ui-x is not available (Default: `false`) 49 | * "autoRestartAfterUpdates": Automatically restart Homebridge after successful automatic updates to apply changes (Default: `false`) 50 | * "failureSensorType": What type of sensor will be used for update/restart failure notifications. Can be `motion`, `contact`, `occupancy`, `humidity`, `light`, `air`, `leak`, `smoke`, `dioxide`, or `monoxide` (Default: `motion`, only shown when auto-updates are enabled) 51 | 52 | Homebridge, Homebridge UI, plugin, and Docker updates can be selected independently. This allows you for example, to ignore available Homebridge, Homebridge UI available updates if you are running Homebridge in a Docker container and wish to only update these components when a new Docker image is available. 53 | 54 | **Note on Automatic Updates:** When automatic updates are enabled, the plugin will: 55 | 1. **Automatically create a backup** before performing any updates (when homebridge-config-ui-x is available) 56 | 2. Attempt to install updates via npm commands. This requires: 57 | - homebridge-config-ui-x to be installed and properly configured (unless `allowDirectNpmUpdates` is enabled) 58 | - Sufficient privileges to install global npm packages 59 | 3. **Automatically restart Homebridge** after successful updates (when `autoRestartAfterUpdates` is enabled) 60 | 4. **Monitor for failures** and provide notifications via a configurable failure sensor (type set by `failureSensorType`) 61 | 62 | Automatic updates are disabled by default for safety. Enable them only if you trust automatic updates. The plugin will automatically create a backup before any updates are performed when homebridge-config-ui-x is available, eliminating the need for manual backup procedures. However, you should still maintain your own backup procedures as a best practice. 63 | 64 | **Note on Docker Updates:** Docker container updates are intentionally not supported via automatic updates for safety reasons. This is because the process performing the update would be running inside the container itself, which would result in the process killing itself halfway through the update, potentially corrupting the container or leaving it in an unusable state. 65 | 66 | When `allowDirectNpmUpdates` is enabled, automatic updates will work even when homebridge-config-ui-x is not available by using direct npm commands. This provides more flexibility but requires ensuring you have the necessary npm privileges. 67 | -------------------------------------------------------------------------------- /src/ignore-plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | // Test for the plugin filtering logic 4 | describe('Plugin Update Filtering', () => { 5 | it('should filter plugins correctly based on ignore list', () => { 6 | // Mock plugin data similar to what would come from the API 7 | const mockPlugins = [ 8 | { 9 | name: 'homebridge-plugin-1', 10 | installedVersion: '1.0.0', 11 | latestVersion: '1.1.0', 12 | updateAvailable: true, 13 | }, 14 | { 15 | name: 'homebridge-plugin-2', 16 | installedVersion: '2.0.0', 17 | latestVersion: '2.1.0', 18 | updateAvailable: true, 19 | }, 20 | { 21 | name: 'homebridge-config-ui-x', 22 | installedVersion: '4.0.0', 23 | latestVersion: '4.1.0', 24 | updateAvailable: true, 25 | }, 26 | { 27 | name: 'homebridge-plugin-3', 28 | installedVersion: '3.0.0', 29 | latestVersion: '3.1.0', 30 | updateAvailable: true, 31 | }, 32 | ] 33 | 34 | const ignoredPlugins = ['homebridge-plugin-2', 'homebridge-plugin-3'] 35 | 36 | // Test the filtering logic that would be used in checkUi() 37 | const filteredPlugins = mockPlugins.filter(plugin => 38 | plugin.name !== 'homebridge-config-ui-x' && 39 | !ignoredPlugins.includes(plugin.name) 40 | ) 41 | 42 | expect(filteredPlugins).toHaveLength(1) 43 | expect(filteredPlugins[0].name).toBe('homebridge-plugin-1') 44 | }) 45 | 46 | it('should include all plugins when ignore list is empty', () => { 47 | const mockPlugins = [ 48 | { 49 | name: 'homebridge-plugin-1', 50 | installedVersion: '1.0.0', 51 | latestVersion: '1.1.0', 52 | updateAvailable: true, 53 | }, 54 | { 55 | name: 'homebridge-plugin-2', 56 | installedVersion: '2.0.0', 57 | latestVersion: '2.1.0', 58 | updateAvailable: true, 59 | }, 60 | ] 61 | 62 | const ignoredPlugins: string[] = [] 63 | 64 | const filteredPlugins = mockPlugins.filter(plugin => 65 | plugin.name !== 'homebridge-config-ui-x' && 66 | !ignoredPlugins.includes(plugin.name) 67 | ) 68 | 69 | expect(filteredPlugins).toHaveLength(2) 70 | expect(filteredPlugins.map(p => p.name)).toEqual(['homebridge-plugin-1', 'homebridge-plugin-2']) 71 | }) 72 | 73 | it('should correctly identify ignored plugins with updates', () => { 74 | const mockPlugins = [ 75 | { 76 | name: 'homebridge-plugin-1', 77 | installedVersion: '1.0.0', 78 | latestVersion: '1.1.0', 79 | updateAvailable: true, 80 | }, 81 | { 82 | name: 'homebridge-plugin-ignored', 83 | installedVersion: '2.0.0', 84 | latestVersion: '2.1.0', 85 | updateAvailable: true, 86 | }, 87 | { 88 | name: 'homebridge-plugin-no-update', 89 | installedVersion: '3.0.0', 90 | latestVersion: '3.0.0', 91 | updateAvailable: false, 92 | }, 93 | ] 94 | 95 | const ignoredPlugins = ['homebridge-plugin-ignored'] 96 | 97 | // Test identifying ignored plugins with available updates 98 | const ignoredWithUpdates = mockPlugins.filter(plugin => 99 | ignoredPlugins.includes(plugin.name) && plugin.updateAvailable 100 | ) 101 | 102 | expect(ignoredWithUpdates).toHaveLength(1) 103 | expect(ignoredWithUpdates[0].name).toBe('homebridge-plugin-ignored') 104 | }) 105 | 106 | it('should filter homebridge-config-ui-x when in ignore list', () => { 107 | const mockPlugins = [ 108 | { 109 | name: 'homebridge-config-ui-x', 110 | installedVersion: '4.0.0', 111 | latestVersion: '4.1.0', 112 | updateAvailable: true, 113 | }, 114 | ] 115 | 116 | const ignoredPlugins = ['homebridge-config-ui-x'] 117 | 118 | // Test filtering homebridge-config-ui-x updates when ignored 119 | const shouldFilterUI = ignoredPlugins.includes('homebridge-config-ui-x') 120 | expect(shouldFilterUI).toBe(true) 121 | 122 | // Simulate what would happen in the checkUi() method 123 | const uiPlugin = mockPlugins.find(plugin => plugin.name === 'homebridge-config-ui-x') 124 | const shouldAddUpdate = uiPlugin && uiPlugin.updateAvailable && !shouldFilterUI 125 | expect(shouldAddUpdate).toBe(false) 126 | }) 127 | 128 | it('should filter homebridge core updates when in ignore list', () => { 129 | const mockHomebridge = { 130 | name: 'homebridge', 131 | installedVersion: '1.0.0', 132 | latestVersion: '1.1.0', 133 | updateAvailable: true, 134 | } 135 | 136 | const ignoredPlugins = ['homebridge'] 137 | 138 | // Test filtering homebridge core updates when ignored 139 | const shouldFilterHomebridge = ignoredPlugins.includes('homebridge') 140 | expect(shouldFilterHomebridge).toBe(true) 141 | 142 | // Simulate what would happen in the checkUi() method 143 | const shouldAddUpdate = mockHomebridge.updateAvailable && !shouldFilterHomebridge 144 | expect(shouldAddUpdate).toBe(false) 145 | }) 146 | }) -------------------------------------------------------------------------------- /docs/variables/default.html: -------------------------------------------------------------------------------- 1 | default | @homebridge-plugins/homebridge-plugin-update-check

    Variable default

    default: (api: API) => void

    Type Declaration

      • (api: API): void
      • Parameters

        • api: API

        Returns void

    2 | -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | @homebridge-plugins/homebridge-plugin-update-check

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

      Variables

      default
      2 | -------------------------------------------------------------------------------- /src/failureSensor.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | API, 3 | Characteristic, 4 | CharacteristicValue, 5 | HAP, 6 | Logging, 7 | PlatformAccessory, 8 | Service, 9 | WithUUID, 10 | } from 'homebridge' 11 | 12 | interface SensorInfo { 13 | serviceType: WithUUID 14 | characteristicType: WithUUID Characteristic> 15 | trippedValue: CharacteristicValue 16 | untrippedValue: CharacteristicValue 17 | } 18 | 19 | /** 20 | * Manages a failure sensor for automatic update operations. 21 | * This sensor is triggered when automatic updates fail. 22 | */ 23 | export class FailureSensor { 24 | private readonly log: Logging 25 | private readonly hap: HAP 26 | private readonly sensorInfo: SensorInfo 27 | private service?: Service 28 | 29 | constructor(log: Logging, api: API, sensorType?: string) { 30 | this.log = log 31 | this.hap = api.hap 32 | this.sensorInfo = this.getSensorInfo(sensorType) 33 | } 34 | 35 | /** 36 | * Configure the failure sensor service on the given accessory 37 | */ 38 | configureService(accessory: PlatformAccessory): void { 39 | this.checkFailureService(accessory, this.hap.Service.MotionSensor) 40 | this.checkFailureService(accessory, this.hap.Service.ContactSensor) 41 | this.checkFailureService(accessory, this.hap.Service.OccupancySensor) 42 | this.checkFailureService(accessory, this.hap.Service.SmokeSensor) 43 | this.checkFailureService(accessory, this.hap.Service.LeakSensor) 44 | this.checkFailureService(accessory, this.hap.Service.LightSensor) 45 | this.checkFailureService(accessory, this.hap.Service.HumiditySensor) 46 | this.checkFailureService(accessory, this.hap.Service.CarbonMonoxideSensor) 47 | this.checkFailureService(accessory, this.hap.Service.CarbonDioxideSensor) 48 | this.checkFailureService(accessory, this.hap.Service.AirQualitySensor) 49 | 50 | // Initialize to normal state 51 | this.setState(false) 52 | } 53 | 54 | /** 55 | * Add the failure sensor service to a new accessory 56 | */ 57 | addToAccessory(accessory: PlatformAccessory): void { 58 | accessory.addService(this.sensorInfo.serviceType as unknown as Service) 59 | } 60 | 61 | /** 62 | * Set the failure sensor state 63 | * @param failed - true if failure detected, false for normal operation 64 | */ 65 | setState(failed: boolean): void { 66 | if (this.service) { 67 | const value = failed ? this.sensorInfo.trippedValue : this.sensorInfo.untrippedValue 68 | this.service.setCharacteristic(this.sensorInfo.characteristicType, value) 69 | this.log.debug(`Set failure sensor to ${failed ? 'triggered' : 'normal'} state`) 70 | } 71 | } 72 | 73 | /** 74 | * Get the sensor info for the configured failure sensor type 75 | */ 76 | private getSensorInfo(sensorType?: string): SensorInfo { 77 | switch (sensorType?.toLowerCase()) { 78 | case 'contact': 79 | return { 80 | serviceType: this.hap.Service.ContactSensor, 81 | characteristicType: this.hap.Characteristic.ContactSensorState, 82 | untrippedValue: 0, 83 | trippedValue: 1, 84 | } 85 | case 'occupancy': 86 | return { 87 | serviceType: this.hap.Service.OccupancySensor, 88 | characteristicType: this.hap.Characteristic.OccupancyDetected, 89 | untrippedValue: 0, 90 | trippedValue: 1, 91 | } 92 | case 'smoke': 93 | return { 94 | serviceType: this.hap.Service.SmokeSensor, 95 | characteristicType: this.hap.Characteristic.SmokeDetected, 96 | untrippedValue: 0, 97 | trippedValue: 1, 98 | } 99 | case 'leak': 100 | return { 101 | serviceType: this.hap.Service.LeakSensor, 102 | characteristicType: this.hap.Characteristic.LeakDetected, 103 | untrippedValue: 0, 104 | trippedValue: 1, 105 | } 106 | case 'light': 107 | return { 108 | serviceType: this.hap.Service.LightSensor, 109 | characteristicType: this.hap.Characteristic.CurrentAmbientLightLevel, 110 | untrippedValue: 0.0001, 111 | trippedValue: 100000, 112 | } 113 | case 'humidity': 114 | return { 115 | serviceType: this.hap.Service.HumiditySensor, 116 | characteristicType: this.hap.Characteristic.CurrentRelativeHumidity, 117 | untrippedValue: 0, 118 | trippedValue: 100, 119 | } 120 | case 'monoxide': 121 | return { 122 | serviceType: this.hap.Service.CarbonMonoxideSensor, 123 | characteristicType: this.hap.Characteristic.CarbonMonoxideDetected, 124 | untrippedValue: 0, 125 | trippedValue: 1, 126 | } 127 | case 'dioxide': 128 | return { 129 | serviceType: this.hap.Service.CarbonDioxideSensor, 130 | characteristicType: this.hap.Characteristic.CarbonDioxideDetected, 131 | untrippedValue: 0, 132 | trippedValue: 1, 133 | } 134 | case 'air': 135 | return { 136 | serviceType: this.hap.Service.AirQualitySensor, 137 | characteristicType: this.hap.Characteristic.AirQuality, 138 | untrippedValue: 1, 139 | trippedValue: 5, 140 | } 141 | case 'motion': 142 | default: 143 | return { 144 | serviceType: this.hap.Service.MotionSensor, 145 | characteristicType: this.hap.Characteristic.MotionDetected, 146 | untrippedValue: false, 147 | trippedValue: true, 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Check and configure the failure service for a specific service type 154 | */ 155 | private checkFailureService(accessory: PlatformAccessory, serviceType: WithUUID): boolean { 156 | const service = accessory.getService(serviceType) 157 | if (this.sensorInfo.serviceType === serviceType) { 158 | if (service) { 159 | this.service = service 160 | } else { 161 | this.service = accessory.addService(serviceType as unknown as Service) 162 | this.service.setCharacteristic(this.hap.Characteristic.Name, 'Update Failure') 163 | } 164 | return true 165 | } else { 166 | // Don't remove services that might be used by the main sensor 167 | // This check should be handled by the caller to avoid conflicts 168 | return false 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # homebridge-plugin-update-check 2 | 3 | A TypeScript-based Homebridge plugin that creates HomeKit sensors to notify when updates are available for Homebridge, Homebridge UI, plugins, and Docker containers. The plugin uses either homebridge-config-ui-x API or npm-check-updates as a fallback to check for updates. 4 | 5 | Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. 6 | 7 | ## Working Effectively 8 | 9 | ### Initial Setup 10 | Bootstrap the repository in a fresh environment: 11 | ```bash 12 | npm install # Takes ~30 seconds. NEVER CANCEL. 13 | ``` 14 | 15 | ### Build Process 16 | Build the TypeScript code and prepare distribution files: 17 | ```bash 18 | npm run build # Takes ~4 seconds. Compiles TS + copies UI files 19 | ``` 20 | 21 | **Build Details:** 22 | - `npm run clean` - Removes dist/ directory (~0.2 seconds) 23 | - `tsc` - Compiles TypeScript to JavaScript (~2 seconds) 24 | - `npm run plugin-ui` - Copies UI files to dist/ (~0.1 seconds) 25 | 26 | ### Testing 27 | Run the test suite: 28 | ```bash 29 | npx vitest run # Takes <1 second 30 | ``` 31 | 32 | The project has minimal test coverage with only configuration type tests in `src/configTypes.test.ts`. 33 | 34 | ### Linting and Code Quality 35 | Always run these before committing changes: 36 | ```bash 37 | npm run lint # Takes ~2 seconds. Runs ESLint on TypeScript files 38 | npm run lint:fix # Auto-fixes ESLint issues 39 | ``` 40 | 41 | ### Documentation 42 | Generate and validate TypeDoc documentation: 43 | ```bash 44 | npm run docs # Takes ~2 seconds. Generates docs/ directory 45 | npm run docs:lint # Takes ~5 seconds. Validates docs with warnings as errors 46 | ``` 47 | 48 | ### Full Validation Pipeline 49 | Run the complete validation used in CI: 50 | ```bash 51 | npm run prepublishOnly # Takes ~17 seconds. Runs lint + build + plugin-ui + docs + docs:lint 52 | ``` 53 | 54 | ### Development Workflow 55 | For continuous development with auto-rebuild: 56 | ```bash 57 | npm run watch # Builds, sets up plugin-ui, links globally, and runs nodemon 58 | ``` 59 | **Note**: This script is intended for use in a Homebridge development environment. 60 | 61 | ### Dependency Management 62 | Check for outdated dependencies: 63 | ```bash 64 | npm run check # Runs npm install && npm outdated 65 | ``` 66 | 67 | ## Validation 68 | 69 | **CRITICAL**: This is a Homebridge plugin - it CANNOT be run standalone like a typical application. It requires integration with a Homebridge environment and HomeKit. 70 | 71 | ### Manual Validation Steps 72 | After making changes, always: 73 | 1. **Build validation**: Run `npm run build` and verify `dist/` contains compiled JS files 74 | 2. **Syntax check**: Run `node -c dist/index.js` to verify JavaScript syntax 75 | 3. **Lint validation**: Run `npm run lint` to ensure code style compliance 76 | 4. **Test validation**: Run `npx vitest run` to ensure tests pass 77 | 5. **Documentation**: Run `npm run docs:lint` to validate TypeDoc comments 78 | 79 | ### Build Artifacts Validation 80 | Verify these files exist after building: 81 | - `dist/index.js` - Main plugin entry point 82 | - `dist/index.d.ts` - TypeScript declarations 83 | - `dist/configTypes.js` - Configuration types 84 | - `dist/ui-api.js` - Homebridge UI integration 85 | - `dist/homebridge-ui/public/index.html` - UI component 86 | 87 | ### CI Pipeline Compatibility 88 | The GitHub Actions workflow uses the homebridge shared workflow. Always run these locally before pushing: 89 | ```bash 90 | npm install && npm run lint && npm run build && npx vitest run 91 | ``` 92 | 93 | ## Project Structure 94 | 95 | ### Key Source Files 96 | - `src/index.ts` - Main plugin implementation (PluginUpdatePlatform class) 97 | - `src/configTypes.ts` - Configuration interface definitions 98 | - `src/ui-api.ts` - Homebridge Config UI X integration 99 | - `src/configTypes.test.ts` - Basic configuration type tests 100 | - `src/homebridge-ui/` - Plugin UI components 101 | 102 | ### Configuration Files 103 | - `package.json` - Dependencies and npm scripts 104 | - `tsconfig.json` - TypeScript compiler configuration 105 | - `eslint.config.js` - ESLint rules (uses @antfu/eslint-config) 106 | - `typedoc.json` - Documentation generation settings 107 | - `config.schema.json` - Homebridge configuration schema 108 | 109 | ### Build Outputs 110 | - `dist/` - Compiled JavaScript and type definitions 111 | - `docs/` - Generated TypeDoc documentation (not committed) 112 | 113 | ### Key Dependencies 114 | - `homebridge` - Platform integration (dev dependency for types) 115 | - `npm-check-updates` - Fallback update checker 116 | - `axios` - HTTP client for API calls 117 | - `croner` - Cron job scheduling 118 | - `jsonwebtoken` - Homebridge UI authentication 119 | 120 | ## Common Tasks 121 | 122 | ### Adding New Features 123 | 1. Modify TypeScript files in `src/` 124 | 2. Add corresponding tests in `src/*.test.ts` if needed 125 | 3. Update configuration schema in `config.schema.json` if adding config options 126 | 4. Run `npm run build && npm run lint && npx vitest run` 127 | 5. Update documentation comments for TypeDoc if adding public APIs 128 | 129 | ### Debugging Build Issues 130 | 1. Check TypeScript compilation: `npx tsc --noEmit` 131 | 2. Validate ESLint configuration: `npm run lint` 132 | 3. Clean and rebuild: `npm run clean && npm run build` 133 | 134 | ### Release Process 135 | The project uses automated releases via GitHub Actions. The `prepublishOnly` script ensures quality before publishing: 136 | ```bash 137 | npm run prepublishOnly # Must pass before any release 138 | ``` 139 | 140 | ## Architecture Notes 141 | 142 | ### Plugin Functionality 143 | - Creates HomeKit sensors (motion, contact, occupancy, etc.) that trigger when updates are available 144 | - Checks updates hourly via cron jobs 145 | - Supports checking Homebridge core, Homebridge UI, plugins, and Docker updates independently 146 | - Uses homebridge-config-ui-x API when available, falls back to npm-check-updates 147 | 148 | ### Configuration Options 149 | See `src/configTypes.ts` for the complete interface. Key options: 150 | - `platform`: Must be "PluginUpdate" 151 | - `sensorType`: Type of HomeKit sensor to create 152 | - `checkHomebridgeUpdates`, `checkHomebridgeUIUpdates`, `checkPluginUpdates`, `checkDockerUpdates`: Boolean flags 153 | - `forceNcu`: Force use of npm-check-updates instead of UI API 154 | 155 | ### Development Limitations 156 | - Cannot test actual update checking without Homebridge environment 157 | - Cannot interact with HomeKit without proper Homebridge setup 158 | - Limited to build/lint/unit test validation in development environment -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "PluginUpdate", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "customUi": true, 6 | "customUiPath": "./dist/homebridge-ui", 7 | "headerDisplay": "A Homebridge plugin for checking for updates to Homebridge and plugins.", 8 | "footerDisplay": "Raise [Issues](https://github.com/Sunoo/homebridge-plugin-update-check/issues) or submit [Pull Requests](https://github.com/Sunoo/homebridge-plugin-update-check/pulls) on [Project Page](https://github.com/Sunoo/homebridge-plugin-update-check).", 9 | "schema": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "title": "Name", 14 | "type": "string", 15 | "default": "Plugin Update", 16 | "description": "A unique name for the accessory. It will be used as the accessory name in HomeKit. Can only contain letters, numbers, spaces, hyphens, and underscores. Periods and other special characters are not allowed as they can cause HomeKit pairing issues.", 17 | "pattern": "^[a-zA-Z0-9\\s_-]+$", 18 | "minLength": 1, 19 | "maxLength": 64 20 | }, 21 | "sensorType": { 22 | "title": "Sensor Type", 23 | "type": "string", 24 | "default": "motion", 25 | "description": "What type of sensor will be exposed to HomeKit.", 26 | "oneOf": [ 27 | { 28 | "title": "Motion Sensor", 29 | "enum": ["motion"] 30 | }, 31 | { 32 | "title": "Contact Sensor", 33 | "enum": ["contact"] 34 | }, 35 | { 36 | "title": "Occupancy Sensor", 37 | "enum": ["occupancy"] 38 | }, 39 | { 40 | "title": "Humidity Sensor", 41 | "enum": ["humidity"] 42 | }, 43 | { 44 | "title": "Light Sensor", 45 | "enum": ["light"] 46 | }, 47 | { 48 | "title": "Air Quality Sensor", 49 | "enum": ["air"] 50 | }, 51 | { 52 | "title": "Leak Sensor", 53 | "enum": ["leak"] 54 | }, 55 | { 56 | "title": "Smoke Sensor", 57 | "enum": ["smoke"] 58 | }, 59 | { 60 | "title": "Carbon Dioxide Sensor", 61 | "enum": ["dioxide"] 62 | }, 63 | { 64 | "title": "Carbon Monoxide Sensor", 65 | "enum": ["monoxide"] 66 | } 67 | ] 68 | }, 69 | "checkHomebridgeUpdates": { 70 | "title": "Check for Homebridge updates", 71 | "type": "boolean", 72 | "description": "Check if an update is available for the Homebridge server", 73 | "default": true 74 | }, 75 | "checkHomebridgeUIUpdates": { 76 | "title": "Check for Homebridge Config UI updates", 77 | "type": "boolean", 78 | "description": "Check if an update is available for the Homebridge UI", 79 | "default": true 80 | }, 81 | "checkPluginUpdates": { 82 | "title": "Check for plugin updates", 83 | "type": "boolean", 84 | "description": "Check if updates are available for any installed plugins", 85 | "default": true 86 | }, 87 | "checkDockerUpdates": { 88 | "title": "Check for Docker image updates", 89 | "type": "boolean", 90 | "description": "Check if Docker image updates are available (ignored if not running in Docker container)" 91 | }, 92 | "initialCheckDelay": { 93 | "title": "Initial Check Delay (seconds)", 94 | "type": "integer", 95 | "description": "Delay in seconds before performing the initial update check after startup", 96 | "default": 10, 97 | "minimum": 0, 98 | "maximum": 300 99 | }, 100 | "autoUpdateHomebridge": { 101 | "title": "Auto-update Homebridge", 102 | "type": "boolean", 103 | "description": "Automatically install Homebridge updates when available (requires homebridge-config-ui-x and sufficient npm privileges)", 104 | "default": false 105 | }, 106 | "autoUpdateHomebridgeUI": { 107 | "title": "Auto-update Homebridge Config UI", 108 | "type": "boolean", 109 | "description": "Automatically install Homebridge Config UI updates when available (requires homebridge-config-ui-x and sufficient npm privileges)", 110 | "default": false 111 | }, 112 | "autoUpdatePlugins": { 113 | "title": "Auto-update plugins", 114 | "type": "boolean", 115 | "description": "Automatically install plugin updates when available (requires homebridge-config-ui-x and sufficient npm privileges)", 116 | "default": false 117 | }, 118 | "allowDirectNpmUpdates": { 119 | "title": "Allow direct npm updates", 120 | "type": "boolean", 121 | "description": "Allow automatic updates using direct npm commands even when homebridge-config-ui-x is not available (requires sufficient npm privileges)", 122 | "default": false 123 | }, 124 | "autoRestartAfterUpdates": { 125 | "title": "Auto-restart after updates", 126 | "type": "boolean", 127 | "description": "Automatically restart Homebridge after successful automatic updates to apply changes", 128 | "default": false 129 | }, 130 | "failureSensorType": { 131 | "title": "Failure Sensor Type", 132 | "type": "string", 133 | "default": "motion", 134 | "description": "What type of sensor will be used for update/restart failure notifications (only shown when auto-updates are enabled).", 135 | "oneOf": [ 136 | { 137 | "title": "Motion Sensor", 138 | "enum": ["motion"] 139 | }, 140 | { 141 | "title": "Contact Sensor", 142 | "enum": ["contact"] 143 | }, 144 | { 145 | "title": "Occupancy Sensor", 146 | "enum": ["occupancy"] 147 | }, 148 | { 149 | "title": "Humidity Sensor", 150 | "enum": ["humidity"] 151 | }, 152 | { 153 | "title": "Light Sensor", 154 | "enum": ["light"] 155 | }, 156 | { 157 | "title": "Air Quality Sensor", 158 | "enum": ["air"] 159 | }, 160 | { 161 | "title": "Leak Sensor", 162 | "enum": ["leak"] 163 | }, 164 | { 165 | "title": "Smoke Sensor", 166 | "enum": ["smoke"] 167 | }, 168 | { 169 | "title": "Carbon Dioxide Sensor", 170 | "enum": ["dioxide"] 171 | }, 172 | { 173 | "title": "Carbon Monoxide Sensor", 174 | "enum": ["monoxide"] 175 | } 176 | ] 177 | }, 178 | "respectDisabledPlugins": { 179 | "title": "Respect disabled plugin update notifications", 180 | "type": "boolean", 181 | "description": "When enabled, respects the 'Hide update notifications for this plugin' setting from homebridge-config-ui-x. Plugins marked to hide updates will not trigger notifications.", 182 | "default": true 183 | }, 184 | "": { 185 | "title": "", 186 | "description": "Note: This feature may require updating Homebridge UI to a newer varsion. It is recommended to update to the latest version.", 187 | "type": "object", 188 | "properties": {} 189 | } 190 | }, 191 | "required": ["name", "sensorType"] 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/homebridge-ui/public/index.html: -------------------------------------------------------------------------------- 1 |

      2 | homebridge-plugin-update-check logo 5 |

      6 | 12 | 17 | 21 | 52 | 106 | -------------------------------------------------------------------------------- /docs/assets/icons.svg: -------------------------------------------------------------------------------- 1 | MMNEPVFCICPMFPCPTTAAATR -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/) 4 | 5 | ## [2.3.7](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.6...v2.3.7) (2025-11-21) 6 | 7 | 8 | 9 | ## [2.3.6](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.5...v2.3.6) (2025-11-01) 10 | 11 | 12 | 13 | ## [2.3.5](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.3...v2.3.5) (2025-10-03) 14 | 15 | 16 | 17 | ## [2.3.3](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.2...v2.3.3) (2025-09-14) 18 | 19 | 20 | 21 | ## [2.3.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.1...v2.3.2) (2025-09-13) 22 | 23 | 24 | 25 | ## [2.3.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.0...v2.3.1) (2025-09-04) 26 | 27 | 28 | 29 | # [2.3.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.2.1...v2.3.0) (2025-08-18) 30 | 31 | 32 | 33 | ## [2.2.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.1.0...v2.2.1) (2025-08-17) 34 | 35 | 36 | 37 | # [2.1.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.2...v2.1.0) (2025-08-09) 38 | 39 | 40 | 41 | ## [2.0.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.1...v2.0.2) (2025-03-05) 42 | 43 | 44 | 45 | ## [2.0.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.0...v2.0.1) (2025-01-26) 46 | 47 | 48 | 49 | ## [1.0.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v1.0.1...v1.0.2) (2022-03-26) 50 | 51 | 52 | 53 | ## [1.0.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v1.0.0...v1.0.1) (2022-01-15) 54 | 55 | 56 | 57 | # [1.0.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v0.2.1...v1.0.0) (2022-01-15) 58 | 59 | 60 | 61 | ## [0.2.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v0.2.0...v0.2.1) (2021-02-20) 62 | 63 | 64 | 65 | # [0.2.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v0.1.1...v0.2.0) (2021-02-20) 66 | 67 | 68 | 69 | ## 0.1.1 (2021-02-19) 70 | 71 | ## [2.3.6](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.5...v2.3.6) (2025-11-01) 72 | 73 | 74 | 75 | ## [2.3.5](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.3...v2.3.5) (2025-10-03) 76 | 77 | 78 | 79 | ## [2.3.3](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.2...v2.3.3) (2025-09-14) 80 | 81 | 82 | 83 | ## [2.3.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.1...v2.3.2) (2025-09-13) 84 | 85 | 86 | 87 | ## [2.3.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.0...v2.3.1) (2025-09-04) 88 | 89 | 90 | 91 | # [2.3.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.2.1...v2.3.0) (2025-08-18) 92 | 93 | 94 | 95 | ## [2.2.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.1.0...v2.2.1) (2025-08-17) 96 | 97 | 98 | 99 | # [2.1.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.2...v2.1.0) (2025-08-09) 100 | 101 | 102 | 103 | ## [2.0.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.1...v2.0.2) (2025-03-05) 104 | 105 | 106 | 107 | ## [2.0.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.0...v2.0.1) (2025-01-26) 108 | 109 | 110 | 111 | ## [1.0.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v1.0.1...v1.0.2) (2022-03-26) 112 | 113 | 114 | 115 | ## [1.0.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v1.0.0...v1.0.1) (2022-01-15) 116 | 117 | 118 | 119 | # [1.0.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v0.2.1...v1.0.0) (2022-01-15) 120 | 121 | 122 | 123 | ## [0.2.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v0.2.0...v0.2.1) (2021-02-20) 124 | 125 | 126 | 127 | # [0.2.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v0.1.1...v0.2.0) (2021-02-20) 128 | 129 | 130 | 131 | ## 0.1.1 (2021-02-19) 132 | 133 | ## [2.3.6](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.6) (2025-11-01) 134 | 135 | ### What's Changed 136 | - Add explicit warning when an outdated Homebridge UI (homebridge-config-ui-x) is detected, explaining that a newer UI version is required for full functionality ([#200](https://github.com/homebridge-plugins/homebridge-plugin-update-check/pull/200)) 137 | - Clarify the requirement notice to better guide users on updating the Homebridge UI 138 | - Housekeeping and updated dependencies. 139 | 140 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.5...v2.3.6 141 | 142 | ## [2.3.5](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.5) (2025-11-01) 143 | 144 | ### What's Changed 145 | - Fix error where current Docker version is not found by @justjam2013 in https://github.com/homebridge-plugins/homebridge-plugin-update-check/pull/166 146 | - v2.3.5 by @donavanbecker in https://github.com/homebridge-plugins/homebridge-plugin-update-check/pull/189 147 | 148 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.4...v2.3.5 149 | 150 | ## [2.3.4](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.4) (2025-09-14) 151 | 152 | ### What's Changed 153 | - Fix error where current Docker version is not found by @justjam2013 in https://github.com/homebridge-plugins/homebridge-plugin-update-check/pull/166 154 | 155 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.3...v2.3.4 156 | 157 | ## [2.3.3](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.3) (2025-09-14) 158 | 159 | ### What's Changed 160 | - v2.3.3 ([b7a4c9e](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/b7a4c9ec2b079b6f3e51411311573080b281d97c)) 161 | 162 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.2...v2.3.3 163 | 164 | ## [2.3.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.2) (2025-09-13) 165 | 166 | ### What's Changed 167 | - Remove double variables for file and directory paths (#115) ([4743e21](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/4743e21c40bac30c1084d6ca0642f1d9ad723579)) 168 | - Remove npm-check-updates (NCU) option as it doesn't add value (#117) ([d8f5ee5](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/d8f5ee5dfc3886d665b646803fa48c1f6506abdb)) 169 | - Added cached dns lookup and retries (#118) ([f375adb](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/f375adb8c7a3bae35ff548c86c16a34e7daa037c)) 170 | - Add NodeJS 24 support by updating engines.node to ^20 || ^22 || ^24 (rebased against beta-2.3.2) (#123) ([f64de4d](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/f64de4d1f3f6d4bf46102b9ca14716360aba6ce0)) 171 | 172 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.1...v2.3.2 173 | 174 | ## [2.3.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.1) (2025-09-03) 175 | 176 | ### What's Changed 177 | - Merge branch 'latest' into beta-2.3.1 ([f30d910](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/f30d91080dd11fecafc84275bd078ce30e15fe2d)) 178 | - Fix NCU filter regex construction to detect plugin updates correctly (#92) ([32733d8](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/32733d889fc9bdcf0d58f279f387b105a36bfc06)) 179 | - v2.3.1 ([c6b1af1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/c6b1af1844f90520bb9217171d6117c6425f58d9)) 180 | - Fix Homebridge restart failure: use PUT method and implement endpoint fallback (#101) ([feb32be](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/feb32befefd97a5f87155ca95f58ebb105a83471)) 181 | - Add name validation to prevent HomeKit pairing issues with child bridges (#91) ([4db28af](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/4db28afd90f4f5c21c10579d8d0da08e022ea8f6)) 182 | - Patch ncu (#99) ([d3b7a3f](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/d3b7a3f5326142f1aa54a6d5247546e6803d8b48)) 183 | - Parenthesis & patch ncu (#98) ([7796773](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/7796773d376e3ff936a9724109125fc1c671dd7f)) 184 | - resolve __dirname is not defined (#97) ([c295a20](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/c295a20962e982b449bd1296b0b02708e1fde058)) 185 | - Add automatic update functionality with configurable npm support, restart capability, automatic backup creation, and configurable failure notifications for Homebridge, UI, and plugins (#94) ([e58b488](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/e58b488edf02eccf801627314107a70f08c36693)) 186 | - Fix npm-check-updates CLI path for v16+ compatibility (#96) ([acf607c](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/acf607c9cd5944eb418b88a8465744cb942d1460)) 187 | - Update available update list (#89) ([bd84b83](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/bd84b83556c3bffdac0d35f15e1047f7c54e8887)) 188 | - Fix ReferenceError: __dirname is not defined in ESM module (#86) ([5668b01](https://github.com/homebridge-plugins/homebridge-plugin-update-check/commit/5668b015900d708bbeb4a38d82fac14a7215f022)) 189 | 190 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.3.0...v2.3.1 191 | 192 | ## [2.3.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.3.0) (2025-08-18) 193 | 194 | ### What's Changed 195 | - Display newer updates in logs 196 | 197 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.2.1...v2.3.0 198 | 199 | ## [2.2.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.2.1) (2025-08-09) 200 | 201 | ### What's Changed 202 | - Change logging to daily 203 | - Fix log info output 204 | 205 | ## [2.1.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.1.0) (2025-08-09) 206 | 207 | ### What's Changed 208 | - Output available updates to log 209 | - Fix plugin update check 210 | - Fixed version checks and added debug output 211 | - Add docker version check and fixed NCU search 212 | - Fixed field names 213 | - Fix default check value 214 | - Select which components to check for updates 215 | - Fix Labler 216 | - Build fixes and improvements 217 | 218 | ## [2.0.2](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.0.2) (2025-03-04) 219 | 220 | ### What's Changes 221 | - Housekeeping and updated dependencies. 222 | 223 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.1...v2.0.2 224 | 225 | ## [2.0.1](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.0.1) (2025-01-25) 226 | 227 | ### What's Changes 228 | - Housekeeping and updated dependencies. 229 | 230 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v2.0.0...v2.0.1 231 | 232 | ## [2.0.0](https://github.com/homebridge-plugins/homebridge-plugin-update-check/releases/tag/v2.0.0) (2025-01-16) 233 | 234 | ### What's Changes 235 | - This plugins has moved to a scoped plugin under the `@homebridge-plugins` org. 236 | - Homebridge UI is designed to transition you to the new scoped plugin. 237 | - Updated to ES Module 238 | 239 | **Full Changelog**: https://github.com/homebridge-plugins/homebridge-plugin-update-check/compare/v1.0.2...v2.0.0 240 | -------------------------------------------------------------------------------- /docs/assets/icons.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | addIcons(); 3 | function addIcons() { 4 | if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); 5 | const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); 6 | svg.innerHTML = `MMNEPVFCICPMFPCPTTAAATR`; 7 | svg.style.display = "none"; 8 | if (location.protocol === "file:") updateUseElements(); 9 | } 10 | 11 | function updateUseElements() { 12 | document.querySelectorAll("use").forEach(el => { 13 | if (el.getAttribute("href").includes("#icon-")) { 14 | el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); 15 | } 16 | }); 17 | } 18 | })() -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | @homebridge-plugins/homebridge-plugin-update-check

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

      homebridge-plugin-update-check

      npm npm verified-by-homebridge

      2 |

      A Homebridge plugin for checking for updates to Homebridge and plugins.

      3 |
        4 |
      1. Install Homebridge using the official instructions.
      2. 5 |
      3. Install this plugin using: sudo npm install -g homebridge-plugin-update-check.
      4. 6 |
      5. Update your configuration file. See sample config.json snippet below.
      6. 7 |
      8 |

      Configuration sample:

      9 |
      "platforms": [
      {
      "name": "PluginUpdate",
      "sensorType": "contact",
      "checkHomebridgeUpdates": false,
      "checkHomebridgeUIUpdates": false,
      "checkPluginUpdates": true,
      "checkDockerUpdates": true,
      "autoUpdateHomebridge": false,
      "autoUpdateHomebridgeUI": false,
      "autoUpdatePlugins": false,
      "allowDirectNpmUpdates": false,
      "autoRestartAfterUpdates": false,
      "failureSensorType": "motion",
      "platform": "PluginUpdate"
      }
      ] 10 |
      11 | 12 |
        13 |
      • "platform": Must always be "PluginUpdate" (required)
      • 14 |
      • "sensorType": What type of sensor will be exposed to HomeKit. Can be motion, contact, occupancy, humidity, light, air, leak, smoke, dioxide, or monoxide (Default: motion)
      • 15 |
      • "checkHomebridgeUpdates": Check if an update is available for the Homebridge server
      • 16 |
      • "checkHomebridgeUIUpdates: Check if an update is available for the Homebridge UI
      • 17 |
      • "checkPluginUpdates": Check if updates are available for any installed plugins
      • 18 |
      • "checkDockerUpdates": If running in Docker, check if newer Docker versions are available. If not running in Docker, does nothing
      • 19 |
      • "autoUpdateHomebridge": Automatically install Homebridge updates when available (Default: false)
      • 20 |
      • "autoUpdateHomebridgeUI": Automatically install Homebridge Config UI updates when available (Default: false)
      • 21 |
      • "autoUpdatePlugins": Automatically install plugin updates when available (Default: false)
      • 22 |
      • "allowDirectNpmUpdates": Allow automatic updates using direct npm commands even when homebridge-config-ui-x is not available (Default: false)
      • 23 |
      • "autoRestartAfterUpdates": Automatically restart Homebridge after successful automatic updates to apply changes (Default: false)
      • 24 |
      • "failureSensorType": What type of sensor will be used for update/restart failure notifications. Can be motion, contact, occupancy, humidity, light, air, leak, smoke, dioxide, or monoxide (Default: motion, only shown when auto-updates are enabled)
      • 25 |
      26 |

      Homebridge, Homebridge UI, plugin, and Docker updates can be selected independently. This allows you for example, to ignore available Homebridge, Homebridge UI available updates if you are running Homebridge in a Docker container and wish to only update these components when a new Docker image is available.

      27 |

      Note on Automatic Updates: When automatic updates are enabled, the plugin will:

      28 |
        29 |
      1. Automatically create a backup before performing any updates (when homebridge-config-ui-x is available)
      2. 30 |
      3. Attempt to install updates via npm commands. This requires: 31 |
          32 |
        • homebridge-config-ui-x to be installed and properly configured (unless allowDirectNpmUpdates is enabled)
        • 33 |
        • Sufficient privileges to install global npm packages
        • 34 |
        35 |
      4. 36 |
      5. Automatically restart Homebridge after successful updates (when autoRestartAfterUpdates is enabled)
      6. 37 |
      7. Monitor for failures and provide notifications via a configurable failure sensor (type set by failureSensorType)
      8. 38 |
      39 |

      Automatic updates are disabled by default for safety. Enable them only if you trust automatic updates. The plugin will automatically create a backup before any updates are performed when homebridge-config-ui-x is available, eliminating the need for manual backup procedures. However, you should still maintain your own backup procedures as a best practice.

      40 |

      Note on Docker Updates: Docker container updates are intentionally not supported via automatic updates for safety reasons. This is because the process performing the update would be running inside the container itself, which would result in the process killing itself halfway through the update, potentially corrupting the container or leaving it in an unusable state.

      41 |

      When allowDirectNpmUpdates is enabled, automatic updates will work even when homebridge-config-ui-x is not available by using direct npm commands. This provides more flexibility but requires ensuring you have the necessary npm privileges.

      42 |
      43 | -------------------------------------------------------------------------------- /src/ui-api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable style/brace-style */ 2 | /* eslint-disable style/operator-linebreak */ 3 | 4 | import type { 5 | HomebridgeConfig, 6 | Logging, 7 | PlatformIdentifier, 8 | PlatformName, 9 | } from 'homebridge' 10 | 11 | import { spawn } from 'node:child_process' 12 | import { readFileSync } from 'node:fs' 13 | import https from 'node:https' 14 | import path from 'node:path' 15 | import process from 'node:process' 16 | 17 | import axios from 'axios' 18 | import axiosRetry from 'axios-retry' 19 | import CacheableLookup from 'cacheable-lookup' 20 | import jwt from 'jsonwebtoken' 21 | 22 | export interface InstalledPlugin { 23 | name: string 24 | installedVersion: string 25 | latestVersion: string 26 | updateAvailable: boolean 27 | } 28 | 29 | interface SecretsFile { 30 | secretKey: string 31 | } 32 | 33 | interface UiConfig { 34 | platform: PlatformName | PlatformIdentifier 35 | host?: string 36 | port?: number 37 | ssl?: { 38 | key?: string 39 | pfx?: string 40 | } 41 | } 42 | 43 | class ApiPluginEndpoints { 44 | static readonly getHomebridgeVersion = '/api/status/homebridge-version' 45 | static readonly getPluginList = '/api/plugins' 46 | static readonly getIgnoredPluginList = '/api/config-editor/ui/plugins/hide-updates-for' 47 | } 48 | 49 | export class UiApi { 50 | private log: Logging 51 | private readonly secrets?: SecretsFile 52 | private readonly baseUrl?: string 53 | private readonly httpsAgent?: https.Agent 54 | private token?: string 55 | private readonly dockerUrl?: string 56 | private readonly cacheable: CacheableLookup 57 | private readonly hbStoragePath: string 58 | 59 | constructor(hbStoragePath: string, log: Logging) { 60 | this.log = log 61 | this.hbStoragePath = hbStoragePath 62 | 63 | axiosRetry(axios, { 64 | retries: 3, 65 | retryDelay: (...arg) => axiosRetry.exponentialDelay(...arg, 1000), 66 | 67 | onRetry: (retryCount, error, requestConfig) => { 68 | this.log.debug(`${requestConfig.url} - retry count: ${retryCount}, error: ${error.message}`) 69 | }, 70 | }) 71 | const MAX_TTL_SEC = 86400; // limit TTL to 24 hours 72 | this.cacheable = new CacheableLookup({ maxTtl: MAX_TTL_SEC }); 73 | 74 | const configPath = path.resolve(hbStoragePath, 'config.json') 75 | const hbConfig = JSON.parse(readFileSync(configPath, 'utf8')) as HomebridgeConfig 76 | const config = hbConfig.platforms.find((config: { platform: string }) => 77 | config.platform === 'config' || config.platform === 'homebridge-config-ui-x.config') as UiConfig 78 | 79 | if (config) { 80 | const secretPath = path.resolve(hbStoragePath, '.uix-secrets') 81 | this.secrets = JSON.parse(readFileSync(secretPath, 'utf8')) 82 | 83 | const ssl = !!config.ssl?.key || !!config.ssl?.pfx 84 | 85 | const protocol = ssl ? 'https://' : 'http://' 86 | const host = config.host ?? 'localhost' 87 | const port = config.port ?? 8581 88 | 89 | this.baseUrl = `${protocol + host}:${port.toString()}` 90 | 91 | const dockerProtocol = 'https://' 92 | const dockerHost = 'hub.docker.com' 93 | 94 | this.dockerUrl = `${dockerProtocol + dockerHost}` 95 | 96 | if (ssl) { 97 | this.httpsAgent = new https.Agent({ rejectUnauthorized: false }) // don't reject self-signed certs 98 | } 99 | } 100 | } 101 | 102 | public isConfigured(): boolean { 103 | return this.secrets !== undefined 104 | } 105 | 106 | public async getHomebridge(): Promise { 107 | if (this.isConfigured()) { 108 | const result = await this.makeCall(ApiPluginEndpoints.getHomebridgeVersion) as Array 109 | 110 | if (result.length > 0) { 111 | return result[0] 112 | } 113 | } 114 | 115 | return { 116 | name: '', 117 | installedVersion: '', 118 | latestVersion: '', 119 | updateAvailable: false, 120 | } 121 | } 122 | 123 | public async getPlugins(): Promise> { 124 | if (this.isConfigured()) { 125 | return await this.makeCall(ApiPluginEndpoints.getPluginList) as Array 126 | } else { 127 | return [] 128 | } 129 | } 130 | 131 | public async getIgnoredPlugins(): Promise> { 132 | if (this.isConfigured()) { 133 | try { 134 | const result = await this.makeCall(ApiPluginEndpoints.getIgnoredPluginList) 135 | 136 | // Validate the response format 137 | if (!Array.isArray(result)) { 138 | this.log.warn(`Unexpected response format from ignored plugins API: ${typeof result}, expected array`) 139 | return [] 140 | } 141 | 142 | const ignoredPlugins = result as Array 143 | this.log.debug(`API returned ${ignoredPlugins.length} ignored plugins: ${ignoredPlugins.join(', ')}`) 144 | return ignoredPlugins 145 | } catch (error: any) { 146 | // Check if it's a 404 error (API endpoint doesn't exist) 147 | if (error?.response?.status === 404) { 148 | this.log.warn('Ignored plugins API endpoint not found - requires homebridge-config-ui-x v5.6.2-beta.2 or later') 149 | } else { 150 | this.log.warn(`Failed to retrieve ignored plugins list from /config-editor/ui/plugins/hide-updates-for: ${error}`) 151 | } 152 | return [] 153 | } 154 | } else { 155 | this.log.debug('homebridge-config-ui-x not configured, cannot retrieve ignored plugins list') 156 | return [] 157 | } 158 | } 159 | 160 | public async getDocker(): Promise { 161 | const currentDockerVersion = process.env.DOCKER_HOMEBRIDGE_VERSION 162 | 163 | let dockerInfo: InstalledPlugin = { 164 | name: '', 165 | installedVersion: '', 166 | latestVersion: '', 167 | updateAvailable: false, 168 | } 169 | 170 | if (this.isConfigured() && currentDockerVersion !== undefined) { 171 | const json = await this.makeDockerCall('/v2/repositories/homebridge/homebridge/tags/?page_size=30&page=1&ordering=last_updated') 172 | const images = json.results as any[] 173 | 174 | // If the currently installed version is not returned in the list of Docker versions (too old or deleted), 175 | // then use a last-updated date Jan 1, 1970 176 | const installedImage = images.filter(image => image.name === currentDockerVersion)[0] ?? undefined 177 | const installedImageDate = Date.parse(installedImage ? installedImage.last_updated : '1970-01-01T00:00:00.000000Z') 178 | 179 | // Filter for version names YYYY-MM-DD (no alphas or betas) 180 | const regex: RegExp = /^\d{4}-\d{2}-\d{2}$/gm 181 | const availableImages = images.filter(image => 182 | (image.name as string).match(regex) && 183 | (Date.parse(image.last_updated) > installedImageDate), 184 | ) 185 | if (availableImages.length > 0) { 186 | dockerInfo = { 187 | name: 'Docker image', 188 | installedVersion: currentDockerVersion, 189 | latestVersion: availableImages[0].name, 190 | updateAvailable: true, 191 | } 192 | } 193 | } 194 | 195 | return dockerInfo 196 | } 197 | 198 | public async updateHomebridge(targetVersion?: string): Promise { 199 | this.log.info(`Attempting to update Homebridge${targetVersion ? ` to ${targetVersion}` : ' to latest version'}`) 200 | 201 | try { 202 | const args = ['install', '-g', `homebridge${targetVersion ? `@${targetVersion}` : '@latest'}`] 203 | const result = await this.runNpmCommand(args) 204 | this.log.info(`Homebridge update command completed successfully (${result})`) 205 | return true 206 | } catch (error) { 207 | this.log.error(`Failed to update Homebridge: ${error}`) 208 | return false 209 | } 210 | } 211 | 212 | public async updatePlugin(pluginName: string, targetVersion?: string): Promise { 213 | this.log.info(`Attempting to update plugin ${pluginName}${targetVersion ? ` to ${targetVersion}` : ' to latest version'}`) 214 | 215 | try { 216 | const args = ['install', '-g', `${pluginName}${targetVersion ? `@${targetVersion}` : '@latest'}`] 217 | const result = await this.runNpmCommand(args) 218 | this.log.info(`Plugin ${pluginName} update command completed successfully (${result})`) 219 | return true 220 | } catch (error) { 221 | this.log.error(`Failed to update plugin ${pluginName}: ${error}`) 222 | return false 223 | } 224 | } 225 | 226 | private async runNpmCommand(args: string[]): Promise { 227 | return new Promise((resolve, reject) => { 228 | try { 229 | const npm = spawn('npm', args, { 230 | env: process.env, 231 | }) 232 | 233 | let stdout = '' 234 | let stderr = '' 235 | 236 | // eslint-disable-next-line node/prefer-global/buffer 237 | npm.stdout.on('data', (chunk: Buffer) => { 238 | stdout += chunk.toString() 239 | }) 240 | 241 | // eslint-disable-next-line node/prefer-global/buffer 242 | npm.stderr.on('data', (chunk: Buffer) => { 243 | stderr += chunk.toString() 244 | }) 245 | 246 | npm.on('close', (code) => { 247 | if (code === 0) { 248 | resolve(stdout) 249 | } else { 250 | reject(new Error(`npm command failed with code ${code}: ${stderr}`)) 251 | } 252 | }) 253 | 254 | npm.on('error', (error) => { 255 | reject(error) 256 | }) 257 | } catch (ex) { 258 | reject(ex) 259 | } 260 | }) 261 | } 262 | 263 | public async createBackup(): Promise { 264 | this.log.info('Creating backup before performing updates') 265 | 266 | try { 267 | if (this.isConfigured()) { 268 | // Try different possible backup API endpoints 269 | const backupEndpoints = [ 270 | '/api/backup/create', 271 | '/api/backups/create', 272 | '/api/backup', 273 | '/api/server/backup', 274 | ] 275 | 276 | for (const endpoint of backupEndpoints) { 277 | try { 278 | await this.makeBackupCall(endpoint) 279 | this.log.info(`Backup created successfully via UI API (${endpoint})`) 280 | return true 281 | } catch (error) { 282 | this.log.debug(`Backup endpoint ${endpoint} failed: ${error}`) 283 | // Continue to next endpoint 284 | } 285 | } 286 | 287 | this.log.warn('All backup endpoints failed - backup creation unavailable') 288 | return false 289 | } else { 290 | this.log.warn('UI API not configured - backup creation skipped') 291 | return false 292 | } 293 | } catch (error) { 294 | this.log.warn(`Failed to create backup: ${error}`) 295 | this.log.warn('Continuing with updates despite backup failure - ensure you have manual backups in place') 296 | return false 297 | } 298 | } 299 | 300 | public async restartHomebridge(): Promise { 301 | this.log.info('Attempting to restart Homebridge to apply updates') 302 | 303 | try { 304 | if (this.isConfigured()) { 305 | // Try different restart endpoints with fallback strategy 306 | const restartEndpoints = [ 307 | '/api/server/restart', 308 | '/api/platform-tools/docker/restart-container', 309 | '/api/platform-tools/linux/restart-host', 310 | ] 311 | 312 | for (const endpoint of restartEndpoints) { 313 | try { 314 | await this.makeRestartCall(endpoint) 315 | this.log.info(`Homebridge restart initiated via UI API (${endpoint})`) 316 | return true 317 | } catch (error) { 318 | this.log.debug(`Restart endpoint ${endpoint} failed: ${error}`) 319 | // Continue to next endpoint 320 | } 321 | } 322 | 323 | this.log.warn('All restart endpoints failed - UI API restart unavailable') 324 | // Fallback: exit process to trigger restart by process manager 325 | this.log.info('Falling back to process exit for restart') 326 | setTimeout(() => { 327 | process.exit(0) 328 | }, 5000) // 5 second delay to allow log message to be written 329 | return true 330 | } else { 331 | // Fallback: exit process to trigger restart by process manager 332 | this.log.info('UI API not available, triggering process exit for restart') 333 | setTimeout(() => { 334 | process.exit(0) 335 | }, 5000) // 5 second delay to allow log message to be written 336 | return true 337 | } 338 | } catch (error) { 339 | this.log.error(`Failed to restart Homebridge: ${error}`) 340 | return false 341 | } 342 | } 343 | 344 | private async makeRestartCall(apiPath: string): Promise { 345 | return axios 346 | .put(this.baseUrl + apiPath, {}, { 347 | headers: { 348 | Authorization: `Bearer ${this.getToken()}`, 349 | }, 350 | httpsAgent: this.httpsAgent, 351 | }) 352 | .then((response) => { 353 | return response.data 354 | }) 355 | .catch((error) => { 356 | // At this point, we should have exhausted the retries 357 | 358 | this.log.error(`${error.code} error connecting to ${this.baseUrl + apiPath}`) 359 | 360 | return null 361 | }) 362 | } 363 | 364 | private async makeBackupCall(apiPath: string): Promise { 365 | return axios 366 | .post(this.baseUrl + apiPath, {}, { 367 | headers: { 368 | Authorization: `Bearer ${this.getToken()}`, 369 | }, 370 | httpsAgent: this.httpsAgent, 371 | timeout: 60000, // 60 second timeout for backup operations 372 | }) 373 | .then((response) => { 374 | return response.data 375 | }) 376 | .catch((error) => { 377 | // At this point, we should have exhausted the retries 378 | 379 | this.log.error(`${error.code} error connecting to ${this.baseUrl + apiPath}`) 380 | 381 | return null 382 | }) 383 | } 384 | 385 | private async makeDockerCall(apiPath: string): Promise { 386 | return axios 387 | .get(this.dockerUrl + apiPath, { 388 | httpsAgent: this.httpsAgent, 389 | lookup: this.cacheable.lookup, 390 | timeout: 60000, 391 | }) 392 | .then((response) => { 393 | return response.data 394 | }) 395 | .catch((error) => { 396 | // At this point, we should have exhausted the retries 397 | 398 | if (error.code === 'ETIMEOUT') { 399 | this.log.error(`Timeout error connecting to ${this.dockerUrl}`) 400 | } 401 | else { 402 | this.log.error(`${error.code} error connecting to ${this.dockerUrl}`) 403 | } 404 | 405 | return '{ "count": 0, "results": [] }' 406 | }) 407 | } 408 | 409 | private async makeCall(apiPath: string): Promise { 410 | return axios 411 | .get(this.baseUrl + apiPath, { 412 | headers: { 413 | Authorization: `Bearer ${this.getToken()}`, 414 | }, 415 | httpsAgent: this.httpsAgent, 416 | lookup: this.cacheable.lookup, 417 | }) 418 | .then((response) => { 419 | this.log.debug(`${this.baseUrl + apiPath}: ${JSON.stringify(response.data)}`) 420 | if (!Array.isArray(response.data)) { 421 | return [response.data] 422 | } 423 | return response.data 424 | }) 425 | .catch((error) => { 426 | // At this point, we should have exhausted the retries 427 | 428 | this.log.error(`${error.code} error connecting to ${this.baseUrl + apiPath}`) 429 | if (error.code === 'ERR_BAD_REQUEST' && error.status === 404 && apiPath === ApiPluginEndpoints.getIgnoredPluginList) { 430 | this.log.debug(`Error: ${JSON.stringify(error, undefined, 2)}`) 431 | this.log.warn(`This feature requires a newer version of Homebridge UI. Please update to the latest version.`) 432 | } 433 | 434 | return [] 435 | }) 436 | } 437 | 438 | public getToken(): string { 439 | if (this.token) { 440 | return this.token 441 | } 442 | 443 | const user = { // fake user 444 | username: '@homebridge-plugins/homebridge-plugin-update-check', 445 | name: '@homebridge-plugins/homebridge-plugin-update-check', 446 | admin: true, 447 | instanceId: 'xxxxxxx', 448 | } 449 | 450 | this.token = jwt.sign(user, this.secrets!.secretKey, { expiresIn: '1m' }) 451 | 452 | setTimeout((): void => { 453 | this.token = undefined 454 | }, 30 * 1000) 455 | 456 | return this.token as string 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable style/operator-linebreak */ 2 | /* eslint-disable object-shorthand */ 3 | /* eslint-disable perfectionist/sort-imports */ 4 | /* eslint-disable antfu/if-newline */ 5 | 6 | import type { 7 | API, 8 | Characteristic, 9 | CharacteristicValue, 10 | DynamicPlatformPlugin, 11 | HAP, 12 | Logging, 13 | PlatformAccessory, 14 | PlatformConfig, 15 | Service, 16 | WithUUID, 17 | } from 'homebridge' 18 | 19 | import { 20 | APIEvent, 21 | LogLevel, 22 | PlatformAccessoryEvent, 23 | } from 'homebridge' 24 | 25 | import type { PluginUpdatePlatformConfig } from './configTypes.js' 26 | 27 | import fs from 'node:fs' 28 | import { hostname } from 'node:os' 29 | import path from 'node:path' 30 | import { fileURLToPath } from 'node:url' 31 | 32 | import { Cron } from 'croner' 33 | 34 | // eslint-disable-next-line ts/consistent-type-imports 35 | import { InstalledPlugin, UiApi } from './ui-api.js' 36 | 37 | // ESM equivalent of __dirname 38 | const __filename = fileURLToPath(import.meta.url) 39 | // eslint-disable-next-line unused-imports/no-unused-vars 40 | const __dirname = path.dirname(__filename) 41 | 42 | let hap: HAP 43 | let Accessory: typeof PlatformAccessory 44 | 45 | const PLUGIN_NAME = '@homebridge-plugins/homebridge-plugin-update-check' 46 | const PLATFORM_NAME = 'PluginUpdate' 47 | 48 | interface SensorInfo { 49 | serviceType: WithUUID 50 | characteristicType: WithUUID Characteristic> 51 | trippedValue: CharacteristicValue 52 | untrippedValue: CharacteristicValue 53 | } 54 | 55 | class PluginUpdatePlatform implements DynamicPlatformPlugin { 56 | private readonly log: Logging 57 | private readonly api: API 58 | private readonly config: PluginUpdatePlatformConfig 59 | private readonly uiApi: UiApi 60 | 61 | private readonly isDocker: boolean 62 | private readonly sensorInfo: SensorInfo 63 | private readonly checkHB: boolean 64 | private readonly checkHBUI: boolean 65 | private readonly checkPlugins: boolean 66 | private readonly checkDocker: boolean 67 | private readonly initialCheckDelay: number 68 | private readonly autoUpdateHB: boolean 69 | private readonly autoUpdateHBUI: boolean 70 | private readonly autoUpdatePlugins: boolean 71 | private readonly allowDirectNpmUpdates: boolean 72 | private readonly autoRestartAfterUpdates: boolean 73 | private readonly respectDisabledPlugins: boolean 74 | 75 | private service?: Service 76 | 77 | private cronJob!: Cron 78 | private firstDailyRun: boolean = true 79 | 80 | private hbUpdates: string[] = [] 81 | private hbUIUpdates: string[] = [] 82 | private pluginUpdates: string[] = [] 83 | private dockerUpdates: string[] = [] 84 | 85 | constructor(log: Logging, config: PlatformConfig, api: API) { 86 | hap = api.hap 87 | Accessory = api.platformAccessory 88 | 89 | this.log = log 90 | this.config = config as PluginUpdatePlatformConfig 91 | this.api = api 92 | 93 | this.uiApi = new UiApi(this.api.user.storagePath(), this.log) 94 | this.isDocker = fs.existsSync('/homebridge/package.json') 95 | this.sensorInfo = this.getSensorInfo(this.config.sensorType) 96 | 97 | this.checkHB = this.config.checkHomebridgeUpdates ?? false 98 | this.checkHBUI = this.config.checkHomebridgeUIUpdates ?? false 99 | this.checkPlugins = this.config.checkPluginUpdates ?? false 100 | this.checkDocker = this.config.checkDockerUpdates ?? false 101 | this.initialCheckDelay = this.config.initialCheckDelay ?? 10 102 | 103 | this.autoUpdateHB = this.config.autoUpdateHomebridge ?? false 104 | this.autoUpdateHBUI = this.config.autoUpdateHomebridgeUI ?? false 105 | this.autoUpdatePlugins = this.config.autoUpdatePlugins ?? false 106 | this.allowDirectNpmUpdates = this.config.allowDirectNpmUpdates ?? false 107 | this.autoRestartAfterUpdates = this.config.autoRestartAfterUpdates ?? false 108 | this.respectDisabledPlugins = this.config.respectDisabledPlugins ?? true 109 | 110 | api.on(APIEvent.DID_FINISH_LAUNCHING, this.addUpdateAccessory.bind(this)) 111 | } 112 | 113 | addUpdateAccessory(): void { 114 | if (!this.service) { 115 | const uuid = hap.uuid.generate(PLATFORM_NAME) 116 | const newAccessory = new Accessory('Plugin Update Check', uuid) 117 | 118 | newAccessory.addService(this.sensorInfo.serviceType as unknown as Service) 119 | 120 | this.configureAccessory(newAccessory) 121 | 122 | this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [newAccessory]) 123 | } 124 | 125 | setTimeout(() => { 126 | this.doCheck() 127 | this.firstDailyRun = false 128 | }, this.initialCheckDelay * 1000) 129 | 130 | const timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone 131 | this.setupFirstDailyRunResetCron(timezone) 132 | this.setupUpdatesCron(timezone) 133 | } 134 | 135 | setupFirstDailyRunResetCron(timezone: string): void { 136 | const cronScheduleAtMidnight = '0 0 * * *' 137 | 138 | this.cronJob = new Cron( 139 | cronScheduleAtMidnight, 140 | { 141 | name: `First Daily Run Reset Cron Job`, 142 | timezone: timezone, 143 | }, 144 | async () => { 145 | this.firstDailyRun = true 146 | this.log.debug(`Reset "firstDailyRun" to ${this.firstDailyRun}`) 147 | }, 148 | ) 149 | } 150 | 151 | setupUpdatesCron(timezone: string): void { 152 | const cronScheduleFiveAfterTheHour = '5 * * * *' 153 | 154 | this.cronJob = new Cron( 155 | cronScheduleFiveAfterTheHour, 156 | { 157 | name: `Updates Available Cron Job`, 158 | timezone: timezone, 159 | }, 160 | async () => { 161 | this.log.debug(`Is first daily run: ${this.firstDailyRun}`) 162 | this.doCheck() 163 | this.firstDailyRun = false 164 | this.log.debug(`Cleared "firstDailyRun" to ${this.firstDailyRun}`) 165 | }, 166 | ) 167 | } 168 | 169 | async checkUi(): Promise { 170 | this.log.debug('Searching for available updates ...') 171 | 172 | let logLevel = (this.firstDailyRun === true) ? LogLevel.INFO : LogLevel.DEBUG 173 | const updatesAvailable: InstalledPlugin[] = [] 174 | 175 | // Get ignored plugins from API if respectDisabledPlugins is enabled 176 | let ignoredPlugins: string[] = [] 177 | if (this.respectDisabledPlugins) { 178 | try { 179 | ignoredPlugins = await this.uiApi.getIgnoredPlugins() 180 | this.log.debug(`Retrieved ${ignoredPlugins.length} ignored plugin(s) from homebridge-config-ui-x: ${ignoredPlugins.join(', ')}`) 181 | } catch (error) { 182 | this.log.warn(`Failed to retrieve ignored plugins list, filtering disabled: ${error}`) 183 | ignoredPlugins = [] 184 | } 185 | } else { 186 | this.log.debug('respectDisabledPlugins is disabled, skipping plugin filtering') 187 | } 188 | 189 | if (this.checkHB) { 190 | const homebridge = await this.uiApi.getHomebridge() 191 | 192 | if (homebridge.updateAvailable) { 193 | // Check if homebridge core updates are ignored 194 | const isIgnored = this.respectDisabledPlugins && ignoredPlugins.includes('homebridge') 195 | 196 | if (!isIgnored) { 197 | updatesAvailable.push(homebridge) 198 | 199 | const version: string = homebridge.latestVersion 200 | 201 | if (this.hbUpdates.length === 0 || !this.hbUpdates.includes(version)) logLevel = LogLevel.INFO 202 | this.log.log(logLevel, `Homebridge update available: ${version}`) 203 | 204 | this.hbUpdates = [version] 205 | } else { 206 | this.log.debug(`Ignoring Homebridge core update: ${homebridge.latestVersion} (update notifications disabled in homebridge-config-ui-x)`) 207 | } 208 | } 209 | } 210 | 211 | if (this.checkHBUI || this.checkPlugins) { 212 | const plugins = await this.uiApi.getPlugins() 213 | 214 | if (this.checkHBUI) { 215 | const homebridgeUiPlugins = plugins.filter(plugin => plugin.name === 'homebridge-config-ui-x') 216 | 217 | // Only one plugin is returned 218 | homebridgeUiPlugins.forEach((homebridgeUI) => { 219 | if (homebridgeUI.updateAvailable) { 220 | // Check if homebridge-config-ui-x updates are ignored 221 | const isIgnored = this.respectDisabledPlugins && ignoredPlugins.includes('homebridge-config-ui-x') 222 | 223 | if (!isIgnored) { 224 | updatesAvailable.push(homebridgeUI) 225 | 226 | const version: string = homebridgeUI.latestVersion 227 | 228 | if (this.hbUIUpdates.length === 0 || !this.hbUIUpdates.includes(version)) logLevel = LogLevel.INFO 229 | this.log.log(logLevel, `Homebridge UI update available: ${version}`) 230 | 231 | this.hbUIUpdates = [version] 232 | } else { 233 | this.log.debug(`Ignoring Homebridge UI update: ${homebridgeUI.latestVersion} (update notifications disabled in homebridge-config-ui-x)`) 234 | } 235 | } 236 | }) 237 | } 238 | 239 | if (this.checkPlugins) { 240 | this.log.debug(`Checking ${plugins.length} plugins for updates (respectDisabledPlugins: ${this.respectDisabledPlugins})`) 241 | 242 | const filteredPlugins = plugins.filter((plugin) => { 243 | // Always exclude homebridge-config-ui-x 244 | if (plugin.name === 'homebridge-config-ui-x') { 245 | return false 246 | } 247 | 248 | // If respectDisabledPlugins is enabled, check API ignored list 249 | if (this.respectDisabledPlugins) { 250 | if (ignoredPlugins.includes(plugin.name)) { 251 | this.log.debug(`Filtering out plugin ${plugin.name} (ignored in homebridge-config-ui-x)`) 252 | return false 253 | } 254 | } 255 | 256 | return true 257 | }) 258 | 259 | this.log.debug(`After filtering: ${filteredPlugins.length} plugins to check for updates`) 260 | 261 | filteredPlugins.forEach((plugin) => { 262 | if (plugin.updateAvailable) { 263 | updatesAvailable.push(plugin) 264 | 265 | const version: string = plugin.latestVersion 266 | 267 | if (this.pluginUpdates.length === 0 || !this.pluginUpdates.includes(version)) logLevel = LogLevel.INFO 268 | this.log.log(logLevel, `Homebridge plugin update available: ${plugin.name} ${plugin.latestVersion}`) 269 | 270 | this.pluginUpdates.push(version) 271 | } 272 | }) 273 | 274 | // Log ignored plugins if any updates are available for them (only when respectDisabledPlugins is enabled) 275 | if (this.respectDisabledPlugins) { 276 | const ignoredWithUpdates = plugins.filter(plugin => 277 | plugin.name !== 'homebridge-config-ui-x' && 278 | plugin.updateAvailable && 279 | ignoredPlugins.includes(plugin.name), 280 | ) 281 | if (ignoredWithUpdates.length > 0) { 282 | this.log.info(`Ignoring updates for ${ignoredWithUpdates.length} plugin(s): ${ignoredWithUpdates.map(p => p.name).join(', ')}`) 283 | } 284 | } 285 | } 286 | } 287 | 288 | if (this.isDocker && this.checkDocker) { 289 | const docker = await this.uiApi.getDocker() 290 | 291 | if (docker.updateAvailable) { 292 | updatesAvailable.push(docker) 293 | 294 | const version: string = docker.latestVersion 295 | 296 | if (this.dockerUpdates.length === 0 || !this.dockerUpdates.includes(version)) logLevel = LogLevel.INFO 297 | this.log.log(logLevel, `Docker update available: ${version}`) 298 | 299 | this.dockerUpdates = [version] 300 | } 301 | } 302 | 303 | this.log.log(logLevel, `Found ${updatesAvailable.length} available update(s)`) 304 | 305 | // Provide additional diagnostic information in debug mode 306 | if (this.respectDisabledPlugins && ignoredPlugins.length > 0) { 307 | this.log.debug(`Filtering enabled with ${ignoredPlugins.length} ignored plugins: ${ignoredPlugins.join(', ')}`) 308 | } else if (this.respectDisabledPlugins) { 309 | this.log.debug('Filtering enabled but no ignored plugins found') 310 | } else { 311 | this.log.debug('Plugin filtering is disabled (respectDisabledPlugins: false)') 312 | } 313 | 314 | return updatesAvailable.length 315 | } 316 | 317 | doCheck(): void { 318 | this.checkUi() 319 | .then((updates) => { 320 | this.service?.setCharacteristic(this.sensorInfo.characteristicType, updates ? this.sensorInfo.trippedValue : this.sensorInfo.untrippedValue) 321 | }) 322 | .catch((ex) => { 323 | this.log.error(ex) 324 | }) 325 | .finally(() => { 326 | this.log.debug('Check complete') 327 | }) 328 | } 329 | 330 | checkService(accessory: PlatformAccessory, serviceType: WithUUID): boolean { 331 | const service = accessory.getService(serviceType) 332 | if (this.sensorInfo.serviceType === serviceType) { 333 | if (service) { 334 | this.service = service 335 | } else { 336 | this.service = accessory.addService(serviceType as unknown as Service) 337 | } 338 | return true 339 | } else { 340 | if (service) { 341 | accessory.removeService(service) 342 | } 343 | return false 344 | } 345 | } 346 | 347 | configureAccessory(accessory: PlatformAccessory): void { 348 | accessory.on(PlatformAccessoryEvent.IDENTIFY, () => { 349 | this.log(`${accessory.displayName} identify requested!`) 350 | }) 351 | 352 | const accInfo = accessory.getService(hap.Service.AccessoryInformation) 353 | if (accInfo) { 354 | accInfo 355 | .setCharacteristic(hap.Characteristic.Manufacturer, 'Homebridge') 356 | .setCharacteristic(hap.Characteristic.Model, 'Plugin Update Check') 357 | .setCharacteristic(hap.Characteristic.SerialNumber, hostname()) 358 | } 359 | 360 | this.checkService(accessory, hap.Service.MotionSensor) 361 | this.checkService(accessory, hap.Service.ContactSensor) 362 | this.checkService(accessory, hap.Service.OccupancySensor) 363 | this.checkService(accessory, hap.Service.SmokeSensor) 364 | this.checkService(accessory, hap.Service.LeakSensor) 365 | this.checkService(accessory, hap.Service.LightSensor) 366 | this.checkService(accessory, hap.Service.HumiditySensor) 367 | this.checkService(accessory, hap.Service.CarbonMonoxideSensor) 368 | this.checkService(accessory, hap.Service.CarbonDioxideSensor) 369 | this.checkService(accessory, hap.Service.AirQualitySensor) 370 | 371 | /* const motionService = accessory.getService(hap.Service.MotionSensor); 372 | const contactService = accessory.getService(hap.Service.ContactSensor); 373 | const occupancyService = accessory.getService(hap.Service.OccupancySensor); 374 | const smokeService = accessory.getService(hap.Service.SmokeSensor); 375 | const leakService = accessory.getService(hap.Service.LeakSensor); 376 | const lightService = accessory.getService(hap.Service.LightSensor); 377 | const humidityService = accessory.getService(hap.Service.HumiditySensor); 378 | const monoxideService = accessory.getService(hap.Service.CarbonMonoxideSensor); 379 | const dioxideService = accessory.getService(hap.Service.CarbonDioxideSensor); 380 | const airService = accessory.getService(hap.Service.AirQualitySensor); 381 | 382 | if (this.sensorInfo.serviceType == hap.Service.MotionSensor) { 383 | this.service = motionService; 384 | } else if (motionService) { 385 | accessory.removeService(motionService); 386 | } 387 | if (this.sensorInfo.serviceType == hap.Service.ContactSensor) { 388 | this.service = contactService; 389 | } else if (contactService) { 390 | accessory.removeService(contactService); 391 | } 392 | if (this.sensorInfo.serviceType == hap.Service.OccupancySensor) { 393 | this.service = occupancyService; 394 | } else if (occupancyService) { 395 | accessory.removeService(occupancyService); 396 | } 397 | if (this.sensorInfo.serviceType == hap.Service.SmokeSensor) { 398 | this.service = smokeService; 399 | } else if (smokeService) { 400 | accessory.removeService(smokeService); 401 | } 402 | if (this.sensorInfo.serviceType == hap.Service.LeakSensor) { 403 | this.service = leakService; 404 | } else if (leakService) { 405 | accessory.removeService(leakService); 406 | } 407 | if (this.sensorInfo.serviceType == hap.Service.LightSensor) { 408 | this.service = lightService; 409 | } else if (lightService) { 410 | accessory.removeService(lightService); 411 | } 412 | if (this.sensorInfo.serviceType == hap.Service.HumiditySensor) { 413 | this.service = humidityService; 414 | } else if (humidityService) { 415 | accessory.removeService(humidityService); 416 | } 417 | if (this.sensorInfo.serviceType == hap.Service.CarbonMonoxideSensor) { 418 | this.service = monoxideService; 419 | } else if (monoxideService) { 420 | accessory.removeService(monoxideService); 421 | } 422 | if (this.sensorInfo.serviceType == hap.Service.CarbonDioxideSensor) { 423 | this.service = dioxideService; 424 | } else if (dioxideService) { 425 | accessory.removeService(dioxideService); 426 | } 427 | if (this.sensorInfo.serviceType == hap.Service.AirQualitySensor) { 428 | this.service = airService; 429 | } else if (airService) { 430 | accessory.removeService(airService); 431 | } */ 432 | 433 | this.service?.setCharacteristic(this.sensorInfo.characteristicType, this.sensorInfo.untrippedValue) 434 | } 435 | 436 | getSensorInfo(sensorType?: string): SensorInfo { 437 | switch (sensorType?.toLowerCase()) { 438 | case 'contact': 439 | return { 440 | serviceType: hap.Service.ContactSensor, 441 | characteristicType: hap.Characteristic.ContactSensorState, 442 | untrippedValue: 0, 443 | trippedValue: 1, 444 | } 445 | case 'occupancy': 446 | return { 447 | serviceType: hap.Service.OccupancySensor, 448 | characteristicType: hap.Characteristic.OccupancyDetected, 449 | untrippedValue: 0, 450 | trippedValue: 1, 451 | } 452 | case 'smoke': 453 | return { 454 | serviceType: hap.Service.SmokeSensor, 455 | characteristicType: hap.Characteristic.SmokeDetected, 456 | untrippedValue: 0, 457 | trippedValue: 1, 458 | } 459 | case 'leak': 460 | return { 461 | serviceType: hap.Service.LeakSensor, 462 | characteristicType: hap.Characteristic.LeakDetected, 463 | untrippedValue: 0, 464 | trippedValue: 1, 465 | } 466 | case 'light': 467 | return { 468 | serviceType: hap.Service.LightSensor, 469 | characteristicType: hap.Characteristic.CurrentAmbientLightLevel, 470 | untrippedValue: 0.0001, 471 | trippedValue: 100000, 472 | } 473 | case 'humidity': 474 | return { 475 | serviceType: hap.Service.HumiditySensor, 476 | characteristicType: hap.Characteristic.CurrentRelativeHumidity, 477 | untrippedValue: 0, 478 | trippedValue: 100, 479 | } 480 | case 'monoxide': 481 | return { 482 | serviceType: hap.Service.CarbonMonoxideSensor, 483 | characteristicType: hap.Characteristic.CarbonMonoxideDetected, 484 | untrippedValue: 0, 485 | trippedValue: 1, 486 | } 487 | case 'dioxide': 488 | return { 489 | serviceType: hap.Service.CarbonDioxideSensor, 490 | characteristicType: hap.Characteristic.CarbonDioxideDetected, 491 | untrippedValue: 0, 492 | trippedValue: 1, 493 | } 494 | case 'air': 495 | return { 496 | serviceType: hap.Service.AirQualitySensor, 497 | characteristicType: hap.Characteristic.AirQuality, 498 | untrippedValue: 1, 499 | trippedValue: 5, 500 | } 501 | case 'motion': 502 | default: 503 | return { 504 | serviceType: hap.Service.MotionSensor, 505 | characteristicType: hap.Characteristic.MotionDetected, 506 | untrippedValue: false, 507 | trippedValue: true, 508 | } 509 | } 510 | } 511 | } 512 | 513 | // Register our platform with homebridge. 514 | export default (api: API): void => { 515 | api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, PluginUpdatePlatform) 516 | } 517 | -------------------------------------------------------------------------------- /docs/assets/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings.","hierarchy_expand":"Expand","hierarchy_collapse":"Collapse","folder":"Folder","search_index_not_available":"The search index is not available","search_no_results_found_for_0":"No results found for {0}","kind_1":"Project","kind_2":"Module","kind_4":"Namespace","kind_8":"Enumeration","kind_16":"Enumeration Member","kind_32":"Variable","kind_64":"Function","kind_128":"Class","kind_256":"Interface","kind_512":"Constructor","kind_1024":"Property","kind_2048":"Method","kind_4096":"Call Signature","kind_8192":"Index Signature","kind_16384":"Constructor Signature","kind_32768":"Parameter","kind_65536":"Type Literal","kind_131072":"Type Parameter","kind_262144":"Accessor","kind_524288":"Get Signature","kind_1048576":"Set Signature","kind_2097152":"Type Alias","kind_4194304":"Reference","kind_8388608":"Document"}; 3 | "use strict";(()=>{var Ke=Object.create;var he=Object.defineProperty;var Ge=Object.getOwnPropertyDescriptor;var Ze=Object.getOwnPropertyNames;var Xe=Object.getPrototypeOf,Ye=Object.prototype.hasOwnProperty;var et=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var tt=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ze(e))!Ye.call(t,i)&&i!==n&&he(t,i,{get:()=>e[i],enumerable:!(r=Ge(e,i))||r.enumerable});return t};var nt=(t,e,n)=>(n=t!=null?Ke(Xe(t)):{},tt(e||!t||!t.__esModule?he(n,"default",{value:t,enumerable:!0}):n,t));var ye=et((me,ge)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=(function(e){return function(n){e.console&&console.warn&&console.warn(n)}})(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,l],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. 4 | `,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(oc?d+=2:a==c&&(n+=r[l+1]*i[d+1],l+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var c=s.node.edges["*"];else{var c=new t.TokenSet;s.node.edges["*"]=c}if(s.str.length==0&&(c.final=!0),i.push({node:c,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),f=s.str.charAt(1),p;f in s.node.edges?p=s.node.edges[f]:(p=new t.TokenSet,s.node.edges[f]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),c=0;c1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},(function(e,n){typeof define=="function"&&define.amd?define(n):typeof me=="object"?ge.exports=n():e.lunr=n()})(this,function(){return t})})()});var M,G={getItem(){return null},setItem(){}},K;try{K=localStorage,M=K}catch{K=G,M=G}var S={getItem:t=>M.getItem(t),setItem:(t,e)=>M.setItem(t,e),disableWritingLocalStorage(){M=G},disable(){localStorage.clear(),M=G},enable(){M=K}};window.TypeDoc||={disableWritingLocalStorage(){S.disableWritingLocalStorage()},disableLocalStorage:()=>{S.disable()},enableLocalStorage:()=>{S.enable()}};window.translations||={copy:"Copy",copied:"Copied!",normally_hidden:"This member is normally hidden due to your filter settings.",hierarchy_expand:"Expand",hierarchy_collapse:"Collapse",search_index_not_available:"The search index is not available",search_no_results_found_for_0:"No results found for {0}",folder:"Folder",kind_1:"Project",kind_2:"Module",kind_4:"Namespace",kind_8:"Enumeration",kind_16:"Enumeration Member",kind_32:"Variable",kind_64:"Function",kind_128:"Class",kind_256:"Interface",kind_512:"Constructor",kind_1024:"Property",kind_2048:"Method",kind_4096:"Call Signature",kind_8192:"Index Signature",kind_16384:"Constructor Signature",kind_32768:"Parameter",kind_65536:"Type Literal",kind_131072:"Type Parameter",kind_262144:"Accessor",kind_524288:"Get Signature",kind_1048576:"Set Signature",kind_2097152:"Type Alias",kind_4194304:"Reference",kind_8388608:"Document"};var pe=[];function X(t,e){pe.push({selector:e,constructor:t})}var Z=class{alwaysVisibleMember=null;constructor(){this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){pe.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!rt(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function rt(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var fe=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var Ie=nt(ye(),1);async function R(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0)),r=new Blob([e]).stream().pipeThrough(new DecompressionStream("deflate")),i=await new Response(r).text();return JSON.parse(i)}var Y="closing",ae="tsd-overlay";function it(){let t=Math.abs(window.innerWidth-document.documentElement.clientWidth);document.body.style.overflow="hidden",document.body.style.paddingRight=`${t}px`}function st(){document.body.style.removeProperty("overflow"),document.body.style.removeProperty("padding-right")}function xe(t,e){t.addEventListener("animationend",()=>{t.classList.contains(Y)&&(t.classList.remove(Y),document.getElementById(ae)?.remove(),t.close(),st())}),t.addEventListener("cancel",n=>{n.preventDefault(),ve(t)}),e?.closeOnClick&&document.addEventListener("click",n=>{t.open&&!t.contains(n.target)&&ve(t)},!0)}function Ee(t){if(t.open)return;let e=document.createElement("div");e.id=ae,document.body.appendChild(e),t.showModal(),it()}function ve(t){if(!t.open)return;document.getElementById(ae)?.classList.add(Y),t.classList.add(Y)}var I=class{el;app;constructor(e){this.el=e.el,this.app=e.app}};var be=document.head.appendChild(document.createElement("style"));be.dataset.for="filters";var le={};function we(t){for(let e of t.split(/\s+/))if(le.hasOwnProperty(e)&&!le[e])return!0;return!1}var ee=class extends I{key;value;constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),be.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } 5 | `,this.app.updateIndexVisibility()}fromLocalStorage(){let e=S.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){S.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),le[`tsd-is-${this.el.name}`]=this.value,this.app.filterChanged(),this.app.updateIndexVisibility()}};var Le=0;async function Se(t,e){if(!window.searchData)return;let n=await R(window.searchData);t.data=n,t.index=Ie.Index.load(n.index),e.innerHTML=""}function _e(){let t=document.getElementById("tsd-search-trigger"),e=document.getElementById("tsd-search"),n=document.getElementById("tsd-search-input"),r=document.getElementById("tsd-search-results"),i=document.getElementById("tsd-search-script"),s=document.getElementById("tsd-search-status");if(!(t&&e&&n&&r&&i&&s))throw new Error("Search controls missing");let o={base:document.documentElement.dataset.base};o.base.endsWith("/")||(o.base+="/"),i.addEventListener("error",()=>{let a=window.translations.search_index_not_available;Pe(s,a)}),i.addEventListener("load",()=>{Se(o,s)}),Se(o,s),ot({trigger:t,searchEl:e,results:r,field:n,status:s},o)}function ot(t,e){let{field:n,results:r,searchEl:i,status:s,trigger:o}=t;xe(i,{closeOnClick:!0});function a(){Ee(i),n.setSelectionRange(0,n.value.length)}o.addEventListener("click",a),n.addEventListener("input",fe(()=>{at(r,n,s,e)},200)),n.addEventListener("keydown",l=>{if(r.childElementCount===0||l.ctrlKey||l.metaKey||l.altKey)return;let d=n.getAttribute("aria-activedescendant"),f=d?document.getElementById(d):null;if(f){let p=!1,v=!1;switch(l.key){case"Home":case"End":case"ArrowLeft":case"ArrowRight":v=!0;break;case"ArrowDown":case"ArrowUp":p=l.shiftKey;break}(p||v)&&ke(n)}if(!l.shiftKey)switch(l.key){case"Enter":f?.querySelector("a")?.click();break;case"ArrowUp":Te(r,n,f,-1),l.preventDefault();break;case"ArrowDown":Te(r,n,f,1),l.preventDefault();break}});function c(){ke(n)}n.addEventListener("change",c),n.addEventListener("blur",c),n.addEventListener("click",c),document.body.addEventListener("keydown",l=>{if(l.altKey||l.metaKey||l.shiftKey)return;let d=l.ctrlKey&&l.key==="k",f=!l.ctrlKey&&!ut()&&l.key==="/";(d||f)&&(l.preventDefault(),a())})}function at(t,e,n,r){if(!r.index||!r.data)return;t.innerHTML="",n.innerHTML="",Le+=1;let i=e.value.trim(),s;if(i){let a=i.split(" ").map(c=>c.length?`*${c}*`:"").join(" ");s=r.index.search(a).filter(({ref:c})=>{let l=r.data.rows[Number(c)].classes;return!l||!we(l)})}else s=[];if(s.length===0&&i){let a=window.translations.search_no_results_found_for_0.replace("{0}",` "${te(i)}" `);Pe(n,a);return}for(let a=0;ac.score-a.score);let o=Math.min(10,s.length);for(let a=0;a`,f=Ce(c.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(f+=` (score: ${s[a].score.toFixed(2)})`),c.parent&&(f=` 6 | ${Ce(c.parent,i)}.${f}`);let p=document.createElement("li");p.id=`tsd-search:${Le}-${a}`,p.role="option",p.ariaSelected="false",p.classList.value=c.classes??"";let v=document.createElement("a");v.tabIndex=-1,v.href=r.base+c.url,v.innerHTML=d+`${f}`,p.append(v),t.appendChild(p)}}function Te(t,e,n,r){let i;if(r===1?i=n?.nextElementSibling||t.firstElementChild:i=n?.previousElementSibling||t.lastElementChild,i!==n){if(!i||i.role!=="option"){console.error("Option missing");return}i.ariaSelected="true",i.scrollIntoView({behavior:"smooth",block:"nearest"}),e.setAttribute("aria-activedescendant",i.id),n?.setAttribute("aria-selected","false")}}function ke(t){let e=t.getAttribute("aria-activedescendant");(e?document.getElementById(e):null)?.setAttribute("aria-selected","false"),t.setAttribute("aria-activedescendant","")}function Ce(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(te(t.substring(s,o)),`${te(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(te(t.substring(s))),i.join("")}var lt={"&":"&","<":"<",">":">","'":"'",'"':"""};function te(t){return t.replace(/[&<>"'"]/g,e=>lt[e])}function Pe(t,e){t.innerHTML=e?`
      ${e}
      `:""}var ct=["button","checkbox","file","hidden","image","radio","range","reset","submit"];function ut(){let t=document.activeElement;return t?t.isContentEditable||t.tagName==="TEXTAREA"||t.tagName==="SEARCH"?!0:t.tagName==="INPUT"&&!ct.includes(t.type):!1}var D="mousedown",Me="mousemove",$="mouseup",ne={x:0,y:0},Qe=!1,ce=!1,dt=!1,F=!1,Oe=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(Oe?"is-mobile":"not-mobile");Oe&&"ontouchstart"in document.documentElement&&(dt=!0,D="touchstart",Me="touchmove",$="touchend");document.addEventListener(D,t=>{ce=!0,F=!1;let e=D=="touchstart"?t.targetTouches[0]:t;ne.y=e.pageY||0,ne.x=e.pageX||0});document.addEventListener(Me,t=>{if(ce&&!F){let e=D=="touchstart"?t.targetTouches[0]:t,n=ne.x-(e.pageX||0),r=ne.y-(e.pageY||0);F=Math.sqrt(n*n+r*r)>10}});document.addEventListener($,()=>{ce=!1});document.addEventListener("click",t=>{Qe&&(t.preventDefault(),t.stopImmediatePropagation(),Qe=!1)});var re=class extends I{active;className;constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener($,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(D,n=>this.onDocumentPointerDown(n)),document.addEventListener($,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){F||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!F&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var ue=new Map,de=class{open;accordions=[];key;constructor(e,n){this.key=e,this.open=n}add(e){this.accordions.push(e),e.open=this.open,e.addEventListener("toggle",()=>{this.toggle(e.open)})}toggle(e){for(let n of this.accordions)n.open=e;S.setItem(this.key,e.toString())}},ie=class extends I{constructor(e){super(e);let n=this.el.querySelector("summary"),r=n.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)});let i=`tsd-accordion-${n.dataset.key??n.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`,s;if(ue.has(i))s=ue.get(i);else{let o=S.getItem(i),a=o?o==="true":this.el.open;s=new de(i,a),ue.set(i,s)}s.add(this.el)}};function He(t){let e=S.getItem("tsd-theme")||"os";t.value=e,Ae(e),t.addEventListener("change",()=>{S.setItem("tsd-theme",t.value),Ae(t.value)})}function Ae(t){document.documentElement.dataset.theme=t}var se;function Ne(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Re),Re())}async function Re(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let e=await R(window.navigationData);se=document.documentElement.dataset.base,se.endsWith("/")||(se+="/"),t.innerHTML="";for(let n of e)Be(n,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Be(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',De(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let c=a.appendChild(document.createElement("ul"));c.className="tsd-nested-navigation";for(let l of t.children)Be(l,c,i)}else De(t,r,t.class)}function De(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));if(r.href=se+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&(r.classList.add("current"),r.ariaCurrent="page"),t.kind){let i=window.translations[`kind_${t.kind}`].replaceAll('"',""");r.innerHTML=``}r.appendChild(Fe(t.text,document.createElement("span")))}else{let r=e.appendChild(document.createElement("span")),i=window.translations.folder.replaceAll('"',""");r.innerHTML=``,r.appendChild(Fe(t.text,document.createElement("span")))}}function Fe(t,e){let n=t.split(/(?<=[^A-Z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[_-])(?=[^_-])/);for(let r=0;r{let i=r.target;for(;i.parentElement&&i.parentElement.tagName!="LI";)i=i.parentElement;i.dataset.dropdown&&(i.dataset.dropdown=String(i.dataset.dropdown!=="true"))});let t=new Map,e=new Set;for(let r of document.querySelectorAll(".tsd-full-hierarchy [data-refl]")){let i=r.querySelector("ul");t.has(r.dataset.refl)?e.add(r.dataset.refl):i&&t.set(r.dataset.refl,i)}for(let r of e)n(r);function n(r){let i=t.get(r).cloneNode(!0);i.querySelectorAll("[id]").forEach(s=>{s.removeAttribute("id")}),i.querySelectorAll("[data-dropdown]").forEach(s=>{s.dataset.dropdown="false"});for(let s of document.querySelectorAll(`[data-refl="${r}"]`)){let o=gt(),a=s.querySelector("ul");s.insertBefore(o,a),o.dataset.dropdown=String(!!a),a||s.appendChild(i.cloneNode(!0))}}}function pt(){let t=document.getElementById("tsd-hierarchy-script");t&&(t.addEventListener("load",Ve),Ve())}async function Ve(){let t=document.querySelector(".tsd-panel.tsd-hierarchy:has(h4 a)");if(!t||!window.hierarchyData)return;let e=+t.dataset.refl,n=await R(window.hierarchyData),r=t.querySelector("ul"),i=document.createElement("ul");if(i.classList.add("tsd-hierarchy"),ft(i,n,e),r.querySelectorAll("li").length==i.querySelectorAll("li").length)return;let s=document.createElement("span");s.classList.add("tsd-hierarchy-toggle"),s.textContent=window.translations.hierarchy_expand,t.querySelector("h4 a")?.insertAdjacentElement("afterend",s),s.insertAdjacentText("beforebegin",", "),s.addEventListener("click",()=>{s.textContent===window.translations.hierarchy_expand?(r.insertAdjacentElement("afterend",i),r.remove(),s.textContent=window.translations.hierarchy_collapse):(i.insertAdjacentElement("afterend",r),i.remove(),s.textContent=window.translations.hierarchy_expand)})}function ft(t,e,n){let r=e.roots.filter(i=>mt(e,i,n));for(let i of r)t.appendChild(je(e,i,n))}function je(t,e,n,r=new Set){if(r.has(e))return;r.add(e);let i=t.reflections[e],s=document.createElement("li");if(s.classList.add("tsd-hierarchy-item"),e===n){let o=s.appendChild(document.createElement("span"));o.textContent=i.name,o.classList.add("tsd-hierarchy-target")}else{for(let a of i.uniqueNameParents||[]){let c=t.reflections[a],l=s.appendChild(document.createElement("a"));l.textContent=c.name,l.href=oe+c.url,l.className=c.class+" tsd-signature-type",s.append(document.createTextNode("."))}let o=s.appendChild(document.createElement("a"));o.textContent=t.reflections[e].name,o.href=oe+i.url,o.className=i.class+" tsd-signature-type"}if(i.children){let o=s.appendChild(document.createElement("ul"));o.classList.add("tsd-hierarchy");for(let a of i.children){let c=je(t,a,n,r);c&&o.appendChild(c)}}return r.delete(e),s}function mt(t,e,n){if(e===n)return!0;let r=new Set,i=[t.reflections[e]];for(;i.length;){let s=i.pop();if(!r.has(s)){r.add(s);for(let o of s.children||[]){if(o===n)return!0;i.push(t.reflections[o])}}}return!1}function gt(){let t=document.createElementNS("http://www.w3.org/2000/svg","svg");return t.setAttribute("width","20"),t.setAttribute("height","20"),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.innerHTML='',t}X(re,"a[data-toggle]");X(ie,".tsd-accordion");X(ee,".tsd-filter-item input[type=checkbox]");var qe=document.getElementById("tsd-theme");qe&&He(qe);var yt=new Z;Object.defineProperty(window,"app",{value:yt});_e();Ne();$e();"virtualKeyboard"in navigator&&(navigator.virtualKeyboard.overlaysContent=!0);})(); 7 | /*! Bundled license information: 8 | 9 | lunr/lunr.js: 10 | (** 11 | * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 12 | * Copyright (C) 2020 Oliver Nightingale 13 | * @license MIT 14 | *) 15 | (*! 16 | * lunr.utils 17 | * Copyright (C) 2020 Oliver Nightingale 18 | *) 19 | (*! 20 | * lunr.Set 21 | * Copyright (C) 2020 Oliver Nightingale 22 | *) 23 | (*! 24 | * lunr.tokenizer 25 | * Copyright (C) 2020 Oliver Nightingale 26 | *) 27 | (*! 28 | * lunr.Pipeline 29 | * Copyright (C) 2020 Oliver Nightingale 30 | *) 31 | (*! 32 | * lunr.Vector 33 | * Copyright (C) 2020 Oliver Nightingale 34 | *) 35 | (*! 36 | * lunr.stemmer 37 | * Copyright (C) 2020 Oliver Nightingale 38 | * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt 39 | *) 40 | (*! 41 | * lunr.stopWordFilter 42 | * Copyright (C) 2020 Oliver Nightingale 43 | *) 44 | (*! 45 | * lunr.trimmer 46 | * Copyright (C) 2020 Oliver Nightingale 47 | *) 48 | (*! 49 | * lunr.TokenSet 50 | * Copyright (C) 2020 Oliver Nightingale 51 | *) 52 | (*! 53 | * lunr.Index 54 | * Copyright (C) 2020 Oliver Nightingale 55 | *) 56 | (*! 57 | * lunr.Builder 58 | * Copyright (C) 2020 Oliver Nightingale 59 | *) 60 | */ 61 | --------------------------------------------------------------------------------