├── .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 | [![Downloads](https://img.shields.io/npm/dt/homebridge-nb.svg)](https://www.npmjs.com/package/homebridge-nb) 8 | [![Version](https://img.shields.io/npm/v/homebridge-nb.svg)](https://www.npmjs.com/package/homebridge-nb) 9 | [![Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/yGvADWt) 10 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 11 | 12 | [![GitHub issues](https://img.shields.io/github/issues/ebaauw/homebridge-nb)](https://github.com/ebaauw/homebridge-nb/issues) 13 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/ebaauw/homebridge-nb)](https://github.com/ebaauw/homebridge-nb/pulls) 14 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](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 | --------------------------------------------------------------------------------