├── .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 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 | [ ](https://www.npmjs.com/package/@homebridge-plugins/homebridge-plugin-update-check) [](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 default : ( api : API ) => void
Type Declaration ( api : API ) : void 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 |
5 |
6 |
7 |
Thank you for installing homebridge-plugin-update-check
8 |
9 | Continue →
10 |
11 |
12 |
17 |
18 | Plugin is currently disabled
19 | Enable
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 | Device Name
32 |
33 |
34 |
35 |
36 |
37 | Serial Number
38 |
39 |
40 |
41 | Model
42 |
43 |
44 |
45 | Firmware Version
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Thank you for using homebridge-plugin-update-check
54 |
The links below will take you to our GitHub wiki
55 |
Setup
56 |
79 |
Features
80 |
90 |
Help/About
91 |
105 |
106 |
--------------------------------------------------------------------------------
/docs/assets/icons.svg:
--------------------------------------------------------------------------------
1 | M M N E P V F C I C P M F P C P T T A A A T R
--------------------------------------------------------------------------------
/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 = `M M N E P V F C I C P M F P C P T T A A A T R `;
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
2 |
A Homebridge plugin for checking for updates to Homebridge and plugins.
3 |
Installation
4 | Install Homebridge using the official instructions .
5 | Install this plugin using: sudo npm install -g homebridge-plugin-update-check.
6 | Update your configuration file. See sample config.json snippet below.
7 |
8 |
Configuration 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 | Copy
11 |
12 |
Fields
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 | Automatically create a backup before performing any updates (when homebridge-config-ui-x is available)
30 | 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 |
36 | Automatically restart Homebridge after successful updates (when autoRestartAfterUpdates is enabled)
37 | Monitor for failures and provide notifications via a configurable failure sensor (type set by failureSensorType)
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 |
--------------------------------------------------------------------------------