├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── cli
└── nb.js
├── config.schema.json
├── homebridge-nb.png
├── index.js
├── lib
├── NbAccessory.js
├── NbAccessory
│ ├── Bridge.js
│ ├── DoorSensor.js
│ ├── Keypad.js
│ ├── Opener.js
│ └── SmartLock.js
├── NbPlatform.js
├── NbService.js
└── NbService
│ ├── Bridge.js
│ ├── DoorBell.js
│ ├── DoorSensor.js
│ ├── Latch.js
│ ├── Opener.js
│ └── SmartLock.js
├── nb.png
├── package-lock.json
└── package.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ebaauw]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ["https://www.paypal.me/ebaauw/EUR"]
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # Homebridge NB
7 | [](https://www.npmjs.com/package/homebridge-nb)
8 | [](https://www.npmjs.com/package/homebridge-nb)
9 | [](https://discord.gg/yGvADWt)
10 | [](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
11 |
12 | [](https://github.com/ebaauw/homebridge-nb/issues)
13 | [](https://github.com/ebaauw/homebridge-nb/pulls)
14 | [](https://standardjs.com)
15 |
16 |
17 |
18 | ## Homebridge plugin for Nuki Bridge
19 | Copyright © 2020-2025 Erik Baauw. All rights reserved.
20 |
21 | This [Homebridge](https://github.com/homebridge/homebridge) plugin exposes
22 | [Nuki Smart Lock](https://nuki.io/nl/smart-lock/) and
23 | [Nuki Opener](https://nuki.io/nl/opener/) devices,
24 | connected to a [Nuki Bridge](https://nuki.io/nl/bridge/),
25 | to Apple's [HomeKit](https://www.apple.com/ios/home/).
26 | It provides the following features:
27 | - Expose each Nuki Bridge as separate accessory with a custom service.
28 | Home shows this accessory as _Not Supported_ in Home, but is required for
29 | Homebridge NB.
30 | In Eve and other HomeKit apps, this accessory provides control of the polling
31 | rate, the level of logging, and rebooting the Bridge.
32 | In future, this accessory might become a separate bridge.
33 | - Expose each Nuki Smart Lock as a separate accessory, with a _Lock Mechanism_
34 | service to control the lock, and a _Battery_ service for the battery.
35 | The when present and configured, door sensor is exposed as separate
36 | _Contact Sensor_ service, including Eve history.
37 | - Expose each Nuki Opener as separate accessory, with a _Doorbell_ service for
38 | the ringer, a _Lock Mechanism_ service for the opener, and a _Battery_ Service
39 | for the battery.
40 | - Zero configuration:
41 | - Automatic discovery of Nuki Bridge and paired Smart Lock and Opener devices.
42 | - Automatic configuration of API token.
43 | The token is persisted across Homebridge restarts (in the Bridge accessory).
44 | - Technical:
45 | - Use local API provided by the Nuki Bridge; no Internet connection needed
46 | (except for discovery of the Bridge).
47 | - Use hashed tokens for added security.
48 | - Subscribe to Nuki Bridge for notifications on device state changes.
49 | - Use _Identify_ on the Smart Lock and Opener accessories to force the Bridge
50 | to contact the device and refresh the cached device state.
51 |
52 | ### Prerequisites
53 | Homebridge NB communicates with the Nuki Bridge using the local
54 | [Nuki Bridge HTTP API](https://developer.nuki.io/page/nuki-bridge-http-api-1-12/4).
55 | You need to enable this API through the Nuki app.
56 |
57 | Use [Eve](https://www.evehome.com/en/eve-app) to get the full functionality,
58 | like history for the door sensor,
59 | and support for _Ring to Open_ and _Continuous Mode_ on the Opener.
60 |
61 | ### Command-Line Tool
62 | Homebridge NB includes a command-line tool, `nb`,
63 | to interact with the Nuki Bridge from the comand line.
64 | It takes a `-h` or `--help` argument to provide a brief overview of
65 | its functionality and command-line arguments.
66 |
67 | ### Installation
68 | To install Homebridge NB:
69 | - Follow the instructions on the [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki) to install Node.js and Homebridge;
70 | - Install the Homebridge NB plugin through Homebridge Config UI X or manually by:
71 | ```
72 | $ sudo npm -g i homebridge-nb
73 | ```
74 | - Edit `config.json` and add the `NB` platform provided by Homebridge NB, see [**Homebridge Configuration**](#homebridge-configuration);
75 | - Run Homebrdige NB for the first time, and press the button on the Nuki bridge.
76 | The bridge should be discovered automatically.
77 | Pressing the button allows Homebridge NB to obtain a token (API key).
78 |
79 | ### Configuration
80 | In Homebridge's `config.json` you need to specify Homebridge NB as a platform
81 | plugin.
82 |
83 | ```json
84 | "platforms": [
85 | {
86 | "platform": "NB"
87 | }
88 | ]
89 | ```
90 |
--------------------------------------------------------------------------------
/cli/nb.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // nb.js
4 | //
5 | // Homebridge NB Tools.
6 | // Copyright © 2018-2025 Erik Baauw. All rights reserved.
7 |
8 | import { createRequire } from 'node:module'
9 |
10 | import { NbTool } from 'hb-nb-tools/NbTool'
11 |
12 | const require = createRequire(import.meta.url)
13 | const packageJson = require('../package.json')
14 |
15 | new NbTool(packageJson).main()
16 |
--------------------------------------------------------------------------------
/config.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "pluginAlias": "NB",
3 | "pluginType": "platform",
4 | "singular": true,
5 | "headerDisplay": "Homebridge plugin for Nuki Bridge",
6 | "footerDisplay": "",
7 | "schema": {
8 | "type": "object",
9 | "properties": {
10 | "name": {
11 | "description": "Plugin name as displayed in the Homebridge log.",
12 | "type": "string",
13 | "required": true,
14 | "default": "Nuki"
15 | },
16 | "bridges": {
17 | "title": "Bridges",
18 | "description": "Nuki bridges (default: discovered automatically).",
19 | "type": "array",
20 | "items": {
21 | "title": "Bridge",
22 | "description": "Nuki bridge configuration.",
23 | "required": true,
24 | "type": "object",
25 | "additionalProperties": false,
26 | "properties": {
27 | "bridgeId": {
28 | "title": "Bridge ID",
29 | "description": "Nuki ID of the bridge.",
30 | "type": "string",
31 | "required": true
32 | },
33 | "host": {
34 | "title": "Host",
35 | "description": "IP address or hostname of the bridge.",
36 | "type": "string",
37 | "required": true
38 | }
39 | }
40 | }
41 | },
42 | "devices": {
43 | "title": "Device",
44 | "description": "Whitelisted Nuki devices (default: all).
Make sure to include the Nuki bridge to which the device is connected.",
45 | "type": "array",
46 | "items": {
47 | "title": "Device",
48 | "description": "Nuki ID of the device.",
49 | "type": "string"
50 | }
51 | },
52 | "encryption": {
53 | "description": "Encryption for Nuki bridge token. Default: Encrypted.",
54 | "type": "string",
55 | "required": true,
56 | "oneOf": [
57 | {
58 | "title": "None",
59 | "enum": [
60 | "none"
61 | ]
62 | },
63 | {
64 | "title": "Hashed Token",
65 | "enum": [
66 | "hashedToken"
67 | ]
68 | },
69 | {
70 | "title": "Encrypted Token",
71 | "enum": [
72 | "encryptedToken"
73 | ]
74 | }
75 | ]
76 | },
77 | "latch": {
78 | "description": "Expose a second Lock Mechanism service to unlatch a Smart Lock.",
79 | "type": "boolean"
80 | },
81 | "port": {
82 | "description": "Port to use for webserver receiving Nuki bridge notifications. Default: random.",
83 | "type": "integer",
84 | "maximum": 65535
85 | },
86 | "openerResetTimeout": {
87 | "description": "The timeout in milliseconds to wait before resetting the Opener to locked. Default: 500. Set to 0 to keep the Opener unlocked.",
88 | "type": "integer",
89 | "minimum": 0,
90 | "maximum": 2000
91 | },
92 | "removeStaleAccessories": {
93 | "description": "Remove stale accessories, whose devices are no longer exposed by a Nuki bridge.",
94 | "type": "boolean"
95 | },
96 | "timeout": {
97 | "description": "The timeout in seconds to wait for a response from a Nuki bridge. Default: 15.",
98 | "type": "integer",
99 | "minimum": 1,
100 | "maximum": 60
101 | }
102 | }
103 | },
104 | "form": [
105 | "name",
106 | "latch",
107 | {
108 | "nodescription": true,
109 | "notitle": true,
110 | "key": "bridges",
111 | "type": "array",
112 | "items": [
113 | {
114 | "type": "div",
115 | "displayFlex": true,
116 | "items": [
117 | {
118 | "key": "bridges[].bridgeId",
119 | "required": true,
120 | "flex": "1 1 50px"
121 | },
122 | {
123 | "key": "bridges[].host",
124 | "required": true,
125 | "flex": "1 1 50px"
126 | }
127 | ]
128 | }
129 | ]
130 | },
131 | {
132 | "type": "fieldset",
133 | "expandable": true,
134 | "title": "Advanced Settings",
135 | "description": "Don't change these, unless you understand what you're doing.",
136 | "items": [
137 | {
138 | "nodescription": true,
139 | "notitle": true,
140 | "key": "devices",
141 | "type": "array",
142 | "items": [
143 | {
144 | "type": "div",
145 | "displayFlex": true,
146 | "items": [
147 | {
148 | "key": "devices[]",
149 | "required": true,
150 | "flex": "1 1 50px"
151 | }
152 | ]
153 | }
154 | ]
155 | },
156 | "encryption",
157 | "openerResetTimeout",
158 | "port",
159 | "removeStaleAccessories",
160 | "timeout"
161 | ]
162 | }
163 | ]
164 | }
165 |
--------------------------------------------------------------------------------
/homebridge-nb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebaauw/homebridge-nb/54c473b9f01f6cae3c89287085f5234aa2271a33/homebridge-nb.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/index.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { createRequire } from 'node:module'
7 |
8 | import { NbPlatform } from './lib/NbPlatform.js'
9 |
10 | const require = createRequire(import.meta.url)
11 | const packageJson = require('./package.json')
12 |
13 | function main (homebridge) {
14 | NbPlatform.loadPlatform(homebridge, packageJson, 'NB', NbPlatform)
15 | }
16 |
17 | export { main as default }
18 |
--------------------------------------------------------------------------------
/lib/NbAccessory.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbAccessory.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { toHexString } from 'homebridge-lib'
7 | import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate'
8 |
9 | class NbAccessory extends AccessoryDelegate {
10 | constructor (bridge, params) {
11 | super(bridge.platform, {
12 | id: params.id,
13 | name: params.name,
14 | category: params.category,
15 | manufacturer: 'Nuki',
16 | model: params.model,
17 | firmware: params.device.firmwareVersion
18 | })
19 | this.inheritLogLevel(bridge)
20 | this.bridge = bridge
21 | this.client = this.bridge.client
22 | this.context.bridgeId = this.bridge.id
23 | this.nukiId = toHexString(params.device.nukiId)
24 | this.deviceType = params.device.deviceType
25 | this.log('Nuki %s v%s %s', params.model, params.device.firmwareVersion, params.id)
26 | this.on('identify', this.identify)
27 | setImmediate(() => {
28 | this.debug('initialised')
29 | this.emit('initialised')
30 | })
31 | }
32 |
33 | async lockAction (action) {
34 | return this.client.lockAction(this.nukiId, this.deviceType, action)
35 | }
36 |
37 | async refresh () {
38 | return this.client.lockState(this.nukiId, this.deviceType)
39 | }
40 |
41 | async identify () {
42 | try {
43 | const response = await this.refresh()
44 | this.update(response.body)
45 | } catch (error) { this.error(error) }
46 | }
47 | }
48 |
49 | export { NbAccessory }
50 |
--------------------------------------------------------------------------------
/lib/NbAccessory/Bridge.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbAccessory/Bridge.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { toHexString } from 'homebridge-lib'
7 | import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate'
8 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
9 | import 'homebridge-lib/ServiceDelegate/Dummy'
10 |
11 | import { NbClient } from 'hb-nb-tools/NbClient'
12 |
13 | import { NbAccessory } from '../NbAccessory.js'
14 | import { NbService } from '../NbService.js'
15 | import '../NbService/Bridge.js'
16 |
17 | class Bridge extends AccessoryDelegate {
18 | constructor (platform, context) {
19 | super(platform, {
20 | id: context.id,
21 | name: context.name,
22 | category: platform.Accessory.Categories.RANGE_EXTENDER,
23 | model: 'Bridge',
24 | firmware: context.firmware
25 | })
26 | this.id = context.id
27 | this.context.host = context.host
28 | this.context.token = context.token
29 | this.context.firmware = context.firmware
30 | this.log(
31 | 'Nuki Bridge v%s %s at %s', context.firmware, context.id, context.host
32 | )
33 | this.on('shutdown', this.shutdown)
34 | this.heartbeatEnabled = true
35 | this.once('heartbeat', this.init)
36 |
37 | this.smartLocks = {}
38 | this.doorSensors = {}
39 | this.keypads = {}
40 | this.openers = {}
41 | this.service = new NbService.Bridge(this)
42 | this.manageLogLevel(this.service.characteristicDelegate('logLevel'))
43 | this.dummyService = new ServiceDelegate.Dummy(this)
44 |
45 | this.client = new NbClient({
46 | encryption: platform.config.encryption,
47 | host: this.context.host,
48 | timeout: platform.config.timeout,
49 | token: this.context.token
50 | })
51 | this.client
52 | .on('error', (error) => {
53 | this.log(
54 | 'request %d: %s %s', error.request.id,
55 | error.request.method, error.request.resource
56 | )
57 | this.warn('request %d: error: %s', error.request.id, error)
58 | })
59 | .on('request', (request) => {
60 | this.debug(
61 | 'request %d: %s %s', request.id, request.method, request.resource
62 | )
63 | this.vdebug(
64 | 'request %d: %s %s', request.id, request.method, request.url
65 | )
66 | })
67 | .on('response', (response) => {
68 | this.vdebug(
69 | 'request %d: response: %j', response.request.id, response.body
70 | )
71 | this.debug(
72 | 'request %d: %s %s', response.request.id,
73 | response.statusCode, response.statusMessage
74 | )
75 | })
76 | .on('event', (event) => {
77 | this.debug('event: %j', event)
78 | const id = toHexString(event.nukiId)
79 | switch (event.deviceType) {
80 | case NbClient.DeviceTypes.SMARTLOCK:
81 | case NbClient.DeviceTypes.SMARTDOOR:
82 | case NbClient.DeviceTypes.SMARTLOCK3:
83 | if (this.smartLocks[id] != null) {
84 | this.smartLocks[id].update(event)
85 | }
86 | if (this.doorSensors[id] != null) {
87 | this.doorSensors[id].update(event)
88 | }
89 | if (this.keypads[id] != null) {
90 | this.keypads[id].update(event)
91 | }
92 | break
93 | case NbClient.DeviceTypes.OPENER:
94 | if (this.openers[id] != null) {
95 | this.openers[id].update(event)
96 | }
97 | if (this.keypads[id] != null) {
98 | this.keypads[id].update(event)
99 | }
100 | break
101 | default:
102 | break
103 | }
104 | })
105 | }
106 |
107 | get host () { return this.context.host }
108 | set host (value) {
109 | if (value !== this.context.host) {
110 | this.debug('now at %s', value)
111 | this.client.host = value
112 | this.context.host = value
113 | this.once('heartbeat', this.init)
114 | }
115 | }
116 |
117 | async init (beat) {
118 | try {
119 | await this.client.init()
120 | this.values.firmware = this.client.firmware
121 | this.context.firmware = this.values.firmware
122 | if (this.values.firmware !== this.platform.packageJson.engines.nuki) {
123 | this.warn(
124 | 'recommended version: Nuki Bridge v%s',
125 | this.platform.packageJson.engines.nuki
126 | )
127 | }
128 | switch (this.client.encryption) {
129 | case 'none':
130 | this.warn('using plain-text tokens')
131 | break
132 | case 'hashedToken':
133 | this.warn('using deprecated hashed tokens')
134 | break
135 | default:
136 | break
137 | }
138 | } catch (error) {
139 | return
140 | }
141 | if (this.context.callbackUrl) {
142 | this.warn('unclean shutdown - checking for stale subscriptions')
143 | try {
144 | const response = await this.client.callbackList()
145 | for (const callback of response.body.callbacks) {
146 | if (callback.url === this.context.callbackUrl) {
147 | this.log('remove stale subscription')
148 | await this.client.callbackRemove(callback.id)
149 | }
150 | }
151 | } catch (error) {
152 | this.warn(error)
153 | }
154 | }
155 | this.context.callbackUrl = await this.platform.addClient(this.client)
156 | this.debug('initialised')
157 | this.emit('initialised')
158 | this.initialBeat = beat
159 | try {
160 | await this.heartbeat(beat)
161 | } catch (error) {}
162 | this.debug('bridgeInitialised')
163 | this.emit('bridgeInitialised')
164 | this.on('heartbeat', this.heartbeat)
165 | }
166 |
167 | async checkSubscription () {
168 | if (this.callbackId == null) {
169 | this.log('subscribe to event notifications')
170 | const response = await this.client.callbackAdd(this.context.callbackUrl)
171 | if (!response.body.success) {
172 | this.error(response.body.message)
173 | return
174 | }
175 | }
176 | const response = await this.client.callbackList()
177 | for (const callback of response.body.callbacks) {
178 | if (callback.url === this.context.callbackUrl) {
179 | this.debug('subscription: %j', callback)
180 | this.callbackId = callback.id
181 | return
182 | }
183 | }
184 | if (this.callbackId != null) {
185 | this.warn('lost subscription to event notifications')
186 | this.callbackId = null
187 | }
188 | return this.checkSubscription()
189 | }
190 |
191 | async shutdown () {
192 | if (this.client != null) {
193 | const response = await this.client.callbackList()
194 | for (const callback of response.body.callbacks) {
195 | if (callback.url === this.context.callbackUrl) {
196 | try {
197 | this.log('unsubscribe from event notifications')
198 | await this.client.callbackRemove(callback.id)
199 | delete this.context.callbackUrl
200 | } catch (error) {
201 | this.error(error)
202 | }
203 | }
204 | }
205 | this.platform.removeClient(this.client)
206 | }
207 | }
208 |
209 | async addDoorSensor (id, context) {
210 | if (NbAccessory.DoorSensor == null) {
211 | await import('../NbAccessory/DoorSensor.js')
212 | }
213 | this.doorSensors[id] = new NbAccessory.DoorSensor(this, context)
214 | }
215 |
216 | async addKeypad (id, context) {
217 | if (NbAccessory.Keypad == null) {
218 | await import('../NbAccessory/Keypad.js')
219 | }
220 | this.keypads[id] = new NbAccessory.Keypad(this, context)
221 | }
222 |
223 | async addOpener (id, context) {
224 | if (NbAccessory.Opener == null) {
225 | await import('../NbAccessory/Opener.js')
226 | }
227 | this.openers[id] = new NbAccessory.Opener(this, context)
228 | }
229 |
230 | async addSmartLock (id, context) {
231 | if (NbAccessory.SmartLock == null) {
232 | await import('../NbAccessory/SmartLock.js')
233 | }
234 | this.smartLocks[id] = new NbAccessory.SmartLock(this, context)
235 | }
236 |
237 | async checkDoorSensor (id, device) {
238 | if (
239 | device.lastKnownState.doorsensorState != null &&
240 | device.lastKnownState.doorsensorState !== NbClient.DoorSensorStates.DEACTIVATED
241 | ) {
242 | if (this.doorSensors[id] == null) {
243 | await this.addDoorSensor(id, { id, device })
244 | }
245 | this.doorSensors[id].context.device = device
246 | this.doorSensors[id].update(device.lastKnownState)
247 | } else if (this.doorSensors[id] != null) {
248 | this.doorSensors[id].destroy()
249 | delete this.doorSensors[id]
250 | }
251 | }
252 |
253 | async checkKeypad (id, device) {
254 | if (
255 | device.lastKnownState.keypadBatteryCritical != null && (
256 | device.deviceType !== 4 || device.lastKnownState.doorsensorState == null
257 | )
258 | ) {
259 | if (this.keypads[id] == null) {
260 | await this.addKeypad(id, { id, device })
261 | }
262 | this.keypads[id].context.device = device
263 | this.keypads[id].update(device.lastKnownState)
264 | } else if (this.keypads[id] != null) {
265 | this.keypads[id].destroy()
266 | delete this.keypads[id]
267 | }
268 | }
269 |
270 | checkFirmware (info) {
271 | if (this.values.firmware !== info.versions.firmwareVersion) {
272 | this.values.firmware = info.versions.firmwareVersion
273 | this.context.firmware = this.values.firmware
274 | if (this.values.firmware !== this.platform.packageJson.engines.nuki) {
275 | this.warn(
276 | 'recommended version: Nuki Bridge v%s',
277 | this.platform.packageJson.engines.nuki
278 | )
279 | }
280 | }
281 | }
282 |
283 | async heartbeat (beat) {
284 | if ((beat - this.initialBeat) % this.service.values.heartrate === 0) {
285 | try {
286 | await this.checkSubscription()
287 | let response = await this.client.info()
288 | this.debug('bridge: %j', response.body)
289 | this.checkFirmware(response.body)
290 | this.service.update(response.body)
291 | response = await this.client.list()
292 | for (const device of response.body) {
293 | try {
294 | this.debug('device: %j', device)
295 | if (device.firmwareVersion == null) { // Issue 93.
296 | continue
297 | }
298 | const id = toHexString(device.nukiId)
299 | if (!this.platform.isWhitelisted(id)) {
300 | continue
301 | }
302 | switch (device.deviceType) {
303 | case NbClient.DeviceTypes.SMARTLOCK:
304 | case NbClient.DeviceTypes.SMARTDOOR:
305 | case NbClient.DeviceTypes.SMARTLOCK3:
306 | if (device.lastKnownState == null) {
307 | this.warn('%s: no last known state', id)
308 | continue
309 | }
310 | if (device.name == null || device.name === '') {
311 | device.name = 'Nuki_' + id
312 | }
313 | if (this.smartLocks[id] == null) {
314 | await this.addSmartLock(id, { id, device })
315 | }
316 | this.smartLocks[id].context.device = device
317 | this.smartLocks[id].update(device.lastKnownState)
318 | await this.checkDoorSensor(id, device)
319 | await this.checkKeypad(id, device)
320 | break
321 | case NbClient.DeviceTypes.OPENER:
322 | if (device.lastKnownState == null) {
323 | this.warn('%s: no last known state', id)
324 | continue
325 | }
326 | if (device.name == null || device.name === '') {
327 | device.name = 'Nuki_Opener_' + id
328 | }
329 | if (this.openers[id] == null) {
330 | await this.addOpener(id, { id, device })
331 | }
332 | this.openers[id].context.device = device
333 | this.openers[id].update(device.lastKnownState)
334 | await this.checkKeypad(id, device)
335 | break
336 | default:
337 | break
338 | }
339 | } catch (error) {
340 | this.warn('heartbeat error: %s', error)
341 | }
342 | }
343 | // Workaround: bridge state isn't always updated
344 | for (const id in this.smartLocks) {
345 | try {
346 | if (this.smartLocks[id].service.needRefresh) {
347 | const response = await this.smartLocks[id].refresh()
348 | const state = response.body
349 | this.debug('device state refresh: %j', state)
350 | this.smartLocks[id].update(state)
351 | }
352 | } catch (error) {
353 | this.warn('heartbeat error: %s', error)
354 | }
355 | }
356 | // End workaround
357 | } catch (error) {
358 | this.warn('heartbeat error: %s', error)
359 | }
360 | }
361 | }
362 | }
363 |
364 | NbAccessory.Bridge = Bridge
365 |
--------------------------------------------------------------------------------
/lib/NbAccessory/DoorSensor.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbAccessory/DoorSensor.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/Battery'
8 | import 'homebridge-lib/ServiceDelegate/History'
9 |
10 | import { NbAccessory } from '../NbAccessory.js'
11 | import { NbService } from '../NbService.js'
12 | import '../NbService/DoorSensor.js'
13 |
14 | class DoorSensor extends NbAccessory {
15 | constructor (bridge, params) {
16 | params.category = bridge.Accessory.Categories.DOOR
17 | params.model = 'Door Sensor'
18 | super(bridge, {
19 | id: params.id + '-S',
20 | name: params.device.name + ' Sensor',
21 | device: params.device,
22 | category: bridge.Accessory.Categories.DOOR,
23 | model: 'Door Sensor'
24 | })
25 | this.service = new NbService.DoorSensor(this)
26 | if (
27 | params.device.deviceType === 4 &&
28 | params.device.lastKnownState.keypadBatteryCritical != null
29 | ) {
30 | this.batteryService = new ServiceDelegate.Battery(this, {
31 | statusLowBattery: params.device.lastKnownState.keypadBatteryCritical
32 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
33 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
34 | })
35 | }
36 | this.historyService = new ServiceDelegate.History(this, {
37 | contactDelegate: this.service.characteristicDelegate('contact'),
38 | lastContactDelegate: this.service.characteristicDelegate('lastActivation'),
39 | timesOpenedDelegate: this.service.characteristicDelegate('timesOpened')
40 | })
41 | }
42 |
43 | update (state) {
44 | this.service.update(state)
45 | if (this.batteryService != null && state.keypadBatteryCritical != null) {
46 | this.batteryService.values.statusLowBattery = state.keypadBatteryCritical
47 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
48 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
49 | }
50 | }
51 | }
52 |
53 | NbAccessory.DoorSensor = DoorSensor
54 |
--------------------------------------------------------------------------------
/lib/NbAccessory/Keypad.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbAccessory/Keypad.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/Battery'
8 | import 'homebridge-lib/ServiceDelegate/Dummy'
9 |
10 | import { NbAccessory } from '../NbAccessory.js'
11 |
12 | class Keypad extends NbAccessory {
13 | constructor (bridge, params) {
14 | params.category = bridge.Accessory.Categories.DOOR_LOCK
15 | params.model = 'Keypad'
16 | super(bridge, {
17 | id: params.id + '-K',
18 | name: params.device.name + ' Keypad',
19 | device: params.device,
20 | category: bridge.Accessory.Categories.PROGRAMMABLE_SWITCH,
21 | model: 'Keypad'
22 | })
23 | this.service = new ServiceDelegate.Dummy(this)
24 | this.batteryService = new ServiceDelegate.Battery(this, {
25 | statusLowBattery: params.device.lastKnownState.keypadBatteryCritical
26 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
27 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
28 | })
29 | }
30 |
31 | update (state) {
32 | if (state.keypadBatteryCritical != null) {
33 | this.batteryService.values.statusLowBattery = state.keypadBatteryCritical
34 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
35 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
36 | }
37 | }
38 | }
39 |
40 | NbAccessory.Keypad = Keypad
41 |
--------------------------------------------------------------------------------
/lib/NbAccessory/Opener.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbAccessory/Opener.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/Battery'
8 |
9 | import { NbClient } from 'hb-nb-tools/NbClient'
10 |
11 | import { NbAccessory } from '../NbAccessory.js'
12 | import { NbService } from '../NbService.js'
13 | import '../NbService/DoorBell.js'
14 | import '../NbService/Opener.js'
15 |
16 | class Opener extends NbAccessory {
17 | constructor (bridge, params) {
18 | super(bridge, {
19 | id: params.id,
20 | name: params.device.name,
21 | device: params.device,
22 | category: bridge.Accessory.Categories.DOOR_LOCK,
23 | model: NbClient.modelName(params.device.deviceType, params.device.firmwareVersion)
24 | })
25 | this.openerService = new NbService.Opener(this)
26 | this.doorBellService = new NbService.DoorBell(this)
27 | this.batteryService = new ServiceDelegate.Battery(this, {
28 | statusLowBattery: params.device.lastKnownState.doorsensorBatteryCritical
29 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
30 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
31 | })
32 | }
33 |
34 | update (state) {
35 | this.openerService.update(state)
36 | this.doorBellService.update(state)
37 | if (state.batteryCritical != null) {
38 | this.batteryService.values.statusLowBattery = state.batteryCritical
39 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
40 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
41 | }
42 | }
43 | }
44 |
45 | NbAccessory.Opener = Opener
46 |
--------------------------------------------------------------------------------
/lib/NbAccessory/SmartLock.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbAccessory/SmartLock.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/Battery'
8 |
9 | import { NbClient } from 'hb-nb-tools/NbClient'
10 |
11 | import { NbAccessory } from '../NbAccessory.js'
12 | import { NbService } from '../NbService.js'
13 | import '../NbService/Latch.js'
14 | import '../NbService/SmartLock.js'
15 |
16 | class SmartLock extends NbAccessory {
17 | constructor (bridge, params) {
18 | super(bridge, {
19 | id: params.id,
20 | name: params.device.name,
21 | device: params.device,
22 | category: bridge.Accessory.Categories.DOOR_LOCK,
23 | model: NbClient.modelName(params.device.deviceType, params.device.firmwareVersion)
24 | })
25 | this.service = new NbService.SmartLock(this)
26 | if (this.platform.config.latch) {
27 | // TODO: only import NbService.Latch when config.latch is set
28 | this.latchService = new NbService.Latch(this)
29 | }
30 | this.batteryService = new ServiceDelegate.Battery(this, {
31 | batteryLevel: params.device.lastKnownState.batteryChargeState,
32 | chargingState: params.device.lastKnownState.batteryCharging
33 | ? this.Characteristics.hap.ChargingState.CHARGING
34 | : this.Characteristics.hap.ChargingState.NOT_CHARGING,
35 | statusLowBattery: params.device.lastKnownState.batteryCritical
36 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
37 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
38 | })
39 | }
40 |
41 | update (state) {
42 | this.service.update(state)
43 | if (this.platform.config.latch) {
44 | this.latchService.update(state)
45 | }
46 | if (state.batteryChargeState) {
47 | this.batteryService.values.batteryLevel = state.batteryChargeState
48 | }
49 | if (state.batteryCharging != null) {
50 | this.batteryService.values.chargingState = state.batteryCharging
51 | ? this.Characteristics.hap.ChargingState.CHARGING
52 | : this.Characteristics.hap.ChargingState.NOT_CHARGING
53 | }
54 | if (state.batteryCritical != null) {
55 | this.batteryService.values.statusLowBattery = state.batteryCritical
56 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
57 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
58 | }
59 | }
60 | }
61 |
62 | NbAccessory.SmartLock = SmartLock
63 |
--------------------------------------------------------------------------------
/lib/NbPlatform.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbPlatform.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { once } from 'node:events'
7 |
8 | import { timeout, toHexString } from 'homebridge-lib'
9 | import { HttpClient } from 'homebridge-lib/HttpClient'
10 | import { OptionParser } from 'homebridge-lib/OptionParser'
11 | import { Platform } from 'homebridge-lib/Platform'
12 |
13 | import { NbClient } from 'hb-nb-tools/NbClient'
14 | import { NbDiscovery } from 'hb-nb-tools/NbDiscovery'
15 | import { NbListener } from 'hb-nb-tools/NbListener'
16 |
17 | import { NbAccessory } from './NbAccessory.js'
18 | import './NbAccessory/Bridge.js'
19 |
20 | const discoveryInterval = 600
21 |
22 | class NbPlatform extends Platform {
23 | constructor (log, configJson, homebridge) {
24 | super(log, configJson, homebridge)
25 | this.config = {
26 | devices: [],
27 | encryption: 'encryptedToken',
28 | bridges: [],
29 | openerResetTimeout: 500,
30 | timeout: 15
31 | }
32 | this.restoredAccessories = {}
33 | const optionParser = new OptionParser(this.config, true)
34 | optionParser
35 | .stringKey('platform')
36 | .stringKey('name')
37 | .arrayKey('devices')
38 | .enumKey('encryption')
39 | .enumKeyValue('encryption', 'none')
40 | .enumKeyValue('encryption', 'hashedToken')
41 | .enumKeyValue('encryption', 'encryptedToken')
42 | .arrayKey('bridges')
43 | .boolKey('latch')
44 | .intKey('port', 0, 65535)
45 | .intKey('openerResetTimeout', 0, 2000) // milliseconds
46 | .boolKey('removeStaleAccessories')
47 | .intKey('timeout', 1, 60) // seconds
48 | .on('userInputError', (message) => {
49 | this.warn('config.json: %s', message)
50 | })
51 | try {
52 | optionParser.parse(configJson)
53 | const validBridges = []
54 | for (const i in this.config.bridges) {
55 | const bridge = {
56 | port: 8080
57 | }
58 | const optionParser = new OptionParser(bridge, true)
59 | optionParser.stringKey('bridgeId')
60 | optionParser.hostKey('host')
61 | try {
62 | optionParser.parse(this.config.bridges[i])
63 | bridge.bridgeId = OptionParser.toInt(
64 | `bridges[${i}].bridgeId`, bridge.bridgeId, 0x10000000, 0xFFFFFFFF, true
65 | )
66 | bridge.ip = bridge.hostname
67 | validBridges.push(bridge)
68 | } catch (error) {
69 | if (error instanceof OptionParser.UserInputError) {
70 | this.warn(error)
71 | } else {
72 | this.error(error)
73 | }
74 | }
75 | }
76 | this.config.bridges = validBridges
77 | const validDevices = []
78 | for (const i in this.config.devices) {
79 | try {
80 | const device = OptionParser.toInt(
81 | `devices[${i}]`, this.config.devices[i], 0x10000000, 0xFFFFFFFF, true
82 | )
83 | validDevices.push(toHexString(device))
84 | } catch (error) {
85 | if (error instanceof OptionParser.UserInputError) {
86 | this.warn(error)
87 | } else {
88 | this.error(error)
89 | }
90 | }
91 | }
92 | this.config.devices = validDevices
93 | this.bridges = {}
94 | this.discovery = new NbDiscovery({
95 | timeout: this.config.timeout
96 | })
97 | this.discovery
98 | .on('error', (error) => {
99 | this.warn(
100 | '%s: request %d: %s %s', error.request.name, error.request.id,
101 | error.request.method, error.request.resource
102 | )
103 | this.warn(
104 | '%s: request %d: error: %s', error.request.name, error.request.id, error
105 | )
106 | })
107 | .on('request', (request) => {
108 | this.debug(
109 | '%s: request %d: %s %s', request.name, request.id,
110 | request.method, request.resource
111 | )
112 | this.vdebug(
113 | '%s: request %d: %s %s', request.name, request.id,
114 | request.method, request.url
115 | )
116 | })
117 | .on('response', (response) => {
118 | this.vdebug(
119 | '%s: request %d: response: %j', response.request.name, response.request.id,
120 | response.body
121 | )
122 | this.debug(
123 | '%s: request %d: %d %s', response.request.name, response.request.id,
124 | response.statusCode, response.statusMessage
125 | )
126 | })
127 | this
128 | .on('accessoryRestored', this.accessoryRestored)
129 | .once('heartbeat', this.init)
130 | } catch (error) {
131 | this.error(error)
132 | }
133 | this.debug('config: %j', this.config)
134 | }
135 |
136 | async init (beat) {
137 | for (const id in this.restoredAccessories) {
138 | const bridge = this.bridges[id]
139 | for (const restoredAccessory of this.restoredAccessories[id]) {
140 | try {
141 | const { className, id, name, context } = restoredAccessory
142 | if (context.device == null) {
143 | // Old plugin version - re-create accessory delegate on bridge initialisation
144 | continue
145 | }
146 | context.id = id
147 | context.name = name
148 | await bridge?.['add' + className](context.id, context)
149 | } catch (error) {
150 | this.warn('%s', error)
151 | }
152 | }
153 | }
154 | try {
155 | const jobs = []
156 | for (const id in this.bridges) {
157 | jobs.push(once(this.bridges[id], 'bridgeInitialised'))
158 | }
159 | for (const bridge of this.config.bridges) {
160 | jobs.push(this.foundBridge(bridge))
161 | }
162 | if (jobs.length === 0) {
163 | jobs.push(this.discover())
164 | }
165 | for (const job of jobs) {
166 | try {
167 | await job
168 | } catch (error) {
169 | if (!(error instanceof HttpClient.HttpError)) {
170 | this.error(error)
171 | }
172 | }
173 | }
174 | } catch (error) {
175 | if (!(error instanceof HttpClient.HttpError)) {
176 | this.error(error)
177 | }
178 | }
179 | this.on('heartbeat', this.heartbeat)
180 | this.debug('initialised')
181 | this.emit('initialised')
182 | }
183 |
184 | async discover () {
185 | const bridges = await this.discovery.discover()
186 | this.debug('discovery: %j', bridges)
187 | const jobs = []
188 | for (const bridge of bridges) {
189 | jobs.push(this.foundBridge(bridge))
190 | }
191 | for (const job of jobs) {
192 | try {
193 | await job
194 | } catch (error) {
195 | if (!(error instanceof HttpClient.HttpError)) {
196 | this.error(error)
197 | }
198 | }
199 | }
200 | }
201 |
202 | async heartbeat (beat) {
203 | if (
204 | this.config.bridges.length === 0 &&
205 | beat % discoveryInterval === discoveryInterval - 5
206 | ) {
207 | try {
208 | await this.discover()
209 | } catch (error) {
210 | if (!(error instanceof HttpClient.HttpError)) {
211 | this.error(error)
212 | }
213 | }
214 | }
215 | }
216 |
217 | isWhitelisted (id) {
218 | return this.config.devices.length === 0 || this.config.devices.includes(id)
219 | }
220 |
221 | async foundBridge (bridge) {
222 | if (bridge.ip == null || bridge.port == null) {
223 | return
224 | }
225 | const id = toHexString(bridge.bridgeId)
226 | if (!this.isWhitelisted(id)) {
227 | return
228 | }
229 | const host = bridge.ip + ':' + bridge.port
230 | if (this.bridges[id] == null) {
231 | const name = 'Nuki Bridge ' + id
232 | this.debug('%s: found bridge %s at %s', name, id, host)
233 | const client = new NbClient({
234 | encryption: this.config.encryption,
235 | host,
236 | timeout: 60,
237 | token: this._accessories[id]?.context?.context?.token
238 | })
239 | client
240 | .on('error', (error) => {
241 | this.log(
242 | '%s: request %d: %s %s', name, error.request.id,
243 | error.request.method, error.request.resource
244 | )
245 | this.warn(
246 | '%s: request %d: error: %s', name, error.request.id, error
247 | )
248 | })
249 | .on('request', (request) => {
250 | this.debug(
251 | '%s: request %d: %s %s', name, request.id,
252 | request.method, request.resource
253 | )
254 | this.vdebug(
255 | '%s: equest %d: %s %s', name, request.id,
256 | request.method, request.url
257 | )
258 | })
259 | .on('response', (response) => {
260 | this.vdebug(
261 | '%s: request %d: response: %j', name, response.request.id,
262 | response.body
263 | )
264 | this.debug(
265 | '%s: request %d: %s %s', name, response.request.id,
266 | response.statusCode, response.statusMessage
267 | )
268 | })
269 | while (client.token === '') {
270 | try {
271 | this.log('%s: press Nuki bridge button to obtain token', name)
272 | await client.auth()
273 | if (client.token == null) {
274 | this.warn('Nuki bridge button not pressed')
275 | }
276 | } catch (error) {
277 | this.warn(error)
278 | await timeout(30000)
279 | }
280 | }
281 | await client.init()
282 | this.bridges[client.id] = new NbAccessory.Bridge(this, {
283 | id: client.id,
284 | name: client.name,
285 | firmware: client.firmware,
286 | host: client.host,
287 | token: client.token
288 | })
289 | await once(this.bridges[client.id], 'bridgeInitialised')
290 | } else {
291 | this.bridges[id].host = host
292 | }
293 | }
294 |
295 | accessoryRestored (className, version, id, name, context) {
296 | if (this.config.removeStaleAccessories) {
297 | return
298 | }
299 | id = id.split('-')[0]
300 | if (!this.isWhitelisted(id)) {
301 | return
302 | }
303 | switch (className) {
304 | case 'Bridge':
305 | {
306 | context.id = id
307 | // Dirty hack en lieu of patching cachedAccessories
308 | let needPatch = false
309 | if (name.startsWith('Nuki_Bridge_')) {
310 | name = name.replace(/_/g, ' ')
311 | needPatch = true
312 | }
313 | // End hack
314 | context.name = name
315 | this.bridges[id] = new NbAccessory.Bridge(this, context)
316 | // Dirty hack en lieu of patching cachedAccessories
317 | if (needPatch) {
318 | this.bridges[id]._accessory._associatedHAPAccessory.displayName = name
319 | this.bridges[id]._context.name = name
320 | this.bridges[id].service.values.configuredName = name
321 | this.bridges[id].dummyService.values.configuredName = name
322 | }
323 | // End hack
324 | }
325 | break
326 | case 'SmartLock':
327 | case 'DoorSensor':
328 | case 'Keypad':
329 | case 'Opener':
330 | {
331 | const bridgeId = context.bridgeId
332 | if (this.restoredAccessories[bridgeId] == null) {
333 | this.restoredAccessories[bridgeId] = [{ className, id, name, context }]
334 | } else {
335 | this.restoredAccessories[bridgeId].push({ className, id, name, context })
336 | }
337 | }
338 | break
339 | default:
340 | this.warn(
341 | '%s: ignore unknown %s v%s accesssory', name, className, version
342 | )
343 | break
344 | }
345 | }
346 |
347 | async addClient (client) {
348 | if (this.listener == null) {
349 | this.listener = new NbListener(this.config.port)
350 | this.listener
351 | .on('error', (error) => { this.error(error) })
352 | .on('listening', (url) => {
353 | this.log('listening on %s', url)
354 | })
355 | .on('close', (url) => {
356 | this.log('closed %s', url)
357 | })
358 | }
359 | return this.listener.addClient(client)
360 | }
361 |
362 | removeClient (client) {
363 | if (this.listener != null) {
364 | this.listener.removeClient(client)
365 | }
366 | }
367 | }
368 |
369 | export { NbPlatform }
370 |
--------------------------------------------------------------------------------
/lib/NbService.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | class NbService extends ServiceDelegate {
9 | static dateToString (date) {
10 | if (date == null) {
11 | return String(new Date()).slice(0, 24)
12 | }
13 | return String(new Date(date)).slice(0, 24)
14 | }
15 | }
16 |
17 | export { NbService }
18 |
--------------------------------------------------------------------------------
/lib/NbService/Bridge.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService/Bridge.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { NbService } from '../NbService.js'
9 |
10 | const { dateToString } = NbService
11 |
12 | class Bridge extends ServiceDelegate {
13 | constructor (nbAccessory, params = {}) {
14 | params.name = nbAccessory.name
15 | params.Service = nbAccessory.Services.my.Resource
16 | params.primaryService = true
17 | super(nbAccessory, params)
18 |
19 | this.addCharacteristicDelegate({
20 | key: 'heartrate',
21 | Characteristic: this.Characteristics.my.Heartrate,
22 | props: { minValue: 10, maxValue: 600, minStep: 10 },
23 | value: 60
24 | })
25 | this.addCharacteristicDelegate({
26 | key: 'lastUpdated',
27 | Characteristic: this.Characteristics.my.LastUpdated
28 | })
29 | this.addCharacteristicDelegate({
30 | key: 'lastBoot',
31 | Characteristic: this.Characteristics.my.LastBoot
32 | })
33 | this.addCharacteristicDelegate({
34 | key: 'restart',
35 | Characteristic: this.Characteristics.my.Restart,
36 | value: false
37 | }).on('didSet', async (value) => {
38 | try {
39 | if (value) {
40 | await nbAccessory.client.reboot()
41 | setTimeout(() => {
42 | this.values.restart = false
43 | }, 500)
44 | }
45 | } catch (error) { this.error(error) }
46 | })
47 | this.addCharacteristicDelegate({
48 | key: 'logLevel',
49 | Characteristic: this.Characteristics.my.LogLevel,
50 | value: this.accessoryDelegate.logLevel
51 | })
52 | this.addCharacteristicDelegate({
53 | key: 'statusFault',
54 | Characteristic: this.Characteristics.hap.StatusFault,
55 | value: this.Characteristics.hap.StatusFault.NO_FAULT
56 | })
57 | }
58 |
59 | update (state) {
60 | try {
61 | this.values.lastUpdated = dateToString(state.currentTime)
62 | const bootTime = new Date(state.currentTime).valueOf() - state.uptime * 1000
63 | this.values.lastBoot = dateToString(new Date(bootTime))
64 | this.values.statusFault = state.serverConnected
65 | ? this.Characteristics.hap.StatusFault.NO_FAULT
66 | : this.Characteristics.hap.StatusFault.GENERAL_FAULT
67 | } catch (error) {
68 | this.warn(error)
69 | }
70 | }
71 | }
72 |
73 | NbService.Bridge = Bridge
74 |
--------------------------------------------------------------------------------
/lib/NbService/DoorBell.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService/DoorBell.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { NbService } from '../NbService.js'
9 |
10 | const { dateToString } = NbService
11 |
12 | class DoorBell extends ServiceDelegate {
13 | constructor (nbAccessory, params = {}) {
14 | params.name = nbAccessory.name
15 | params.Service = nbAccessory.Services.hap.Doorbell
16 | super(nbAccessory, params)
17 |
18 | this.addCharacteristicDelegate({
19 | key: 'programmableSwitchEvent',
20 | Characteristic: this.Characteristics.hap.ProgrammableSwitchEvent,
21 | props: {
22 | minValue: this.Characteristics.hap.ProgrammableSwitchEvent.SINGLE_PRESS,
23 | maxValue: this.Characteristics.hap.ProgrammableSwitchEvent.SINGLE_PRESS
24 | }
25 | })
26 | this.addCharacteristicDelegate({
27 | key: 'lastUpdated',
28 | Characteristic: this.Characteristics.my.LastUpdated
29 | })
30 | this.addCharacteristicDelegate({
31 | key: 'enabled',
32 | Characteristic: this.Characteristics.my.Enabled,
33 | value: true
34 | })
35 | }
36 |
37 | update (state) {
38 | if (state.ringactionTimestamp != null) {
39 | const lastUpdated = dateToString(state.ringactionTimestamp)
40 | if (lastUpdated !== this.values.lastUpdated) {
41 | if (/* state.ringactionState && */ this.values.enabled) {
42 | this.values.programmableSwitchEvent =
43 | this.Characteristics.hap.ProgrammableSwitchEvent.SINGLE_PRESS
44 | }
45 | this.values.lastUpdated = lastUpdated
46 | }
47 | }
48 | }
49 | }
50 |
51 | NbService.DoorBell = DoorBell
52 |
--------------------------------------------------------------------------------
/lib/NbService/DoorSensor.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService/DoorSensor.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { NbClient } from 'hb-nb-tools/NbClient'
9 |
10 | import { NbService } from '../NbService.js'
11 |
12 | const { dateToString } = NbService
13 |
14 | class DoorSensor extends ServiceDelegate {
15 | constructor (nbAccessory, params = {}) {
16 | params.name = nbAccessory.name
17 | params.Service = nbAccessory.Services.hap.ContactSensor
18 | params.primaryService = true
19 | super(nbAccessory, params)
20 |
21 | this.addCharacteristicDelegate({
22 | key: 'contact',
23 | Characteristic: this.Characteristics.hap.ContactSensorState
24 | })
25 | this.addCharacteristicDelegate({
26 | key: 'timesOpened',
27 | Characteristic: this.Characteristics.eve.TimesOpened,
28 | value: 0
29 | // silent: true
30 | })
31 | this.addCharacteristicDelegate({
32 | key: 'lastActivation',
33 | Characteristic: this.Characteristics.eve.LastActivation
34 | // silent: true
35 | })
36 | this.addCharacteristicDelegate({
37 | key: 'lastUpdated',
38 | Characteristic: this.Characteristics.my.LastUpdated
39 | })
40 | this.addCharacteristicDelegate({
41 | key: 'statusFault',
42 | Characteristic: this.Characteristics.hap.StatusFault,
43 | value: this.Characteristics.hap.StatusFault.NO_FAULT
44 | })
45 | }
46 |
47 | update (state) {
48 | if (state.doorsensorState != null) {
49 | switch (state.doorsensorState) {
50 | case NbClient.DoorSensorStates.CLOSED:
51 | this.values.contact = this.Characteristics.hap.ContactSensorState.CONTACT_DETECTED
52 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
53 | break
54 | case NbClient.DoorSensorStates.OPEN:
55 | this.values.contact = this.Characteristics.hap.ContactSensorState.CONTACT_NOT_DETECTED
56 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
57 | break
58 | default:
59 | this.values.contact = this.Characteristics.hap.ContactSensorState.CONTACT_DETECTED
60 | this.values.statusFault = this.Characteristics.hap.StatusFault.GENERAL_FAULT
61 | break
62 | }
63 | }
64 | if (state.timestamp != null) {
65 | this.values.lastUpdated = dateToString(state.timestamp)
66 | }
67 | }
68 | }
69 |
70 | NbService.DoorSensor = DoorSensor
71 |
--------------------------------------------------------------------------------
/lib/NbService/Latch.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService/Latch.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { NbClient } from 'hb-nb-tools/NbClient'
9 |
10 | import { NbService } from '../NbService.js'
11 |
12 | class Latch extends ServiceDelegate {
13 | constructor (nbAccessory, params = {}) {
14 | params.name = nbAccessory.name + ' Latch'
15 | params.Service = nbAccessory.Services.hap.LockMechanism
16 | params.subtype = 1
17 | super(nbAccessory, params)
18 |
19 | this.addCharacteristicDelegate({
20 | key: 'currentState',
21 | Characteristic: this.Characteristics.hap.LockCurrentState
22 | })
23 | this.addCharacteristicDelegate({
24 | key: 'targetState',
25 | Characteristic: this.Characteristics.hap.LockTargetState
26 | }).on('didSet', async (value, fromHomeKit) => {
27 | try {
28 | if (!fromHomeKit) {
29 | return
30 | }
31 | if (value === this.Characteristics.hap.LockTargetState.UNSECURED) {
32 | nbAccessory.update({ state: NbClient.LockStates.UNLATCHING })
33 | setTimeout(() => {
34 | nbAccessory.update({ state: NbClient.LockStates.UNLATCHED })
35 | }, 3000)
36 | const response = await nbAccessory.lockAction(NbClient.LockActions.UNLATCH)
37 | if (response != null && response.body.success) {
38 | nbAccessory.update(response.body)
39 | nbAccessory.update({ state: NbClient.LockStates.UNLOCKED })
40 | }
41 | }
42 | } catch (error) { this.error(error) }
43 | })
44 | this.addCharacteristicDelegate({
45 | key: 'label',
46 | Characteristic: this.Characteristics.hap.ServiceLabelIndex,
47 | value: 2
48 | })
49 | }
50 |
51 | update (state) {
52 | if (state.state != null) {
53 | switch (state.state) {
54 | case NbClient.LockStates.UNLATCHED:
55 | this.values.currentState = this.Characteristics.hap.LockCurrentState.UNSECURED
56 | this.values.targetState = this.Characteristics.hap.LockTargetState.UNSECURED
57 | break
58 | case NbClient.LockStates.UNLATCHING:
59 | this.values.currentState = this.Characteristics.hap.LockCurrentState.SECURED
60 | this.values.targetState = this.Characteristics.hap.LockTargetState.UNSECURED
61 | break
62 | case NbClient.LockStates.UNLOCKED:
63 | case NbClient.LockStates.UNLOCKED_LOCK_N_GO:
64 | case NbClient.LockStates.LOCKING:
65 | case NbClient.LockStates.LOCKED:
66 | case NbClient.LockStates.UNLOCKING:
67 | this.values.currentState = this.Characteristics.hap.LockCurrentState.SECURED
68 | this.values.targetState = this.Characteristics.hap.LockTargetState.SECURED
69 | break
70 | case NbClient.LockStates.MOTOR_BLOCKED:
71 | this.values.currentState = this.Characteristics.hap.LockCurrentState.JAMMED
72 | break
73 | case NbClient.LockStates.UNDEFINED:
74 | default:
75 | this.values.currentState = this.Characteristics.hap.LockCurrentState.UNKNOWN
76 | break
77 | }
78 | }
79 | }
80 | }
81 |
82 | NbService.Latch = Latch
83 |
--------------------------------------------------------------------------------
/lib/NbService/Opener.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService/Opener.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { NbClient } from 'hb-nb-tools/NbClient'
9 |
10 | import { NbService } from '../NbService.js'
11 |
12 | const { dateToString } = NbService
13 |
14 | class Opener extends ServiceDelegate {
15 | constructor (nbAccessory, params = {}) {
16 | params.name = nbAccessory.name
17 | params.Service = nbAccessory.Services.hap.LockMechanism
18 | params.primaryService = true
19 | super(nbAccessory, params)
20 |
21 | this.addCharacteristicDelegate({
22 | key: 'currentState',
23 | Characteristic: this.Characteristics.hap.LockCurrentState,
24 | value: this.Characteristics.hap.LockCurrentState.SECURED
25 | })
26 | this.addCharacteristicDelegate({
27 | key: 'targetState',
28 | Characteristic: this.Characteristics.hap.LockTargetState,
29 | value: this.Characteristics.hap.LockTargetState.SECURED
30 | }).on('didSet', async (value, fromHomeKit) => {
31 | try {
32 | if (!fromHomeKit) {
33 | return
34 | }
35 | if (value === this.Characteristics.hap.LockTargetState.UNSECURED) {
36 | const response = await nbAccessory.lockAction(NbClient.OpenerActions.OPEN)
37 | if (response != null && response.body.success) {
38 | this.values.currentState = value
39 | nbAccessory.update(response.body)
40 | }
41 | }
42 | if (this.platform.config.openerResetTimeout > 0) {
43 | setTimeout(() => {
44 | this.values.currentState = this.Characteristics.hap.LockCurrentState.SECURED
45 | this.values.targetState = this.Characteristics.hap.LockTargetState.SECURED
46 | }, this.platform.config.openerResetTimeout)
47 | }
48 | } catch (error) { this.error(error) }
49 | })
50 | this.addCharacteristicDelegate({
51 | key: 'rto',
52 | Characteristic: this.Characteristics.my.RingToOpen
53 | }).on('didSet', async (value, fromHomeKit) => {
54 | try {
55 | if (!fromHomeKit) {
56 | return
57 | }
58 | await nbAccessory.lockAction(
59 | value
60 | ? NbClient.OpenerActions.ACTIVATE_RTO
61 | : NbClient.OpenerActions.DEACTIVATE_RTO
62 | )
63 | } catch (error) { this.error(error) }
64 | })
65 | this.addCharacteristicDelegate({
66 | key: 'cm',
67 | Characteristic: this.Characteristics.my.ContinuousMode
68 | }).on('didSet', async (value, fromHomeKit) => {
69 | try {
70 | if (!fromHomeKit) {
71 | return
72 | }
73 | await nbAccessory.lockAction(
74 | value
75 | ? NbClient.OpenerActions.ACTIVATE_CM
76 | : NbClient.OpenerActions.DEACTIVATE_CM
77 | )
78 | } catch (error) { this.error(error) }
79 | })
80 | this.addCharacteristicDelegate({
81 | key: 'lastUpdated',
82 | Characteristic: this.Characteristics.my.LastUpdated
83 | })
84 | this.addCharacteristicDelegate({
85 | key: 'statusFault',
86 | Characteristic: this.Characteristics.hap.StatusFault,
87 | value: this.Characteristics.hap.StatusFault.NO_FAULT
88 | })
89 | }
90 |
91 | update (state) {
92 | if (state.mode != null) {
93 | this.values.cm = state.mode === NbClient.OpenerModes.CONTINUOUS_MODE
94 | }
95 | if (state.state != null) {
96 | switch (state.state) {
97 | case NbClient.OpenerStates.ONLINE:
98 | case NbClient.OpenerStates.RTO_ACTIVE:
99 | this.values.currentState = this.Characteristics.hap.LockCurrentState.SECURED
100 | this.values.targetState = this.Characteristics.hap.LockTargetState.SECURED
101 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
102 | break
103 | case NbClient.OpenerStates.OPEN:
104 | this.values.currentState = this.Characteristics.hap.LockCurrentState.UNSECURED
105 | this.values.targetState = this.Characteristics.hap.LockTargetState.UNSECURED
106 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
107 | break
108 | case NbClient.OpenerStates.OPENING:
109 | this.values.currentState = this.Characteristics.hap.LockCurrentState.SECURED
110 | this.values.targetState = this.Characteristics.hap.LockTargetState.UNSECURED
111 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
112 | break
113 | case NbClient.OpenerStates.UNTRAINED:
114 | case NbClient.OpenerStates.BOOT_RUN:
115 | case NbClient.OpenerStates.UNDEFINED:
116 | default:
117 | this.values.currentState = this.Characteristics.hap.LockCurrentState.UNKNOWN
118 | this.values.statusFault = this.Characteristics.hap.StatusFault.GENERAL_FAULT
119 | break
120 | }
121 | this.values.rto = state.state === NbClient.OpenerStates.RTO_ACTIVE
122 | }
123 | if (state.timestamp != null) {
124 | this.values.lastUpdated = dateToString(state.timestamp)
125 | }
126 | }
127 | }
128 |
129 | NbService.Opener = Opener
130 |
--------------------------------------------------------------------------------
/lib/NbService/SmartLock.js:
--------------------------------------------------------------------------------
1 | // homebridge-nb/lib/NbService/SmartLock.js
2 | // Copyright © 2020-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plug-in for Nuki Bridge.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { NbClient } from 'hb-nb-tools/NbClient'
9 |
10 | import { NbService } from '../NbService.js'
11 |
12 | const { dateToString } = NbService
13 |
14 | class SmartLock extends ServiceDelegate {
15 | constructor (nbAccessory, params = {}) {
16 | params.name = nbAccessory.name
17 | params.Service = nbAccessory.Services.hap.LockMechanism
18 | params.primaryService = true
19 | super(nbAccessory, params)
20 |
21 | this.addCharacteristicDelegate({
22 | key: 'currentState',
23 | Characteristic: this.Characteristics.hap.LockCurrentState
24 | })
25 | this.addCharacteristicDelegate({
26 | key: 'targetState',
27 | Characteristic: this.Characteristics.hap.LockTargetState
28 | }).on('didSet', async (value, fromHomeKit) => {
29 | try {
30 | if (!fromHomeKit) {
31 | return
32 | }
33 | const response = await nbAccessory.lockAction(
34 | value === this.Characteristics.hap.LockTargetState.UNSECURED
35 | ? NbClient.LockActions.UNLOCK
36 | : NbClient.LockActions.LOCK
37 | )
38 | if (response != null && response.body.success) {
39 | this.values.currentState = value
40 | nbAccessory.update(response.body)
41 | }
42 | } catch (error) { this.error(error) }
43 | // HomeKit sends a dozen or so notifications under iOS 18 when just moving the slider.
44 | // }).on('didTouch', async (value, fromHomeKit) => {
45 | // try {
46 | // if (!fromHomeKit) {
47 | // return
48 | // }
49 | // if (value === this.Characteristics.hap.LockTargetState.UNSECURED) {
50 | // await nbAccessory.lockAction(NbClient.LockActions.UNLATCH)
51 | // }
52 | // } catch (error) { this.error(error) }
53 | })
54 | this.addCharacteristicDelegate({
55 | key: 'unlatch',
56 | Characteristic: this.Characteristics.my.Unlatch,
57 | value: false
58 | }).on('didSet', async (value, fromHomeKit) => {
59 | try {
60 | if (!fromHomeKit) {
61 | return
62 | }
63 | if (value) {
64 | nbAccessory.update({ state: NbClient.LockStates.UNLATCHING })
65 | setTimeout(() => {
66 | nbAccessory.update({ state: NbClient.LockStates.UNLATCHED })
67 | }, 3000)
68 | const response = await nbAccessory.lockAction(NbClient.LockActions.UNLATCH)
69 | if (response != null && response.body.success) {
70 | nbAccessory.update(response.body)
71 | nbAccessory.update({ state: NbClient.LockStates.UNLOCKED })
72 | }
73 | }
74 | } catch (error) { this.error(error) }
75 | })
76 | this.addCharacteristicDelegate({
77 | key: 'lastUpdated',
78 | Characteristic: this.Characteristics.my.LastUpdated
79 | })
80 | this.addCharacteristicDelegate({
81 | key: 'statusFault',
82 | Characteristic: this.Characteristics.hap.StatusFault,
83 | value: this.Characteristics.hap.StatusFault.NO_FAULT
84 | })
85 | if (this.platform.config.latch) {
86 | this.addCharacteristicDelegate({
87 | key: 'label',
88 | Characteristic: this.Characteristics.hap.ServiceLabelIndex,
89 | value: 1
90 | })
91 | }
92 | }
93 |
94 | update (state) {
95 | if (state.state != null) {
96 | // Workaround: bridge state isn't always updated
97 | this.needRefresh = this.previousState === state.state && [
98 | NbClient.LockStates.UNLOCKED_LOCK_N_GO,
99 | NbClient.LockStates.LOCKING,
100 | NbClient.LockStates.UNLOCKING,
101 | NbClient.LockStates.UNLATCHING
102 | ].includes(state.state)
103 | this.previousState = state.state
104 | // End workaround
105 | switch (state.state) {
106 | case NbClient.LockStates.UNLOCKED:
107 | case NbClient.LockStates.UNLATCHED:
108 | case NbClient.LockStates.UNLOCKED_LOCK_N_GO:
109 | this.values.currentState = this.Characteristics.hap.LockCurrentState.UNSECURED
110 | this.values.targetState = this.Characteristics.hap.LockTargetState.UNSECURED
111 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
112 | break
113 | case NbClient.LockStates.LOCKING:
114 | this.values.targetState = this.Characteristics.hap.LockTargetState.SECURED
115 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
116 | break
117 | case NbClient.LockStates.LOCKED:
118 | this.values.currentState = this.Characteristics.hap.LockCurrentState.SECURED
119 | this.values.targetState = this.Characteristics.hap.LockTargetState.SECURED
120 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
121 | break
122 | case NbClient.LockStates.UNLOCKING:
123 | case NbClient.LockStates.UNLATCHING:
124 | this.values.targetState = this.Characteristics.hap.LockTargetState.UNSECURED
125 | this.values.statusFault = this.Characteristics.hap.StatusFault.NO_FAULT
126 | break
127 | case NbClient.LockStates.MOTOR_BLOCKED:
128 | this.values.currentState = this.Characteristics.hap.LockCurrentState.JAMMED
129 | this.values.statusFault = this.Characteristics.hap.StatusFault.GENERAL_FAULT
130 | break
131 | case NbClient.LockStates.UNDEFINED:
132 | default:
133 | this.values.currentState = this.Characteristics.hap.LockCurrentState.UNKNOWN
134 | this.values.statusFault = this.Characteristics.hap.StatusFault.GENERAL_FAULT
135 | break
136 | }
137 | this.values.unlatch = [
138 | NbClient.LockStates.UNLATCHED, NbClient.LockStates.UNLATCHING
139 | ].includes(state.state)
140 | }
141 | if (state.timestamp != null) {
142 | this.values.lastUpdated = dateToString(state.timestamp)
143 | }
144 | }
145 | }
146 |
147 | NbService.SmartLock = SmartLock
148 |
--------------------------------------------------------------------------------
/nb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebaauw/homebridge-nb/54c473b9f01f6cae3c89287085f5234aa2271a33/nb.png
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "homebridge-nb",
3 | "version": "1.5.2",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "homebridge-nb",
9 | "version": "1.5.2",
10 | "funding": [
11 | {
12 | "type": "github",
13 | "url": "https://github.com/sponsors/ebaauw"
14 | },
15 | {
16 | "type": "paypal",
17 | "url": "https://www.paypal.me/ebaauw/EUR"
18 | }
19 | ],
20 | "license": "Apache-2.0",
21 | "dependencies": {
22 | "hb-nb-tools": "~2.0.13",
23 | "homebridge-lib": "~7.1.5"
24 | },
25 | "bin": {
26 | "nb": "cli/nb.js"
27 | },
28 | "engines": {
29 | "homebridge": "^1.9.0||^2.0.0-beta",
30 | "node": "^22||^20||^18",
31 | "nuki": "2.18.0"
32 | }
33 | },
34 | "node_modules/@homebridge/plugin-ui-utils": {
35 | "version": "2.0.2",
36 | "resolved": "https://registry.npmjs.org/@homebridge/plugin-ui-utils/-/plugin-ui-utils-2.0.2.tgz",
37 | "integrity": "sha512-2o81veVuJ09GSm/epnw8Mn6CLpyqdqV7AclHt3psTYyaKdwNw3cGLpEyEteVNBwOU/ChMDNMNzJpeQ8pvlLojg==",
38 | "license": "MIT"
39 | },
40 | "node_modules/@leichtgewicht/ip-codec": {
41 | "version": "2.0.5",
42 | "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
43 | "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
44 | "license": "MIT"
45 | },
46 | "node_modules/array-buffer-byte-length": {
47 | "version": "1.0.2",
48 | "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
49 | "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
50 | "license": "MIT",
51 | "dependencies": {
52 | "call-bound": "^1.0.3",
53 | "is-array-buffer": "^3.0.5"
54 | },
55 | "engines": {
56 | "node": ">= 0.4"
57 | },
58 | "funding": {
59 | "url": "https://github.com/sponsors/ljharb"
60 | }
61 | },
62 | "node_modules/array-flatten": {
63 | "version": "3.0.0",
64 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
65 | "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==",
66 | "license": "MIT"
67 | },
68 | "node_modules/available-typed-arrays": {
69 | "version": "1.0.7",
70 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
71 | "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
72 | "license": "MIT",
73 | "dependencies": {
74 | "possible-typed-array-names": "^1.0.0"
75 | },
76 | "engines": {
77 | "node": ">= 0.4"
78 | },
79 | "funding": {
80 | "url": "https://github.com/sponsors/ljharb"
81 | }
82 | },
83 | "node_modules/bonjour-hap": {
84 | "version": "3.9.0",
85 | "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.9.0.tgz",
86 | "integrity": "sha512-g/25iC9U3vYCwR8NvspPJhsl8kNgVSsXPbgAFO/+Gm0x6kn33XCL6CMvg79ZViAAo0NZRHqa5VR52eUw1zE2IA==",
87 | "license": "MIT",
88 | "dependencies": {
89 | "array-flatten": "^3.0.0",
90 | "deep-equal": "^2.2.3",
91 | "multicast-dns": "^7.2.5",
92 | "multicast-dns-service-types": "^1.1.0"
93 | }
94 | },
95 | "node_modules/call-bind": {
96 | "version": "1.0.8",
97 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
98 | "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
99 | "license": "MIT",
100 | "dependencies": {
101 | "call-bind-apply-helpers": "^1.0.0",
102 | "es-define-property": "^1.0.0",
103 | "get-intrinsic": "^1.2.4",
104 | "set-function-length": "^1.2.2"
105 | },
106 | "engines": {
107 | "node": ">= 0.4"
108 | },
109 | "funding": {
110 | "url": "https://github.com/sponsors/ljharb"
111 | }
112 | },
113 | "node_modules/call-bind-apply-helpers": {
114 | "version": "1.0.2",
115 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
116 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
117 | "license": "MIT",
118 | "dependencies": {
119 | "es-errors": "^1.3.0",
120 | "function-bind": "^1.1.2"
121 | },
122 | "engines": {
123 | "node": ">= 0.4"
124 | }
125 | },
126 | "node_modules/call-bound": {
127 | "version": "1.0.4",
128 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
129 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
130 | "license": "MIT",
131 | "dependencies": {
132 | "call-bind-apply-helpers": "^1.0.2",
133 | "get-intrinsic": "^1.3.0"
134 | },
135 | "engines": {
136 | "node": ">= 0.4"
137 | },
138 | "funding": {
139 | "url": "https://github.com/sponsors/ljharb"
140 | }
141 | },
142 | "node_modules/chalk": {
143 | "version": "5.4.1",
144 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
145 | "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
146 | "license": "MIT",
147 | "engines": {
148 | "node": "^12.17.0 || ^14.13 || >=16.0.0"
149 | },
150 | "funding": {
151 | "url": "https://github.com/chalk/chalk?sponsor=1"
152 | }
153 | },
154 | "node_modules/deep-equal": {
155 | "version": "2.2.3",
156 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
157 | "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
158 | "license": "MIT",
159 | "dependencies": {
160 | "array-buffer-byte-length": "^1.0.0",
161 | "call-bind": "^1.0.5",
162 | "es-get-iterator": "^1.1.3",
163 | "get-intrinsic": "^1.2.2",
164 | "is-arguments": "^1.1.1",
165 | "is-array-buffer": "^3.0.2",
166 | "is-date-object": "^1.0.5",
167 | "is-regex": "^1.1.4",
168 | "is-shared-array-buffer": "^1.0.2",
169 | "isarray": "^2.0.5",
170 | "object-is": "^1.1.5",
171 | "object-keys": "^1.1.1",
172 | "object.assign": "^4.1.4",
173 | "regexp.prototype.flags": "^1.5.1",
174 | "side-channel": "^1.0.4",
175 | "which-boxed-primitive": "^1.0.2",
176 | "which-collection": "^1.0.1",
177 | "which-typed-array": "^1.1.13"
178 | },
179 | "engines": {
180 | "node": ">= 0.4"
181 | },
182 | "funding": {
183 | "url": "https://github.com/sponsors/ljharb"
184 | }
185 | },
186 | "node_modules/define-data-property": {
187 | "version": "1.1.4",
188 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
189 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
190 | "license": "MIT",
191 | "dependencies": {
192 | "es-define-property": "^1.0.0",
193 | "es-errors": "^1.3.0",
194 | "gopd": "^1.0.1"
195 | },
196 | "engines": {
197 | "node": ">= 0.4"
198 | },
199 | "funding": {
200 | "url": "https://github.com/sponsors/ljharb"
201 | }
202 | },
203 | "node_modules/define-properties": {
204 | "version": "1.2.1",
205 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
206 | "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
207 | "license": "MIT",
208 | "dependencies": {
209 | "define-data-property": "^1.0.1",
210 | "has-property-descriptors": "^1.0.0",
211 | "object-keys": "^1.1.1"
212 | },
213 | "engines": {
214 | "node": ">= 0.4"
215 | },
216 | "funding": {
217 | "url": "https://github.com/sponsors/ljharb"
218 | }
219 | },
220 | "node_modules/dns-packet": {
221 | "version": "5.6.1",
222 | "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
223 | "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
224 | "license": "MIT",
225 | "dependencies": {
226 | "@leichtgewicht/ip-codec": "^2.0.1"
227 | },
228 | "engines": {
229 | "node": ">=6"
230 | }
231 | },
232 | "node_modules/dunder-proto": {
233 | "version": "1.0.1",
234 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
235 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
236 | "license": "MIT",
237 | "dependencies": {
238 | "call-bind-apply-helpers": "^1.0.1",
239 | "es-errors": "^1.3.0",
240 | "gopd": "^1.2.0"
241 | },
242 | "engines": {
243 | "node": ">= 0.4"
244 | }
245 | },
246 | "node_modules/es-define-property": {
247 | "version": "1.0.1",
248 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
249 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
250 | "license": "MIT",
251 | "engines": {
252 | "node": ">= 0.4"
253 | }
254 | },
255 | "node_modules/es-errors": {
256 | "version": "1.3.0",
257 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
258 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
259 | "license": "MIT",
260 | "engines": {
261 | "node": ">= 0.4"
262 | }
263 | },
264 | "node_modules/es-get-iterator": {
265 | "version": "1.1.3",
266 | "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
267 | "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
268 | "license": "MIT",
269 | "dependencies": {
270 | "call-bind": "^1.0.2",
271 | "get-intrinsic": "^1.1.3",
272 | "has-symbols": "^1.0.3",
273 | "is-arguments": "^1.1.1",
274 | "is-map": "^2.0.2",
275 | "is-set": "^2.0.2",
276 | "is-string": "^1.0.7",
277 | "isarray": "^2.0.5",
278 | "stop-iteration-iterator": "^1.0.0"
279 | },
280 | "funding": {
281 | "url": "https://github.com/sponsors/ljharb"
282 | }
283 | },
284 | "node_modules/es-object-atoms": {
285 | "version": "1.1.1",
286 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
287 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
288 | "license": "MIT",
289 | "dependencies": {
290 | "es-errors": "^1.3.0"
291 | },
292 | "engines": {
293 | "node": ">= 0.4"
294 | }
295 | },
296 | "node_modules/for-each": {
297 | "version": "0.3.5",
298 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
299 | "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
300 | "license": "MIT",
301 | "dependencies": {
302 | "is-callable": "^1.2.7"
303 | },
304 | "engines": {
305 | "node": ">= 0.4"
306 | },
307 | "funding": {
308 | "url": "https://github.com/sponsors/ljharb"
309 | }
310 | },
311 | "node_modules/function-bind": {
312 | "version": "1.1.2",
313 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
314 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
315 | "license": "MIT",
316 | "funding": {
317 | "url": "https://github.com/sponsors/ljharb"
318 | }
319 | },
320 | "node_modules/functions-have-names": {
321 | "version": "1.2.3",
322 | "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
323 | "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
324 | "license": "MIT",
325 | "funding": {
326 | "url": "https://github.com/sponsors/ljharb"
327 | }
328 | },
329 | "node_modules/get-intrinsic": {
330 | "version": "1.3.0",
331 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
332 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
333 | "license": "MIT",
334 | "dependencies": {
335 | "call-bind-apply-helpers": "^1.0.2",
336 | "es-define-property": "^1.0.1",
337 | "es-errors": "^1.3.0",
338 | "es-object-atoms": "^1.1.1",
339 | "function-bind": "^1.1.2",
340 | "get-proto": "^1.0.1",
341 | "gopd": "^1.2.0",
342 | "has-symbols": "^1.1.0",
343 | "hasown": "^2.0.2",
344 | "math-intrinsics": "^1.1.0"
345 | },
346 | "engines": {
347 | "node": ">= 0.4"
348 | },
349 | "funding": {
350 | "url": "https://github.com/sponsors/ljharb"
351 | }
352 | },
353 | "node_modules/get-proto": {
354 | "version": "1.0.1",
355 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
356 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
357 | "license": "MIT",
358 | "dependencies": {
359 | "dunder-proto": "^1.0.1",
360 | "es-object-atoms": "^1.0.0"
361 | },
362 | "engines": {
363 | "node": ">= 0.4"
364 | }
365 | },
366 | "node_modules/gopd": {
367 | "version": "1.2.0",
368 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
369 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
370 | "license": "MIT",
371 | "engines": {
372 | "node": ">= 0.4"
373 | },
374 | "funding": {
375 | "url": "https://github.com/sponsors/ljharb"
376 | }
377 | },
378 | "node_modules/has-bigints": {
379 | "version": "1.1.0",
380 | "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
381 | "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
382 | "license": "MIT",
383 | "engines": {
384 | "node": ">= 0.4"
385 | },
386 | "funding": {
387 | "url": "https://github.com/sponsors/ljharb"
388 | }
389 | },
390 | "node_modules/has-property-descriptors": {
391 | "version": "1.0.2",
392 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
393 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
394 | "license": "MIT",
395 | "dependencies": {
396 | "es-define-property": "^1.0.0"
397 | },
398 | "funding": {
399 | "url": "https://github.com/sponsors/ljharb"
400 | }
401 | },
402 | "node_modules/has-symbols": {
403 | "version": "1.1.0",
404 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
405 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
406 | "license": "MIT",
407 | "engines": {
408 | "node": ">= 0.4"
409 | },
410 | "funding": {
411 | "url": "https://github.com/sponsors/ljharb"
412 | }
413 | },
414 | "node_modules/has-tostringtag": {
415 | "version": "1.0.2",
416 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
417 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
418 | "license": "MIT",
419 | "dependencies": {
420 | "has-symbols": "^1.0.3"
421 | },
422 | "engines": {
423 | "node": ">= 0.4"
424 | },
425 | "funding": {
426 | "url": "https://github.com/sponsors/ljharb"
427 | }
428 | },
429 | "node_modules/hasown": {
430 | "version": "2.0.2",
431 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
432 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
433 | "license": "MIT",
434 | "dependencies": {
435 | "function-bind": "^1.1.2"
436 | },
437 | "engines": {
438 | "node": ">= 0.4"
439 | }
440 | },
441 | "node_modules/hb-lib-tools": {
442 | "version": "2.2.3",
443 | "resolved": "https://registry.npmjs.org/hb-lib-tools/-/hb-lib-tools-2.2.3.tgz",
444 | "integrity": "sha512-9fuBPJP8HmTGa8ePQ/vBCdo/Sg47pT3RZV/XXAQaJAk+JyAOhqc5ZrPyPEkJieNWwUDm6uGL2Ndp0UJUq+wrKg==",
445 | "license": "Apache-2.0",
446 | "dependencies": {
447 | "bonjour-hap": "^3.9.0",
448 | "chalk": "^5.4.1",
449 | "semver": "^7.7.1"
450 | },
451 | "bin": {
452 | "hap": "cli/hap.js",
453 | "json": "cli/json.js",
454 | "sysinfo": "cli/sysinfo.js",
455 | "upnp": "cli/upnp.js"
456 | },
457 | "engines": {
458 | "node": "22.15.0||^22||^20||^18"
459 | }
460 | },
461 | "node_modules/hb-nb-tools": {
462 | "version": "2.0.13",
463 | "resolved": "https://registry.npmjs.org/hb-nb-tools/-/hb-nb-tools-2.0.13.tgz",
464 | "integrity": "sha512-LMhxLFV9jhf13edrULQUZlEXmKvuKDcJoY2cqq0+/kVDUykut2KZEU7Poz9DtiY11/3iYwKUTanDmDuXd5artA==",
465 | "funding": [
466 | {
467 | "type": "github",
468 | "url": "https://github.com/sponsors/ebaauw"
469 | },
470 | {
471 | "type": "paypal",
472 | "url": "https://www.paypal.me/ebaauw/EUR"
473 | }
474 | ],
475 | "license": "Apache-2.0",
476 | "dependencies": {
477 | "hb-lib-tools": "~2.2.3",
478 | "tweetnacl": "~1.0.3"
479 | },
480 | "bin": {
481 | "nb": "cli/nb.js"
482 | },
483 | "engines": {
484 | "node": "^22||^20||^18"
485 | }
486 | },
487 | "node_modules/homebridge-lib": {
488 | "version": "7.1.5",
489 | "resolved": "https://registry.npmjs.org/homebridge-lib/-/homebridge-lib-7.1.5.tgz",
490 | "integrity": "sha512-lF4n+WQzrXPIRw8CZaJBcZygKjfuE4umJ+LgXWIBuws8B1qHNz6GgO+YU1KJ8dkv1qL77EepoBlqoMAyobYfXg==",
491 | "license": "Apache-2.0",
492 | "dependencies": {
493 | "@homebridge/plugin-ui-utils": "~2.0.2",
494 | "hb-lib-tools": "~2.2.3"
495 | },
496 | "bin": {
497 | "hap": "cli/hap.js",
498 | "json": "cli/json.js",
499 | "sysinfo": "cli/sysinfo.js",
500 | "upnp": "cli/upnp.js"
501 | },
502 | "engines": {
503 | "homebridge": "^1.9.0||^2.0.0-beta",
504 | "node": "22.15.0||^22||^20||^18"
505 | }
506 | },
507 | "node_modules/internal-slot": {
508 | "version": "1.1.0",
509 | "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
510 | "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
511 | "license": "MIT",
512 | "dependencies": {
513 | "es-errors": "^1.3.0",
514 | "hasown": "^2.0.2",
515 | "side-channel": "^1.1.0"
516 | },
517 | "engines": {
518 | "node": ">= 0.4"
519 | }
520 | },
521 | "node_modules/is-arguments": {
522 | "version": "1.2.0",
523 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
524 | "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
525 | "license": "MIT",
526 | "dependencies": {
527 | "call-bound": "^1.0.2",
528 | "has-tostringtag": "^1.0.2"
529 | },
530 | "engines": {
531 | "node": ">= 0.4"
532 | },
533 | "funding": {
534 | "url": "https://github.com/sponsors/ljharb"
535 | }
536 | },
537 | "node_modules/is-array-buffer": {
538 | "version": "3.0.5",
539 | "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
540 | "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
541 | "license": "MIT",
542 | "dependencies": {
543 | "call-bind": "^1.0.8",
544 | "call-bound": "^1.0.3",
545 | "get-intrinsic": "^1.2.6"
546 | },
547 | "engines": {
548 | "node": ">= 0.4"
549 | },
550 | "funding": {
551 | "url": "https://github.com/sponsors/ljharb"
552 | }
553 | },
554 | "node_modules/is-bigint": {
555 | "version": "1.1.0",
556 | "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
557 | "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
558 | "license": "MIT",
559 | "dependencies": {
560 | "has-bigints": "^1.0.2"
561 | },
562 | "engines": {
563 | "node": ">= 0.4"
564 | },
565 | "funding": {
566 | "url": "https://github.com/sponsors/ljharb"
567 | }
568 | },
569 | "node_modules/is-boolean-object": {
570 | "version": "1.2.2",
571 | "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
572 | "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
573 | "license": "MIT",
574 | "dependencies": {
575 | "call-bound": "^1.0.3",
576 | "has-tostringtag": "^1.0.2"
577 | },
578 | "engines": {
579 | "node": ">= 0.4"
580 | },
581 | "funding": {
582 | "url": "https://github.com/sponsors/ljharb"
583 | }
584 | },
585 | "node_modules/is-callable": {
586 | "version": "1.2.7",
587 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
588 | "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
589 | "license": "MIT",
590 | "engines": {
591 | "node": ">= 0.4"
592 | },
593 | "funding": {
594 | "url": "https://github.com/sponsors/ljharb"
595 | }
596 | },
597 | "node_modules/is-date-object": {
598 | "version": "1.1.0",
599 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
600 | "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
601 | "license": "MIT",
602 | "dependencies": {
603 | "call-bound": "^1.0.2",
604 | "has-tostringtag": "^1.0.2"
605 | },
606 | "engines": {
607 | "node": ">= 0.4"
608 | },
609 | "funding": {
610 | "url": "https://github.com/sponsors/ljharb"
611 | }
612 | },
613 | "node_modules/is-map": {
614 | "version": "2.0.3",
615 | "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
616 | "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
617 | "license": "MIT",
618 | "engines": {
619 | "node": ">= 0.4"
620 | },
621 | "funding": {
622 | "url": "https://github.com/sponsors/ljharb"
623 | }
624 | },
625 | "node_modules/is-number-object": {
626 | "version": "1.1.1",
627 | "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
628 | "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
629 | "license": "MIT",
630 | "dependencies": {
631 | "call-bound": "^1.0.3",
632 | "has-tostringtag": "^1.0.2"
633 | },
634 | "engines": {
635 | "node": ">= 0.4"
636 | },
637 | "funding": {
638 | "url": "https://github.com/sponsors/ljharb"
639 | }
640 | },
641 | "node_modules/is-regex": {
642 | "version": "1.2.1",
643 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
644 | "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
645 | "license": "MIT",
646 | "dependencies": {
647 | "call-bound": "^1.0.2",
648 | "gopd": "^1.2.0",
649 | "has-tostringtag": "^1.0.2",
650 | "hasown": "^2.0.2"
651 | },
652 | "engines": {
653 | "node": ">= 0.4"
654 | },
655 | "funding": {
656 | "url": "https://github.com/sponsors/ljharb"
657 | }
658 | },
659 | "node_modules/is-set": {
660 | "version": "2.0.3",
661 | "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
662 | "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
663 | "license": "MIT",
664 | "engines": {
665 | "node": ">= 0.4"
666 | },
667 | "funding": {
668 | "url": "https://github.com/sponsors/ljharb"
669 | }
670 | },
671 | "node_modules/is-shared-array-buffer": {
672 | "version": "1.0.4",
673 | "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
674 | "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
675 | "license": "MIT",
676 | "dependencies": {
677 | "call-bound": "^1.0.3"
678 | },
679 | "engines": {
680 | "node": ">= 0.4"
681 | },
682 | "funding": {
683 | "url": "https://github.com/sponsors/ljharb"
684 | }
685 | },
686 | "node_modules/is-string": {
687 | "version": "1.1.1",
688 | "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
689 | "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
690 | "license": "MIT",
691 | "dependencies": {
692 | "call-bound": "^1.0.3",
693 | "has-tostringtag": "^1.0.2"
694 | },
695 | "engines": {
696 | "node": ">= 0.4"
697 | },
698 | "funding": {
699 | "url": "https://github.com/sponsors/ljharb"
700 | }
701 | },
702 | "node_modules/is-symbol": {
703 | "version": "1.1.1",
704 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
705 | "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
706 | "license": "MIT",
707 | "dependencies": {
708 | "call-bound": "^1.0.2",
709 | "has-symbols": "^1.1.0",
710 | "safe-regex-test": "^1.1.0"
711 | },
712 | "engines": {
713 | "node": ">= 0.4"
714 | },
715 | "funding": {
716 | "url": "https://github.com/sponsors/ljharb"
717 | }
718 | },
719 | "node_modules/is-weakmap": {
720 | "version": "2.0.2",
721 | "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
722 | "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
723 | "license": "MIT",
724 | "engines": {
725 | "node": ">= 0.4"
726 | },
727 | "funding": {
728 | "url": "https://github.com/sponsors/ljharb"
729 | }
730 | },
731 | "node_modules/is-weakset": {
732 | "version": "2.0.4",
733 | "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
734 | "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
735 | "license": "MIT",
736 | "dependencies": {
737 | "call-bound": "^1.0.3",
738 | "get-intrinsic": "^1.2.6"
739 | },
740 | "engines": {
741 | "node": ">= 0.4"
742 | },
743 | "funding": {
744 | "url": "https://github.com/sponsors/ljharb"
745 | }
746 | },
747 | "node_modules/isarray": {
748 | "version": "2.0.5",
749 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
750 | "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
751 | "license": "MIT"
752 | },
753 | "node_modules/math-intrinsics": {
754 | "version": "1.1.0",
755 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
756 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
757 | "license": "MIT",
758 | "engines": {
759 | "node": ">= 0.4"
760 | }
761 | },
762 | "node_modules/multicast-dns": {
763 | "version": "7.2.5",
764 | "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
765 | "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
766 | "license": "MIT",
767 | "dependencies": {
768 | "dns-packet": "^5.2.2",
769 | "thunky": "^1.0.2"
770 | },
771 | "bin": {
772 | "multicast-dns": "cli.js"
773 | }
774 | },
775 | "node_modules/multicast-dns-service-types": {
776 | "version": "1.1.0",
777 | "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
778 | "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
779 | "license": "MIT"
780 | },
781 | "node_modules/object-inspect": {
782 | "version": "1.13.4",
783 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
784 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
785 | "license": "MIT",
786 | "engines": {
787 | "node": ">= 0.4"
788 | },
789 | "funding": {
790 | "url": "https://github.com/sponsors/ljharb"
791 | }
792 | },
793 | "node_modules/object-is": {
794 | "version": "1.1.6",
795 | "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
796 | "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
797 | "license": "MIT",
798 | "dependencies": {
799 | "call-bind": "^1.0.7",
800 | "define-properties": "^1.2.1"
801 | },
802 | "engines": {
803 | "node": ">= 0.4"
804 | },
805 | "funding": {
806 | "url": "https://github.com/sponsors/ljharb"
807 | }
808 | },
809 | "node_modules/object-keys": {
810 | "version": "1.1.1",
811 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
812 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
813 | "license": "MIT",
814 | "engines": {
815 | "node": ">= 0.4"
816 | }
817 | },
818 | "node_modules/object.assign": {
819 | "version": "4.1.7",
820 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
821 | "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
822 | "license": "MIT",
823 | "dependencies": {
824 | "call-bind": "^1.0.8",
825 | "call-bound": "^1.0.3",
826 | "define-properties": "^1.2.1",
827 | "es-object-atoms": "^1.0.0",
828 | "has-symbols": "^1.1.0",
829 | "object-keys": "^1.1.1"
830 | },
831 | "engines": {
832 | "node": ">= 0.4"
833 | },
834 | "funding": {
835 | "url": "https://github.com/sponsors/ljharb"
836 | }
837 | },
838 | "node_modules/possible-typed-array-names": {
839 | "version": "1.1.0",
840 | "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
841 | "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
842 | "license": "MIT",
843 | "engines": {
844 | "node": ">= 0.4"
845 | }
846 | },
847 | "node_modules/regexp.prototype.flags": {
848 | "version": "1.5.4",
849 | "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
850 | "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
851 | "license": "MIT",
852 | "dependencies": {
853 | "call-bind": "^1.0.8",
854 | "define-properties": "^1.2.1",
855 | "es-errors": "^1.3.0",
856 | "get-proto": "^1.0.1",
857 | "gopd": "^1.2.0",
858 | "set-function-name": "^2.0.2"
859 | },
860 | "engines": {
861 | "node": ">= 0.4"
862 | },
863 | "funding": {
864 | "url": "https://github.com/sponsors/ljharb"
865 | }
866 | },
867 | "node_modules/safe-regex-test": {
868 | "version": "1.1.0",
869 | "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
870 | "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
871 | "license": "MIT",
872 | "dependencies": {
873 | "call-bound": "^1.0.2",
874 | "es-errors": "^1.3.0",
875 | "is-regex": "^1.2.1"
876 | },
877 | "engines": {
878 | "node": ">= 0.4"
879 | },
880 | "funding": {
881 | "url": "https://github.com/sponsors/ljharb"
882 | }
883 | },
884 | "node_modules/semver": {
885 | "version": "7.7.1",
886 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
887 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
888 | "license": "ISC",
889 | "bin": {
890 | "semver": "bin/semver.js"
891 | },
892 | "engines": {
893 | "node": ">=10"
894 | }
895 | },
896 | "node_modules/set-function-length": {
897 | "version": "1.2.2",
898 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
899 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
900 | "license": "MIT",
901 | "dependencies": {
902 | "define-data-property": "^1.1.4",
903 | "es-errors": "^1.3.0",
904 | "function-bind": "^1.1.2",
905 | "get-intrinsic": "^1.2.4",
906 | "gopd": "^1.0.1",
907 | "has-property-descriptors": "^1.0.2"
908 | },
909 | "engines": {
910 | "node": ">= 0.4"
911 | }
912 | },
913 | "node_modules/set-function-name": {
914 | "version": "2.0.2",
915 | "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
916 | "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
917 | "license": "MIT",
918 | "dependencies": {
919 | "define-data-property": "^1.1.4",
920 | "es-errors": "^1.3.0",
921 | "functions-have-names": "^1.2.3",
922 | "has-property-descriptors": "^1.0.2"
923 | },
924 | "engines": {
925 | "node": ">= 0.4"
926 | }
927 | },
928 | "node_modules/side-channel": {
929 | "version": "1.1.0",
930 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
931 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
932 | "license": "MIT",
933 | "dependencies": {
934 | "es-errors": "^1.3.0",
935 | "object-inspect": "^1.13.3",
936 | "side-channel-list": "^1.0.0",
937 | "side-channel-map": "^1.0.1",
938 | "side-channel-weakmap": "^1.0.2"
939 | },
940 | "engines": {
941 | "node": ">= 0.4"
942 | },
943 | "funding": {
944 | "url": "https://github.com/sponsors/ljharb"
945 | }
946 | },
947 | "node_modules/side-channel-list": {
948 | "version": "1.0.0",
949 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
950 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
951 | "license": "MIT",
952 | "dependencies": {
953 | "es-errors": "^1.3.0",
954 | "object-inspect": "^1.13.3"
955 | },
956 | "engines": {
957 | "node": ">= 0.4"
958 | },
959 | "funding": {
960 | "url": "https://github.com/sponsors/ljharb"
961 | }
962 | },
963 | "node_modules/side-channel-map": {
964 | "version": "1.0.1",
965 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
966 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
967 | "license": "MIT",
968 | "dependencies": {
969 | "call-bound": "^1.0.2",
970 | "es-errors": "^1.3.0",
971 | "get-intrinsic": "^1.2.5",
972 | "object-inspect": "^1.13.3"
973 | },
974 | "engines": {
975 | "node": ">= 0.4"
976 | },
977 | "funding": {
978 | "url": "https://github.com/sponsors/ljharb"
979 | }
980 | },
981 | "node_modules/side-channel-weakmap": {
982 | "version": "1.0.2",
983 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
984 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
985 | "license": "MIT",
986 | "dependencies": {
987 | "call-bound": "^1.0.2",
988 | "es-errors": "^1.3.0",
989 | "get-intrinsic": "^1.2.5",
990 | "object-inspect": "^1.13.3",
991 | "side-channel-map": "^1.0.1"
992 | },
993 | "engines": {
994 | "node": ">= 0.4"
995 | },
996 | "funding": {
997 | "url": "https://github.com/sponsors/ljharb"
998 | }
999 | },
1000 | "node_modules/stop-iteration-iterator": {
1001 | "version": "1.1.0",
1002 | "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
1003 | "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
1004 | "license": "MIT",
1005 | "dependencies": {
1006 | "es-errors": "^1.3.0",
1007 | "internal-slot": "^1.1.0"
1008 | },
1009 | "engines": {
1010 | "node": ">= 0.4"
1011 | }
1012 | },
1013 | "node_modules/thunky": {
1014 | "version": "1.1.0",
1015 | "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
1016 | "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
1017 | "license": "MIT"
1018 | },
1019 | "node_modules/tweetnacl": {
1020 | "version": "1.0.3",
1021 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
1022 | "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
1023 | "license": "Unlicense"
1024 | },
1025 | "node_modules/which-boxed-primitive": {
1026 | "version": "1.1.1",
1027 | "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
1028 | "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
1029 | "license": "MIT",
1030 | "dependencies": {
1031 | "is-bigint": "^1.1.0",
1032 | "is-boolean-object": "^1.2.1",
1033 | "is-number-object": "^1.1.1",
1034 | "is-string": "^1.1.1",
1035 | "is-symbol": "^1.1.1"
1036 | },
1037 | "engines": {
1038 | "node": ">= 0.4"
1039 | },
1040 | "funding": {
1041 | "url": "https://github.com/sponsors/ljharb"
1042 | }
1043 | },
1044 | "node_modules/which-collection": {
1045 | "version": "1.0.2",
1046 | "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
1047 | "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
1048 | "license": "MIT",
1049 | "dependencies": {
1050 | "is-map": "^2.0.3",
1051 | "is-set": "^2.0.3",
1052 | "is-weakmap": "^2.0.2",
1053 | "is-weakset": "^2.0.3"
1054 | },
1055 | "engines": {
1056 | "node": ">= 0.4"
1057 | },
1058 | "funding": {
1059 | "url": "https://github.com/sponsors/ljharb"
1060 | }
1061 | },
1062 | "node_modules/which-typed-array": {
1063 | "version": "1.1.19",
1064 | "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
1065 | "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
1066 | "license": "MIT",
1067 | "dependencies": {
1068 | "available-typed-arrays": "^1.0.7",
1069 | "call-bind": "^1.0.8",
1070 | "call-bound": "^1.0.4",
1071 | "for-each": "^0.3.5",
1072 | "get-proto": "^1.0.1",
1073 | "gopd": "^1.2.0",
1074 | "has-tostringtag": "^1.0.2"
1075 | },
1076 | "engines": {
1077 | "node": ">= 0.4"
1078 | },
1079 | "funding": {
1080 | "url": "https://github.com/sponsors/ljharb"
1081 | }
1082 | }
1083 | }
1084 | }
1085 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "homebridge-nb",
3 | "description": "Homebridge plugin for Nuki Bridge",
4 | "displayName": "Homebridge NB",
5 | "author": "Erik Baauw",
6 | "maintainers": [
7 | "ebaauw"
8 | ],
9 | "license": "Apache-2.0",
10 | "version": "1.5.2",
11 | "keywords": [
12 | "homebridge-plugin",
13 | "homekit",
14 | "nuki",
15 | "smart-lock",
16 | "lock",
17 | "opener"
18 | ],
19 | "type": "module",
20 | "main": "index.js",
21 | "bin": {
22 | "nb": "cli/nb.js"
23 | },
24 | "engines": {
25 | "homebridge": "^1.9.0||^2.0.0-beta",
26 | "node": "^22||^20||^18",
27 | "nuki": "2.18.0"
28 | },
29 | "dependencies": {
30 | "homebridge-lib": "~7.1.5",
31 | "hb-nb-tools": "~2.0.13"
32 | },
33 | "scripts": {
34 | "prepare": "standard",
35 | "test": "standard && echo \"Error: no test specified\" && exit 1"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/ebaauw/homebridge-nb.git"
40 | },
41 | "bugs": {
42 | "url": "https://github.com/ebaauw/homebridge-nb/issues"
43 | },
44 | "homepage": "https://github.com/ebaauw/homebridge-nb#readme",
45 | "funding": [
46 | {
47 | "type": "github",
48 | "url": "https://github.com/sponsors/ebaauw"
49 | },
50 | {
51 | "type": "paypal",
52 | "url": "https://www.paypal.me/ebaauw/EUR"
53 | }
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------