├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── cli ├── hap.js ├── json.js ├── sysinfo.js └── upnp.js ├── doc └── homebridge-lib.md ├── homebridge-lib.png ├── index.js ├── jsdoc.json ├── lib ├── AccessoryDelegate.js ├── AdaptiveLighting.js ├── Bonjour.js ├── CharacteristicDelegate.js ├── Colour.js ├── CommandLineParser.js ├── CommandLineTool.js ├── CustomHomeKitTypes.js ├── Delegate.js ├── EveHomeKitTypes.js ├── HttpClient.js ├── JsonFormatter.js ├── MyHomeKitTypes.js ├── OptionParser.js ├── Platform.js ├── PropertyDelegate.js ├── ServiceDelegate.js ├── ServiceDelegate │ ├── AccessoryInformation.js │ ├── Battery.js │ ├── Dummy.js │ ├── History.js │ └── ServiceLabel.js ├── SystemInfo.js ├── UiServer.js ├── UpnpClient.js ├── chalk.js └── semver.js ├── 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 | out 3 | npm-debug.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /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 Lib 7 | [![Downloads](https://img.shields.io/npm/dt/homebridge-lib.svg)](https://www.npmjs.com/package/homebridge-lib) 8 | [![Version](https://img.shields.io/npm/v/homebridge-lib.svg)](https://www.npmjs.com/package/homebridge-lib) 9 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 10 | [![GitHub issues](https://img.shields.io/github/issues/ebaauw/homebridge-lib)](https://github.com/ebaauw/homebridge-lib/issues) 11 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/ebaauw/homebridge-lib)](https://github.com/ebaauw/homebridge-lib/pulls) 12 | 13 | 14 | 15 | ## Library for Homebridge Plugins 16 | Copyright © 2018-2025 Erik Baauw. All rights reserved. 17 | 18 | While developing a number of [Homebridge](https://github.com/homebridge/homebridge) plugins, I find myself duplicating a lot of code. 19 | The idea behind this library is to ease developing and maintaining Homebridge plugins by separating this generic code, dealing with [HomeKit](http://www.apple.com/ios/home/) and Homebridge, from the specific code, dealing with the actual devices being exposed to HomeKit. 20 | 21 | ### Documentation 22 | The documentation, how to develop a plugin using Homebridge Lib, is provided in the code and through tutorials in the `doc` directory. 23 | To generate the documentation, install [`jsdoc`](https://github.com/jsdoc3/jsdoc) and run `jsdoc -c jsdoc.json`. 24 | To view the documentation, open `index.html` in the `out` directory. 25 | 26 | See [Homebridge WS](https://github.com/ebaauw/homebridge-ws) for an example plugin based on Homebridge Lib. 27 | 28 | ### Command-Line Tools 29 | The Homebridge Lib library comes with a number of command-line tools for troubleshooting Homebridge installations. 30 | 31 | Tool | Description 32 | --------- | ----------- 33 | `hap` | Logger for HomeKit accessory announcements. 34 | `json` | JSON formatter. 35 | `sysinfo` | Print hardware and operating system information. 36 | `upnp` | UPnP tool. 37 | 38 | Each command-line tool takes a `-h` or `--help` argument to provide a brief overview of its functionality and command-line arguments. 39 | 40 | ### Installation 41 | This library is _not_ a Homebridge plugin and does not need to be installed manually. 42 | Instead, Homebridge plugins using this library should list it as a dependency in their `package.json`. 43 | This way, `npm` installs Homebridge Lib automatically when installing the actual plugin. 44 | 45 | To install the command-line tools, use: 46 | ``` 47 | $ sudo npm -g i hb-lib-tools 48 | ``` 49 | This creates symlinks to these tools in `/usr/bin` or `/usr/local/bin` (depending on how you installed NodeJS). 50 | 51 | ### Credits 52 | The logic for handling [Eve](https://www.evehome.com/en/eve-app) history was copied from Simone Tisa's [`fakegato-history`](https://github.com/simont77/fakegato-history) repository, copyright © 2017 simont77. 53 | 54 | ### Caveats 55 | Homebridge Lib is a hobby project of mine, provided as-is, with no warranty whatsoever. I've been running it successfully at my home for years, but your mileage might vary. 56 | -------------------------------------------------------------------------------- /cli/hap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // homebridge-lib/cli/hap.js 4 | // 5 | // Library for Homebridge plugins. 6 | // Copyright © 2018-2025 Erik Baauw. All rights reserved. 7 | // 8 | // Logger for HomeKit accessory announcements. 9 | 10 | import { createRequire } from 'node:module' 11 | 12 | import { HapTool } from 'hb-lib-tools/HapTool' 13 | 14 | const require = createRequire(import.meta.url) 15 | const packageJson = require('../package.json') 16 | 17 | new HapTool(packageJson).main() 18 | -------------------------------------------------------------------------------- /cli/json.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // homebridge-lib/cli/json.js 4 | // 5 | // Library for Homebridge plugins. 6 | // Copyright © 2018-2025 Erik Baauw. All rights reserved. 7 | // 8 | // JSON formatter. 9 | 10 | import { createRequire } from 'node:module' 11 | 12 | import { JsonTool } from 'hb-lib-tools/JsonTool' 13 | 14 | const require = createRequire(import.meta.url) 15 | const packageJson = require('../package.json') 16 | 17 | new JsonTool(packageJson).main() 18 | -------------------------------------------------------------------------------- /cli/sysinfo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // homebridge-lib/cli/sysinfo.js 4 | // 5 | // Library for Homebridge plugins. 6 | // Copyright © 2021-2025 Erik Baauw. All rights reserved. 7 | // 8 | // Show system info. 9 | 10 | import { createRequire } from 'node:module' 11 | 12 | import { SysinfoTool } from 'hb-lib-tools/SysinfoTool' 13 | 14 | const require = createRequire(import.meta.url) 15 | const packageJson = require('../package.json') 16 | 17 | new SysinfoTool(packageJson).main() 18 | -------------------------------------------------------------------------------- /cli/upnp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // homebridge-lib/cli/upnp.js 4 | // 5 | // Library for Homebridge plugins. 6 | // Copyright © 2018-2025 Erik Baauw. All rights reserved. 7 | // 8 | // Logger for UPnP device announcements. 9 | 10 | import { createRequire } from 'node:module' 11 | 12 | import { UpnpTool } from 'hb-lib-tools/UpnpTool' 13 | 14 | const require = createRequire(import.meta.url) 15 | const packageJson = require('../package.json') 16 | 17 | new UpnpTool(packageJson).main() 18 | -------------------------------------------------------------------------------- /doc/homebridge-lib.md: -------------------------------------------------------------------------------- 1 | Copyright © 2017-2025 Erik Baauw. All rights reserved. 2 | 3 | ## Introduction 4 | While developing a number of [Homebridge](https://github.com/nfarina/homebridge) plugins, I find myself duplicating a lot of code. 5 | The idea behind this library is to ease developing and maintaining Homebridge plugins by separating this generic code, dealing with [HomeKit](http://www.apple.com/ios/home/) and Homebridge, from the specific code, dealing with the actual devices being exposed to HomeKit. 6 | 7 | Technically, the Homebridge Lib library is based on the following starting points: 8 | - Using the Homebridge [dynamic platform plugin API](https://github.com/nfarina/homebridge/wiki/On-Programming-Dynamic-Platforms); 9 | - Using JavaScript [asynchronous functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) rather than callbacks; 10 | - Using JavaScript [classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) rather than prototypes. An actual plugin would extend the classes provided by Homebridge Lib; 11 | - Using NodeJS [events](https://nodejs.org/dist/latest-v14.x/docs/api/events.html). In fact all Homebridge Lib delegate classes extend `EventEmitter`; 12 | - Using the [JavaScript Standard Style](https://github.com/standard/standard); 13 | - Using [JSDoc](https://jsdoc.app) for API documentation; 14 | - Using [Mocha](https://mochajs.org) for automated testing of the generic utilities. 15 | 16 | ### Abstract Superclasses 17 | The Homebridge Lib library provides a number of abstract superclasses for a Homebridge dynamic platform plugin and for delegates to a HomeKit accessory, service, and characteristic. 18 | 19 | Class | Description 20 | ------------------------------ | ----------- 21 | {@link Platform} | Homebridge dynamic platform plugin. 22 | {@link AccessoryDelegate} | Delegate of a HomeKit accessory. 23 | {@link ServiceDelegate} | Delegate of a HomeKit service. 24 | {@link CharacteristicDelegate} | Delegate of a HomeKit characteristic. 25 | {@link Delegate} | Abstract superclass for {@link Platform}, {@link AccessoryDelegate}, {@link ServiceDelegate}, and {@link CharacteristicDelegate}. 26 | {@link PropertyDelegate} | Delegate of a persisted property. 27 | 28 | These delegate classes provide the following functionality: 29 | - Saving and restoring accessories from persistent storage between Homebridge sessions (using the Homebridge dynamic platform API); 30 | - History for [Eve](https://www.evehome.com/en/eve-app); 31 | - Universal Plug & Play (UPnP) device discovery; 32 | - Setting up a heartbeat for device polling; 33 | - Logging and error handling; 34 | - Checking the NodeJS and Homebridge versions used; 35 | - Checking the latest plugin version published to the NPM registry. 36 | 37 | A Homebridge plugin based on Homebridge Lib extends these classes, providing the device-specific logic. 38 | See [Homebridge WS](https://github.com/ebaauw/homebridge-ws) for an example plugin based on Homebridge Lib. 39 | 40 | ### API Wrapper 41 | The Homebridge Lib library provides a wrapper around the remaining Homebride and [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) APIs, that cannot be hidden by the abstract superclasses. 42 | 43 | Class | Description 44 | ----------- | ----------- 45 | {@link hap} | HomeKit Accessory Protocol. 46 | {@link eve} | Custom HomeKit services and characteristic used by the [Eve](https://www.evehome.com/en/eve-app) app. 47 | {@link my} | Custom HomeKit services and characteristics used by [my plugins](https://github.com/ebaauw?tab=repositories). 48 | 49 | ### Command-Line Utilities 50 | The Homebridge Lib library comes with a number of command-line tools for troubleshooting Homebridge installations. 51 | 52 | Tool | Description 53 | --------- | ----------- 54 | `hap` | Logger for HomeKit accessory announcements. 55 | `json` | JSON formatter. 56 | `sysinfo` | Print hardware and operating system information. 57 | `upnp` | Logger for UPnP device announcements. 58 | 59 | Each command-line tool takes a `-h` or `--help` argument to provide a brief overview of its functionality and command-line arguments. 60 | 61 | ### Utility Classes 62 | The Homebridge Lib library provides a number of utility classes for Homebridge plugins and/or command-line tools. 63 | 64 | Class | Description 65 | -------------------------- | ----------- 66 | {@link AdaptiveLighting} | Adaptive Lighting. 67 | {@link Colour} | Colour conversions. 68 | {@link CommandLineParser} | Parser and validator for command-line arguments. 69 | {@link CommandLineTool} | Command-line tool. 70 | {@link CustomHomeKitTypes} | Abstract superclass for {@link EveHomeKitTypes} and {@link MyHomeKitTypes}. 71 | {@link EveHomeKitTypes} | Custom HomeKit Services and Characteristic used by [Eve](https://www.evehome.com/en) accessories and by the [Eve app](https://www.evehome.com/en/eve-app). 72 | {@link HttpClient} | HTTP client. 73 | {@link JsonFormatter} | JSON formatter. 74 | {@link MyHomeKitTypes} | My own collection of custom HomeKit Services and Characteristics. 75 | {@link OptionParser} | Parser and validator for options and other parameters. 76 | {@link UiServer} | Server for handling Homebridge Plugin UI requests. 77 | {@link UpnpClient} | Universal Plug and Play client. 78 | -------------------------------------------------------------------------------- /homebridge-lib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebaauw/homebridge-lib/e95b8cbb11859dd015e0d29c26f8be08f03e33ac/homebridge-lib.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/index.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Library for Homebridge plugins. 7 | * see the {@tutorial homebridge-lib} tutorial. 8 | * 9 | * Homebridge Lib provides: 10 | * - A series of base classes for building Homebridge dynamic platform plugins: 11 | * {@link Platform}, 12 | * {@link AccessoryDelegate}, 13 | * {@link ServiceDelegate}, and 14 | * {@link CharacteristicDelegate}. 15 | * - An abstract base class to building command-line tools: 16 | * {@link CommandLineTool}. 17 | * - A series of helper classes for building homebridge plugins (of any type) 18 | * and/or command-line utilities: 19 | * {@link AdaptiveLighting}, 20 | * {@link Colour}, 21 | * {@link CommandLineParser}, 22 | * {@link CustomHomeKitTypes}, 23 | * {@link Delegate}, 24 | * {@link EveHomeKitTypes}, 25 | * {@link HttpClient}, 26 | * {@link JsonFormatter}, 27 | * {@link MyHomeKitTypes}, 28 | * {@link OptionParser}, 29 | * {@link PropertyDelegate}, 30 | * {@link SystemInfo}, 31 | * {@link UiServer}, and 32 | * {@link UpnpClient}. 33 | * 34 | * To access the classes provided by Homebridge Lib from your module, 35 | * simply load it by: 36 | * ```javascript 37 | * import { Class } from 'homebridge-lib/Class' 38 | * ``` 39 | * 40 | * Note that each class provided by Homebridge Lib is implemented as a 41 | * separate Javascript module, that is loaded lazily on first use. 42 | * Due to the way NodeJS deals with circular module dependencies, these modules 43 | * might not yet be initialised while your module is loading. 44 | * 45 | * @module homebridge-lib 46 | */ 47 | 48 | /** Convert Error to string. 49 | * 50 | * Include the stack trace only for programming errors (JavaScript and NodeJS 51 | * runtime errors). 52 | * Translate system errors into more readable messages. 53 | * @function formatError 54 | * @param {Error} e - The error. 55 | * @param {boolean} [useChalk=false] - Use chalk to grey out the stack trace. 56 | * @returns {string} - The error as string. 57 | * @memberof module:homebridge-lib 58 | */ 59 | export { formatError } from 'hb-lib-tools' 60 | 61 | /** Return the recommended version of NodeJS from package.json. 62 | * This is the version used to develop and test the software, 63 | * typically the latest LTS version. 64 | * @function recommendedNodeVersion 65 | * @param {string} packageJson - The contents of package.json 66 | * #return {string} - The recommended version. 67 | * @memberof module:hbLibTools 68 | */ 69 | export { recommendedNodeVersion } from 'hb-lib-tools' 70 | 71 | /** Resolve after given period, delaying execution. 72 | * 73 | * E.g. to delay execution for 1.5 seconds, issue: 74 | * ```javascript 75 | * import { timeout } from 'homebridge-lib' 76 | * 77 | * await timeout(1500) 78 | * ``` 79 | * 80 | * @function timeout 81 | * @param {integer} msec - Period (in msec) to wait. 82 | * @throws {TypeError} On invalid parameter type. 83 | * @throws {RangeError} On invalid parameter value. 84 | * @memberof module:homebridge-lib 85 | */ 86 | export { timeout } from 'hb-lib-tools' 87 | 88 | /** Convert integer to hex string. 89 | * @function toHexString 90 | * @param {integer} i - The integer. 91 | * @param {integer} [length=4] - The number of digits in the hex string. 92 | * @returns {string} - The hex string. 93 | * @memberof module:homebridge-lib 94 | */ 95 | export { toHexString } from 'hb-lib-tools' 96 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "plugins/markdown" 4 | ], 5 | "rescurseDepth": 10, 6 | "source": { 7 | "include": [ 8 | "README.md", 9 | "index.js", 10 | "lib", 11 | "lib/ServiceDelegate", 12 | "node_modules/hb-lib-tools/index.js", 13 | "node_modules/hb-lib-tools/lib", 14 | "doc" 15 | ] 16 | }, 17 | "opts": { 18 | "tutorials": "doc" 19 | }, 20 | "templates": { 21 | "monospaceLinks": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/AccessoryDelegate.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/AccessoryDelegate.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { CharacteristicDelegate } from 'homebridge-lib/CharacteristicDelegate' 7 | import { Delegate } from 'homebridge-lib/Delegate' 8 | import { PropertyDelegate } from 'homebridge-lib/PropertyDelegate' 9 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 10 | import 'homebridge-lib/ServiceDelegate/AccessoryInformation' 11 | 12 | const startsWithUuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/ 13 | 14 | /** Delegate of a HomeKit accessory. 15 | *
See {@link AccessoryDelegate}. 16 | * @name AccessoryDelegate 17 | * @type {Class} 18 | * @memberof module:homebridge-lib 19 | */ 20 | 21 | /** Delegate of a HomeKit accessory. 22 | * 23 | * @abstract 24 | * @extends Delegate 25 | */ 26 | class AccessoryDelegate extends Delegate { 27 | /** Create a new instance of a HomeKit accessory delegate. 28 | * 29 | * When the corresponding HomeKit accessory was restored from persistent 30 | * storage, it is linked to the delegate. Otherwise a new accessory 31 | * will be created, using the values from `params`. 32 | * @param {!Platform} platform - Reference to the corresponding platform 33 | * plugin instance. 34 | * @param {!object} params - Properties of the HomeKit accessory. 35 | * @param {!string} params.id - The unique ID of the accessory, used to 36 | * derive the HomeKit accessory UUID.
37 | * Must be unchangeable, preferably a serial number or mac address. 38 | * @param {!string} params.name - The accessory name.
39 | * Also used to prefix log and error messages. 40 | * @param {?string} params.category - The accessory category. 41 | * @param {!string} params.manufacturer - The accessory manufacturer. 42 | * @param {!string} params.model - The accessory model. 43 | * @param {!string} params.firmware - The accessory firmware revision. 44 | * @param {?string} params.hardware - The accessory hardware revision. 45 | * @param {?string} params.software - The accessory software revision. 46 | * @param {?integer} params.logLevel - The log level for the accessory 47 | */ 48 | constructor (platform, params = {}) { 49 | if (params.name == null) { 50 | throw new SyntaxError('params.name: missing') 51 | } 52 | super(platform, params.name) 53 | if (params.id == null || typeof params.id !== 'string') { 54 | throw new TypeError('params.id: not a string') 55 | } 56 | if (params.id === '') { 57 | throw new RangeError('params.id: invalid id') 58 | } 59 | if (params.logLevel == null) { 60 | params.logLevel = platform.logLevel 61 | } 62 | 63 | // Link or create associated PlatformAccessory. 64 | this._accessory = this._platform._getAccessory(this, params) 65 | this._context = this._accessory.context 66 | 67 | // Setup shortcut for property values and values of the characteristics 68 | // of the _Accessory Information_ service. 69 | this._values = {} // by key 70 | this._propertyDelegates = {} 71 | this.addPropertyDelegate({ key: 'className', value: this.constructor.name, silent: true }) 72 | this.addPropertyDelegate({ key: 'version', silent: true }) 73 | this.values.version = platform.packageJson.version 74 | this.addPropertyDelegate({ key: 'id', value: params.id, silent: true }) 75 | this.addPropertyDelegate({ key: 'logLevel', value: params.logLevel }) 76 | this.addPropertyDelegate({ key: 'name', value: this.name, silent: true }) 77 | .on('didSet', (name) => { this.name = name }) 78 | if (typeof platform.onUiRequest === 'function') { 79 | this.addPropertyDelegate({ key: 'uiPort', silent: true }) 80 | if (platform._ui != null) { 81 | this.values.uiPort = platform._ui.port 82 | } 83 | } 84 | 85 | // Create delegate for AccessoryInformation service. 86 | this._serviceDelegates = {} 87 | this._accessoryInformationDelegate = new ServiceDelegate.AccessoryInformation(this, params) 88 | 89 | // Configure PlatformAccessory. 90 | this._accessory.on('identify', this._identify.bind(this)) 91 | 92 | this.once('initialised', () => { 93 | const staleServices = [] 94 | for (const service of this._accessory.services) { 95 | const key = service.UUID + 96 | (service.subtype == null ? '' : '.' + service.subtype) 97 | if (this._serviceDelegates[key] == null) { 98 | if (service.UUID !== this.Services.hap.ProtocolInformation.UUID) { 99 | service.key = key 100 | staleServices.push(service) 101 | } 102 | } else { 103 | this._serviceDelegates[key].emit('initialised') 104 | } 105 | } 106 | for (const service of staleServices) { 107 | this.warn('remove stale service %s (%s)', service.key, service.constructor.name) 108 | this._accessory.removeService(service) 109 | } 110 | const staleKeys = [] 111 | for (const key in this._context) { 112 | if (startsWithUuid.test(key)) { 113 | if (this._serviceDelegates[key] == null) { 114 | staleKeys.push(key) 115 | } 116 | } else { 117 | if (key !== 'context' && this._propertyDelegates[key] == null) { 118 | staleKeys.push(key) 119 | } 120 | } 121 | } 122 | for (const key of staleKeys) { 123 | this.warn('remove stale context %s', key) 124 | delete this._context[key] 125 | } 126 | }) 127 | } 128 | 129 | /** Destroy accessory delegate and associated HomeKit accessory. 130 | * @params {boolean} [delegateOnly=false] - Destroy the delegate, but keep the 131 | * associated HomeKit accessory (including context). 132 | */ 133 | destroy (delegateOnly = false) { 134 | this.removeAllListeners() 135 | for (const key in this._serviceDelegates) { 136 | this._serviceDelegates[key].destroy(delegateOnly) 137 | } 138 | for (const key in this._propertyDelegates) { 139 | this._propertyDelegates[key]._destroy(delegateOnly) 140 | } 141 | if (delegateOnly) { 142 | this._accessory.removeAllListeners() 143 | return 144 | } 145 | this._platform._removeAccessory(this._accessory) 146 | } 147 | 148 | _linkServiceDelegate (serviceDelegate) { 149 | const key = serviceDelegate._key 150 | // this.debug('link service %s', key) 151 | this._serviceDelegates[key] = serviceDelegate 152 | return serviceDelegate 153 | } 154 | 155 | _unlinkServiceDelegate (serviceDelegate, delegateOnly) { 156 | const key = serviceDelegate._key 157 | // this.debug('unlink service %s', key) 158 | delete this._serviceDelegates[key] 159 | if (delegateOnly) { 160 | return 161 | } 162 | delete this._context[key] 163 | } 164 | 165 | /** Creates a new {@link PropertyDelegate} instance, for a property of the 166 | * associated HomeKit accessory. 167 | * 168 | * The property value is accessed through 169 | * {@link AccessoryDelegate#values values}. 170 | * The delegate is returned, but can also be accessed through 171 | * {@link AccessoryDelegate#propertyDelegate propertyDelegate()}. 172 | * @param {!object} params - Parameters of the property delegate. 173 | * @param {!string} params.key - The key for the property delegate.
174 | * Needs to be unique with parent delegate. 175 | // * @param {!type} params.type - The type of the property value. 176 | * @param {?*} params.value - The initial value of the property.
177 | * Only used when the property delegate is created for the first time. 178 | * Otherwise, the value is restored from persistent storage. 179 | * @param {?boolean} params.logLevel - Level for homebridge log messages 180 | * when property was set or has been changed. 181 | * @param {?string} params.unit - The unit of the value of the property. 182 | * @returns {PropertyDelegate} 183 | * @throws {TypeError} When a parameter has an invalid type. 184 | * @throws {RangeError} When a parameter has an invalid value. 185 | * @throws {SyntaxError} When a mandatory parameter is missing or an 186 | * optional parameter is not applicable. 187 | */ 188 | addPropertyDelegate (params = {}) { 189 | if (typeof params.key !== 'string') { 190 | throw new TypeError(`params.key: ${params.key}: invalid key`) 191 | } 192 | if (params.key === '') { 193 | throw new RangeError(`params.key: ${params.key}: invalid key`) 194 | } 195 | const key = params.key 196 | if (this._values[key] !== undefined) { 197 | throw new SyntaxError(`${key}: duplicate key`) 198 | } 199 | 200 | const delegate = new PropertyDelegate(this, params) 201 | this._propertyDelegates[key] = delegate 202 | 203 | // Create shortcut for characteristic value. 204 | Object.defineProperty(this._values, key, { 205 | configurable: true, // make sure we can delete it again 206 | writeable: true, 207 | get () { return delegate.value }, 208 | set (value) { delegate.value = value } 209 | }) 210 | 211 | return delegate 212 | } 213 | 214 | removePropertyDelegate (key) { 215 | if (this._accessoryInformationDelegate.values[key] != null) { 216 | throw new RangeError('%s: invalid key') 217 | } 218 | delete this._values[key] 219 | const delegate = this._propertyDelegates[key] 220 | delegate._destroy() 221 | delete this._propertyDelegates[key] 222 | delete this.context[key] 223 | } 224 | 225 | /** Returns the property delegate correspondig to the property key. 226 | * @param {!string} key - The key for the property. 227 | * @returns {PropertyDelegate} 228 | */ 229 | propertyDelegate (key) { 230 | return this._propertyDelegates[key] 231 | } 232 | 233 | /** Values of the HomeKit characteristics for the `AccessoryInformation` service. 234 | * 235 | * Contains the key of each property and of each characteristic in 236 | * {@link ServiceDelegate.AccessoryInformation AccessoryInformation}. 237 | * When the value is written, the value of the corresponding HomeKit 238 | * characteristic is updated. 239 | * @type {object} 240 | */ 241 | get values () { 242 | return this._values 243 | } 244 | 245 | /** Enable `heartbeat` events for this accessory delegate. 246 | * @type {boolean} 247 | */ 248 | get heartbeatEnabled () { return this._heartbeatEnabled } 249 | 250 | set heartbeatEnabled (value) { this._heartbeatEnabled = !!value } 251 | 252 | setAlive () { 253 | this.warn('setAlive() has been deprecated, use heartbeatEnabled instead') 254 | this._heartbeatEnabled = true 255 | } 256 | 257 | /** Plugin-specific context to be persisted across Homebridge restarts. 258 | * 259 | * After restart, this object is passed back to the plugin through the 260 | * {@link Platform#event:accessoryRestored accessoryRestored} event. 261 | * The plugin should store enough information to re-create the accessory 262 | * delegate, after Homebridge has restored the accessory. 263 | * @type {object} 264 | */ 265 | get context () { 266 | return this._context.context 267 | } 268 | 269 | /** Current log level. 270 | * 271 | * The log level determines what type of messages are printed: 272 | * 273 | * 0. Print error and warning messages. 274 | * 1. Print error, warning, and log messages. 275 | * 2. Print error, warning, log, and debug messages. 276 | * 3. Print error, warning, log, debug, and verbose debug messages. 277 | * 278 | * Note that debug messages (level 2 and 3) are only printed when 279 | * Homebridge was started with the `-D` or `--debug` command line option. 280 | * 281 | * The log level is initialised at 2 when the accessory is newly created. 282 | * It can be changed programmatically. 283 | * The log level is persisted across Homebridge restarts. 284 | * @type {!integer} 285 | */ 286 | get logLevel () { 287 | return this.values.logLevel == null ? this.platform.logLevel : this.values.logLevel 288 | } 289 | 290 | /** Inherit `logLevel` from another accessory delegate. 291 | * @param {AccessoryDelegate} delegate - The delegate to inherit `logLevel` 292 | * from. 293 | */ 294 | inheritLogLevel (delegate) { 295 | if (!(delegate instanceof AccessoryDelegate)) { 296 | throw new TypeError('delegate: not an AccessoryDelegate') 297 | } 298 | if (delegate === this) { 299 | throw new RangeError('delegate: cannot inherit from oneself') 300 | } 301 | Object.defineProperty(this, 'logLevel', { 302 | get () { return delegate.logLevel } 303 | }) 304 | } 305 | 306 | /** Manage `logLevel` from characteristic delegate. 307 | * @param {CharacteristicDelegate|PropertyDelegate} delegate - The delegate 308 | * of the `logLevel` characteristic. 309 | * @param {Boolean} [forPlatform=false] - Manage the Platform `logLevel` as 310 | * well. 311 | */ 312 | manageLogLevel (delegate, forPlatform = false) { 313 | if ( 314 | !(delegate instanceof CharacteristicDelegate) && 315 | !(delegate instanceof PropertyDelegate) 316 | ) { 317 | throw new TypeError('delegate: not a CharacteristicDelegate or PropertyDelegate') 318 | } 319 | Object.defineProperty(this, 'logLevel', { 320 | get () { return delegate.value } 321 | }) 322 | if (forPlatform) { 323 | try { 324 | // TODO handle multiple delegates for platform log level 325 | // now platform log level is linked to first delegate only 326 | Object.defineProperty(this.platform, 'logLevel', { 327 | get () { return delegate.value } 328 | }) 329 | } catch (error) { } 330 | } 331 | } 332 | 333 | // Called by homebridge when Identify is selected. 334 | _identify () { 335 | this.emit('identify') 336 | this.vdebug('context: %j', this._context) 337 | } 338 | } 339 | 340 | export { AccessoryDelegate } 341 | -------------------------------------------------------------------------------- /lib/AdaptiveLighting.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/AdaptiveLighting.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | import { CharacteristicDelegate } from 'homebridge-lib/CharacteristicDelegate' 7 | 8 | /* global BigInt */ 9 | 10 | const epoch = (new Date('2001-01-01T00:00:00Z')).valueOf() 11 | 12 | // Types in TLV values for Adaptive Lighting. 13 | const types = { 14 | 1: { key: 'configuration', type: 'tlv' }, 15 | 1.1: { key: 'iid', type: 'uint' }, 16 | 1.2: { key: 'characteristic', type: 'uint' }, 17 | 2: { key: 'control', type: 'tlv' }, 18 | 2.1: { key: 'colorTemperature', type: 'tlv' }, 19 | '2.1.1': { key: 'iid', type: 'uint' }, 20 | '2.1.2': { key: 'transitionParameters', type: 'tlv' }, 21 | '2.1.2.1': { type: 'hex' }, 22 | '2.1.2.2': { key: 'startTime', type: 'date' }, 23 | '2.1.2.3': { type: 'hex' }, 24 | '2.1.3': { key: 'runtime', type: 'uint' }, 25 | '2.1.5': { key: 'curve', type: 'tlv' }, 26 | '2.1.5.1': { key: 'entries', type: 'tlv' }, 27 | '2.1.5.1.1': { key: 'adjustmentFactor', type: 'float' }, 28 | '2.1.5.1.2': { key: 'mired', type: 'float' }, 29 | '2.1.5.1.3': { key: 'offset', type: 'uint' }, 30 | '2.1.5.1.4': { key: 'duration', type: 'uint' }, 31 | '2.1.5.2': { key: 'adjustmentIid', type: 'uint' }, 32 | '2.1.5.3': { key: 'adjustmentRange', type: 'tlv' }, 33 | '2.1.5.3.1': { key: 'min', type: 'uint' }, 34 | '2.1.5.3.2': { key: 'max', type: 'uint' }, 35 | '2.1.6': { key: 'updateInterval', type: 'uint' }, 36 | '2.1.8': { key: 'notifyIntervalThreshold', type: 'uint' } 37 | } 38 | 39 | // Recursively parse TLV value into an object. 40 | function parseTlv (path, buf) { 41 | path = path == null ? '' : path + '.' 42 | const result = {} 43 | for (let i = 0; i < buf.length;) { 44 | let type = buf[i++] 45 | let length = buf[i++] 46 | let value = buf.slice(i, i + length) 47 | i += length 48 | while (length === 255 && i < buf.length) { 49 | if (buf[i] === type) { 50 | i++ 51 | length = buf[i++] 52 | value = Buffer.concat([value, buf.slice(i, i + length)]) 53 | i += length 54 | } 55 | } 56 | type = path + type 57 | // console.error('type: %s, length: %d, value: %j', type, length, value) 58 | 59 | let key = type 60 | if (types[type] != null) { 61 | if (types[type].key != null) { 62 | key = types[type].key 63 | } 64 | switch (types[type].type) { 65 | case 'uint': 66 | if (length === 1) { 67 | value = value.readUInt8() 68 | } else if (length === 2) { 69 | value = value.readUInt16LE() 70 | } else if (length === 4) { 71 | value = value.readUInt32LE() 72 | } else if (length === 8) { 73 | value = Number(value.readBigUInt64LE()) 74 | } 75 | break 76 | case 'float': 77 | if (length === 4) { 78 | value = value.readFloatLE() 79 | } 80 | break 81 | case 'date': 82 | if (length === 8) { 83 | value = new Date(Number(value.readBigUInt64LE()) + epoch).toISOString() 84 | } 85 | break 86 | case 'hex': 87 | value = value.toString('hex').toUpperCase() 88 | break 89 | case 'tlv': 90 | value = parseTlv(type, value) 91 | break 92 | default: 93 | break 94 | } 95 | } else if (length === 0) { 96 | // ignore empty value 97 | key = null 98 | value = null 99 | } 100 | 101 | if (key != null) { 102 | // Add key/value-pair to result. 103 | if (result[key] == null) { 104 | // New key: add key/value-pair. 105 | result[key] = value 106 | } else { 107 | // Duplicate key. 108 | if (!Array.isArray(result[key])) { 109 | // Turn value into array. 110 | result[key] = [result[key]] 111 | } 112 | // Add new value to value array. 113 | result[key].push(value) 114 | } 115 | } 116 | } 117 | return result 118 | } 119 | 120 | // Return a TLV buffer for given type and length, with empty value. 121 | function tlvBuffer (type, length) { 122 | const buf = Buffer.alloc(2 + length) 123 | buf[0] = type 124 | buf[1] = length 125 | return buf 126 | } 127 | 128 | // Return a TLV buffer for given type with length 0. 129 | function tlvFromNull (type) { 130 | return tlvBuffer(type, 0) 131 | } 132 | 133 | // Return a TLV buffer for given type and buffer value. 134 | function tlvFromBuffer (type, value) { 135 | const buf = tlvBuffer(type, value.length) 136 | value.copy(buf, 2, 0) 137 | return buf 138 | } 139 | 140 | // Return a TLV buffer for given type and uint value. 141 | function tlvFromUInt (type, value) { 142 | const buf = Buffer.alloc(8) 143 | buf.writeBigUInt64LE(BigInt(value)) 144 | let length 145 | if (value > 0xFFFFFFFF) { 146 | length = 8 147 | } else if (value > 0xFFFF) { 148 | length = 4 149 | } else if (value > 0xFF) { 150 | length = 2 151 | } else { 152 | length = 1 153 | } 154 | return tlvFromBuffer(type, buf.slice(0, length)) 155 | } 156 | 157 | // Return a TVL buffer for given type and hex string value. 158 | function tlvFromHexString (type, value) { 159 | if (value == null) { 160 | return Buffer.alloc(0) 161 | } 162 | return tlvFromBuffer(type, Buffer.from(value, 'hex')) 163 | } 164 | 165 | /** Adaptive Lighting. 166 | *
See {@link AdaptiveLighting}. 167 | * @name AdaptiveLighting 168 | * @type {Class} 169 | * @memberof module:homebridge-lib 170 | */ 171 | 172 | /** Adaptive Lighting. 173 | */ 174 | class AdaptiveLighting { 175 | /** Create an instance for a _Lightbulb_ service. 176 | * @param {integer} bri - The IID of the _Brightness_ characteristic. 177 | * @param {integer} ct - The IID of the _Color Temperature_ characteristic. 178 | */ 179 | constructor (bri, ct) { 180 | this._bri = bri 181 | this._ct = ct 182 | this._active = false 183 | } 184 | 185 | /** Adaptive lighting active. 186 | * @type {boolean} 187 | * @readonly 188 | */ 189 | get active () { return this._control != null } 190 | 191 | get briIid () { 192 | if (this._briIid == null) { 193 | this._briIid = this._bri instanceof CharacteristicDelegate 194 | ? this._bri._characteristic.iid 195 | : this._bri 196 | } 197 | return this._briIid 198 | } 199 | 200 | get ctIid () { 201 | if (this._ctIid == null) { 202 | this._ctIid = this._ct instanceof CharacteristicDelegate 203 | ? this._ct._characteristic.iid 204 | : this._ct 205 | } 206 | return this._ctIid 207 | } 208 | 209 | /** Deactivtate adaptive lighting. 210 | */ 211 | deactivate () { 212 | delete this._control 213 | } 214 | 215 | /** Generate the value for _Supported Transition Configuration_. 216 | * @return {string} value - Base64-encodeded configuration. 217 | */ 218 | generateConfiguration () { 219 | return Buffer.concat([ 220 | tlvFromBuffer(1, Buffer.concat([ 221 | tlvFromUInt(1, this.briIid), 222 | tlvFromUInt(2, 1) 223 | ])), 224 | tlvFromNull(0), 225 | tlvFromBuffer(1, Buffer.concat([ 226 | tlvFromUInt(1, this.ctIid), 227 | tlvFromUInt(2, 2) 228 | ])) 229 | ]).toString('base64') 230 | } 231 | 232 | _generateControl () { 233 | return tlvFromBuffer(1, Buffer.concat([ 234 | tlvFromUInt(1, this.ctIid), 235 | tlvFromBuffer(2, Buffer.concat([ 236 | tlvFromHexString(1, this._control.transitionParameters['2.1.2.1']), 237 | tlvFromUInt(2, this._startTime - epoch), 238 | tlvFromHexString(3, this._control.transitionParameters['2.1.2.3']) 239 | ])), 240 | tlvFromUInt(3, Math.max(1, (new Date()).valueOf() - this._startTime)) 241 | ])) 242 | } 243 | 244 | /** Generate the response value for setting _Transition Control_. 245 | * @return {string} value - Base-64 encodeded response. 246 | */ 247 | generateControlResponse () { 248 | if (this._control == null) { 249 | return '' 250 | } 251 | return tlvFromBuffer(2, this._generateControl()).toString('base64') 252 | } 253 | 254 | /** Generate the value for _Transition Control_. 255 | * @return {string} value - Base64-encodeded value. 256 | */ 257 | generateControl () { 258 | if (this._control == null) { 259 | return '' 260 | } 261 | return this._generateControl().toString('base64') 262 | } 263 | 264 | /** Parse a _Supported Transition Configuration_ value. 265 | * @param {string} value - Base64-encodeded configuration. 266 | * @return {object} configuration - Configuration as JavaScript object. 267 | */ 268 | parseConfiguration (value) { 269 | return parseTlv(null, Buffer.from(value, 'base64')) 270 | } 271 | 272 | /** Parse a _Transition Control_ value. 273 | * @param {string} value - Base64-encodeded control. 274 | * @return {object} control - Control as JavaScript object. 275 | * @throws {Error} In case control value doesn't match configuration. 276 | */ 277 | parseControl (value) { 278 | if (value === '') { 279 | return '' 280 | } 281 | const buf = Buffer.from(value, 'base64') 282 | if (buf[0] === 2) { 283 | value = parseTlv(null, buf).control 284 | } else { 285 | value = parseTlv('2', buf) 286 | } 287 | const control = value.colorTemperature 288 | if (control.iid != null && control.iid !== this.ctIid) { 289 | throw new Error('%d: bad ColorTemperature iid', control.iid) 290 | } 291 | if (control.curve != null) { 292 | if (control.curve.adjustmentIid !== this.briIid) { 293 | throw new Error('%d: bad Brightness iid', control.curve.adjustmentIid) 294 | } 295 | this._control = control 296 | this._startTime = (new Date(control.transitionParameters.startTime)).valueOf() 297 | } 298 | return value 299 | } 300 | 301 | /** Get the colour temperature in mired for given brightness and time offset. 302 | * @param {integer} bri - Value for _Brightness_, between 0 and 100%. 303 | * @param {?integer} offset - Offset in milliseconds from start of adaptive 304 | * lighting.
305 | * When not present, current time is used to compute the offset. 306 | * @return {integer} ct - The _Color Temperature_ value in mired. 307 | */ 308 | getCt (bri, offset) { 309 | if (this._control == null) { 310 | return null 311 | } 312 | if (offset == null) { 313 | offset = (new Date()).valueOf() - this._startTime 314 | } 315 | offset %= 86400000 316 | bri = Math.max(this._control.curve.adjustmentRange.min, bri) 317 | bri = Math.min(bri, this._control.curve.adjustmentRange.max) 318 | for (let i = 1; i < this._control.curve.entries.length; i++) { 319 | const entry = this._control.curve.entries[i] 320 | const targetCt = Math.round(entry.mired + entry.adjustmentFactor * bri) 321 | if (offset < entry.offset) { 322 | const pEntry = this._control.curve.entries[i - 1] 323 | const ratio = offset / entry.offset 324 | const mired = (1 - ratio) * pEntry.mired + ratio * entry.mired 325 | const adjustmentFactor = (1 - ratio) * pEntry.adjustmentFactor + 326 | ratio * entry.adjustmentFactor 327 | return Math.round(mired + adjustmentFactor * bri) 328 | // return { 329 | // ct: Math.round(mired + adjustmentFactor * bri), 330 | // targetCt: targetCt, 331 | // interval: entry.offset - offset 332 | // } 333 | } 334 | offset -= entry.offset 335 | if (entry.duration != null) { 336 | if (offset < entry.duration) { 337 | return targetCt 338 | // return { 339 | // ct: targetCt, 340 | // targetCt: targetCt, 341 | // interval: entry.duration - offset 342 | // } 343 | } 344 | offset -= entry.duration 345 | } 346 | } 347 | } 348 | } 349 | 350 | export { AdaptiveLighting } 351 | -------------------------------------------------------------------------------- /lib/Bonjour.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/Bonjour.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Return the `Bonjour` class from [`bonjour-hap`](https://github.com/homebridge/bonjour), 7 | * so plugins don't have to list this as a separate dependency. 8 | * @name Bonjour 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { Bonjour } from 'hb-lib-tools/Bonjour' 13 | -------------------------------------------------------------------------------- /lib/CharacteristicDelegate.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/CharacteristicDelegate.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { Delegate } from 'homebridge-lib/Delegate' 7 | import { OptionParser } from 'homebridge-lib/OptionParser' 8 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 9 | 10 | /** Delegate of a HomeKit characteristic. 11 | *
See {@link CharacteristicDelegate}. 12 | * @name CharacteristicDelegate 13 | * @type {Class} 14 | * @memberof module:homebridge-lib 15 | */ 16 | 17 | /** Delegate of a HomeKit characteristic. 18 | * 19 | * A characteristic delegate manages a value that: 20 | * - Is persisted across homebridge restarts; 21 | * - Can be monitored through homebridge's log output; 22 | * - Can be monitored programmatially, through `didSet` events; and 23 | * - Mirrors the value of the associated HomeKit characteristic: 24 | * - When the value is changed from HomeKit, the delegate's value is updated; 25 | * - When the value is changed programmatically, the HomeKit characteristic 26 | * value is updated. 27 | * 28 | * A characteristic delegate might be created without an associated 29 | * characteristic, to manage a value that's hidden from HomeKit, but still 30 | * need to be persisted (e.g. credentials) or monitored (e.g. derived values). 31 | * 32 | * A characteristic delegate might be configured with asynchronous functions 33 | * to be called when HomeKit tries to read or update the characteristic value: 34 | * - The `willGet` function is called when HomeKit tries to reads the 35 | * characteristic value.
36 | * It returns a promise that resolves to the value to return to HomeKit. 37 | * When the promise rejects, the previously cached value is returned. 38 | * - The `willSet` function is called when HomeKit tries to update the 39 | * characteristic value.
40 | * It returns a promise that resolves to indicate that the accessory has 41 | * processed the new value. 42 | * When the promise rejects, the HomeKit characteristic is reset to the 43 | * previous value. 44 | * 45 | * HomeKit only sets or clears the error state of an accessory when it tries 46 | * to read or updates a charactertic value. 47 | * There's no way for an accessory to push the error state to HomeKit. 48 | * Because of this, the characteristic delegate never returns an error to 49 | * HomeKit. 50 | * To indicate that an accessory is in error, use the _Status Fault_ 51 | * characteristic. 52 | * 53 | * Because the HomeKit app is unresponsive until the `getter` or `setter` 54 | * completes, the characteristic delegate provides timeout timers when calling 55 | * these functions. 56 | * Timeouts are handled as if the promise had rejected. 57 | * 58 | * @extends Delegate 59 | */ 60 | class CharacteristicDelegate extends Delegate { 61 | /** Create a new instance of a HomeKit characteristic delegate. 62 | * 63 | * Note that instances of `CharacteristicDelegate` are normally created by 64 | * invoking 65 | * {@link ServiceDelegate#addCharacteristicDelegate addCharacteristicDelegate()} 66 | * of the associated service delegate. 67 | * @param {!ServiceDelegate} serviceDelegate - Reference to the corresponding 68 | * HomeKit service delegate. 69 | * @param {!object} params - Parameters of the HomeKit characteristic 70 | * delegate.
71 | * See 72 | * {@link ServiceDelegate#addCharacteristicDelegate addCharacteristicDelegate()} 73 | 74 | * @param {!string} params.key - The key for the characteristic delegate.
75 | * Needs to be unique with the service delegate. 76 | * @param {?*} params.value - The initial value.
77 | * Only used when the characteristic delegate is created for the first time. 78 | * Normally, the value is restored from persistent storage. 79 | * @param {?boolean} params.silent - Suppress homebridge log messages. 80 | * @param {?Characteristic} params.Characteristic - The type of the 81 | * accociated HomeKit characteristic, from 82 | * {@link Delegate#Characteristic Characteristic}. 83 | * @param {?object} params.props - The properties of the accociated HomeKit 84 | * characteristic.
85 | * Overrides the properties from the characteristic type. 86 | * @param {?string} params.unit - The unit of the value of the HomeKit 87 | * characteristic.
88 | * Overrides the unit from the characteristic type. 89 | * @param {?function} params.getter - Asynchronous function to be invoked 90 | * when HomeKit tries to read the characteristic value.
91 | * This must be an `async` function returning a `Promise` to the new 92 | * characteristic value. 93 | * @param {?function} params.setter - Asynchronous function to be invoked 94 | * when HomeKit tries to update the characteristic value.
95 | * This must be an `async` function returning a `Promise` that resolves 96 | * when the corresonding accessory has processed the updated value. 97 | * @param {integer} [params.timeout=1000] - Timeout (in msec) for blocking 98 | * HomeKit while waiting for the getter or setter. 99 | * @throws {TypeError} When a parameter has an invalid type. 100 | * @throws {RangeError} When a parameter has an invalid value. 101 | * @throws {SyntaxError} When a mandatory parameter is missing or an 102 | * optional parameter is not applicable. 103 | */ 104 | constructor (serviceDelegate, params = {}) { 105 | if (!(serviceDelegate instanceof ServiceDelegate)) { 106 | throw new TypeError('serviceDelegate: not a ServiceDelegate') 107 | } 108 | super(serviceDelegate.platform, serviceDelegate.name) 109 | if (typeof params.key !== 'string') { 110 | throw new TypeError('params.key: not a string') 111 | } 112 | if (params.Characteristic != null) { 113 | if ( 114 | typeof params.Characteristic !== 'function' || 115 | typeof params.Characteristic.UUID !== 'string' 116 | ) { 117 | throw new TypeError( 118 | `params.Characteristic: ${params.Characteristic}: not a Characteristic` 119 | ) 120 | } 121 | } 122 | this._serviceDelegate = serviceDelegate 123 | this._service = this._serviceDelegate._service 124 | this._key = params.key 125 | this._log = params.silent ? this.debug : this.log 126 | 127 | if (params.Characteristic != null) { 128 | const Characteristic = params.Characteristic 129 | if (this._service.testCharacteristic(Characteristic)) { 130 | this._characteristic = this._service.getCharacteristic(Characteristic) 131 | } else { 132 | this._characteristic = this._service.addCharacteristic(Characteristic) 133 | } 134 | if (params.props != null) { 135 | if (this._characteristic.value < params.props.minValue) { 136 | this._characteristic.updateValue(params.props.minValue) 137 | } 138 | this._characteristic.setProps(params.props) 139 | } 140 | if (this._characteristic.props.unit != null) { 141 | this._unit = ' ' + this._characteristic.props.unit 142 | } 143 | // Install getter and setter when needed. 144 | if (params.getter != null && typeof params.getter === 'function') { 145 | this._getter = params.getter 146 | this._onGetCount = 0 147 | this._characteristic.on('get', this._onGet.bind(this)) 148 | } 149 | if (this._canWrite && Characteristic !== this.Characteristics.hap.Identify) { 150 | if (params.setter != null && typeof params.setter === 'function') { 151 | this._setter = params.setter 152 | } 153 | this._characteristic.on('set', this._onSet.bind(this)) 154 | } 155 | this._timeout = OptionParser.toInt( 156 | 'params.timeout', params.timeout == null ? 1000 : params.timeout, 500, 5000 157 | ) 158 | // Check that we are the only listener 159 | if (this._characteristic.listenerCount('set') > 1) { 160 | this.warn('%d listeners', this._characteristic.listenerCount('set')) 161 | } 162 | } 163 | if (params.unit != null) { 164 | this._unit = params.unit 165 | } else if (this._unit == null) { 166 | this._unit = '' 167 | } 168 | 169 | // Set initial value. 170 | if (this.value == null && params.value != null) { 171 | this.value = params.value 172 | if (this._characteristic != null) { 173 | this._characteristic.updateValue(this.value) 174 | } 175 | } 176 | } 177 | 178 | /** Destroy characteristic delegate and delete associated HomeKit characteristic. 179 | * @params {boolean} [delegateOnly=false] - Destroy the delegate, but keep the 180 | * associated HomeKit characteristic (including context). 181 | */ 182 | _destroy (delegateOnly = false) { 183 | this.debug('destroy %s', this._key) 184 | this.removeAllListeners() 185 | if (delegateOnly) { 186 | if (this._characteristic != null) { 187 | // this._characteristic.removeAllListeners() // Doesn't work ?! 188 | this._characteristic.removeAllListeners('get') 189 | this._characteristic.removeAllListeners('set') 190 | } 191 | return 192 | } 193 | if (this._characteristic != null) { 194 | this._service.removeCharacteristic(this._characteristic) 195 | } 196 | delete this._serviceDelegate._context[this.key] 197 | } 198 | 199 | // Check characteristic permissions. 200 | _hasPerm (perm) { 201 | return this._characteristic == null 202 | ? false 203 | : this._characteristic.props.perms.includes(perm) 204 | } 205 | 206 | get _canRead () { 207 | return this._hasPerm(this.Characteristic.Perms.PAIRED_READ) 208 | } 209 | 210 | get _canWrite () { 211 | return this._hasPerm(this.Characteristic.Perms.PAIRED_WRITE) 212 | } 213 | 214 | get _canNotify () { 215 | return this._hasPerm(this.Characteristic.Perms.NOTIFY) 216 | } 217 | 218 | get _writeResponse () { 219 | return this._hasPerm(this.Characteristic.Perms.WRITE_RESPONSE) 220 | } 221 | 222 | get _writeOnly () { 223 | return this._canWrite && !this._canRead && !this._canNotifiy 224 | } 225 | 226 | get _notifyOnly () { 227 | return this._characteristic == null 228 | ? false 229 | : this._characteristic.UUID === 230 | this.Characteristics.hap.ProgrammableSwitchEvent.UUID 231 | } 232 | 233 | /** Current log level (of the associated accessory delegate). 234 | * 235 | * The log level determines what type of messages are printed: 236 | * 237 | * 0. Print error and warning messages. 238 | * 1. Print error, warning, and log messages. 239 | * 2. Print error, warning, log, and debug messages. 240 | * 3. Print error, warning, log, debug, and verbose debug messages. 241 | * 242 | * Note that debug messages (level 2 and 3) are only printed when 243 | * Homebridge was started with the `-D` or `--debug` command line option. 244 | * 245 | * @type {!integer} 246 | * @readonly 247 | */ 248 | get logLevel () { 249 | return this._serviceDelegate._accessoryDelegate.logLevel 250 | } 251 | 252 | get displayName () { 253 | return this._characteristic == null 254 | ? this._key 255 | : this._characteristic.displayName 256 | } 257 | 258 | get _namePrefix () { 259 | return this._serviceDelegate._namePrefix + this.displayName + ': ' 260 | } 261 | 262 | validate (value) { 263 | let s = '' 264 | if (this._characteristic != null) { 265 | switch (this._characteristic.props.format) { 266 | case this.Characteristic.Formats.BOOL: 267 | break 268 | case this.Characteristic.Formats.UINT8: 269 | case this.Characteristic.Formats.UINT16: 270 | case this.Characteristic.Formats.UINT32: 271 | case this.Characteristic.Formats.UINT64: 272 | case this.Characteristic.Formats.INT: 273 | value = Math.round(value) 274 | // fallsthrough 275 | case this.Characteristic.Formats.FLOAT: 276 | if (value < this._characteristic.props.minValue) { 277 | value = this._characteristic.props.minValue 278 | s = ' [min]' 279 | } else if (value > this._characteristic.props.maxValue) { 280 | value = this._characteristic.props.maxValue 281 | s = ' [max]' 282 | } 283 | break 284 | case this.Characteristic.Formats.STRING: 285 | if ( 286 | value != null && value.length > this._characteristic.props.length 287 | ) { 288 | value = value.slice(0, this._characteristic.props.length) 289 | s = ' [truncated]' 290 | } 291 | break 292 | default: 293 | break 294 | } 295 | } 296 | return { value, s } 297 | } 298 | 299 | /** Value of associated Characteristic. 300 | */ 301 | get value () { 302 | return this._serviceDelegate._context[this._key] 303 | } 304 | 305 | set value (v) { 306 | const { value, s } = this.validate(v) 307 | 308 | // Check for actual change. 309 | if (value === this.value && !this._notifyOnly) { 310 | return 311 | } 312 | 313 | // Issue info message that Characteristic value is updated by the plugin. 314 | if (this._notifyOnly) { 315 | this._log('%s%s', ['Single Press', 'Double Press', 'Long Press'][value], s) 316 | } else if (this.value == null) { 317 | this._log('set to %j%s%s', value, this._unit, s) 318 | } else { 319 | this._log('set to %j%s%s (from %j%s)', value, this._unit, s, this.value, this._unit) 320 | } 321 | 322 | // Update persisted value in ~/.homebridge/accessories/cachedAccessories. 323 | this._serviceDelegate._context[this._key] = value 324 | 325 | if (this._characteristic != null) { 326 | // Update value of associated Characteristic. 327 | this._characteristic.updateValue(value) 328 | } 329 | 330 | /** Emitted when Homebridge characteristic value has changed, either from 331 | * HomeKit or by the plugin. 332 | * 333 | * On receiving this event, the plugin should update the corresponding 334 | * device attribute. 335 | * @event CharacteristicDelegate#didSet 336 | * @param {*} value - The new characteristic value. 337 | * @param {boolean} byHomeKit - Value was set by HomeKit. 338 | */ 339 | this.emit('didSet', value, false) 340 | } 341 | 342 | /** Set the value of the associated HomeKit characteristic. 343 | */ 344 | setValue (value) { 345 | if (this._characteristic != null) { 346 | // Update value of associated Characteristic. 347 | this._characteristic.setValue(value) 348 | } 349 | } 350 | 351 | // Called when characteristic with a getter is read from HomeKit. 352 | async _onGet (callback) { 353 | let timedOut = false 354 | let value = this.value 355 | const timeout = setTimeout(() => { 356 | timedOut = true 357 | try { 358 | callback(null, this.value) 359 | } catch (error) { this.warn(error) } 360 | }, this._timeout) 361 | try { 362 | value = await this._getter() 363 | } catch (error) { 364 | this.error(error) 365 | } 366 | clearTimeout(timeout) 367 | 368 | if (timedOut) { 369 | this._log('get: ignore %j%s - timed out', value, this._unit) 370 | } else { 371 | // Return value to HomeKit. 372 | this._log('get: return %j%s', value, this._unit) 373 | try { 374 | callback(null, value) 375 | } catch (error) { this.warn(error) } 376 | } 377 | 378 | if (value !== this.value) { 379 | // Update persisted value in ~/.homebridge/accessories/cachedAccessories. 380 | this._serviceDelegate._context[this._key] = value 381 | 382 | // Inform service delegate. 383 | this.emit('didSet', value, false) 384 | } 385 | } 386 | 387 | // Called when characteristic is updated from HomeKit. 388 | async _onSet (v, callback) { 389 | const { value } = this.validate(v) 390 | let result 391 | 392 | // Issue info message that Characteristic value was updated from HomeKit. 393 | if (this._writeOnly || this.value == null) { 394 | this._log('changed to %j%s', value, this._unit) 395 | } else if (value !== this.value) { 396 | this._log('changed to %j%s (from %j%s)', value, this._unit, this.value, this._unit) 397 | } 398 | 399 | // Check for actual change. 400 | if (value === this.value && !this._writeOnly) { 401 | /** Emitted when Homebridge characteristic value has not changed, but 402 | * HomeKit issued an update anyways. 403 | * @event CharacteristicDelegate#didTouch 404 | * @param {*} value - The (unchanged) characteristic value. 405 | * @param {boolean} byHomeKit - Value was set by HomeKit (always true). 406 | */ 407 | this.emit('didTouch', value, true) 408 | try { 409 | callback() 410 | } catch (error) { this.warn(error) } 411 | return 412 | } 413 | 414 | // Invoke setter if defined, but guard with a timeout. 415 | if (this._setter) { 416 | let timedOut = false 417 | const timeout = setTimeout(() => { 418 | timedOut = true 419 | this.warn('set: timed out') 420 | try { 421 | callback(new Error('timed out')) 422 | } catch (error) { this.warn(error) } 423 | }, this._timeout) 424 | try { 425 | result = await this._setter(value) 426 | } catch (error) { 427 | clearTimeout(timeout) 428 | this.warn('set: %s', error) 429 | if (!timedOut) { 430 | try { 431 | callback(error) 432 | } catch (error) { this.warn(error) } 433 | } 434 | return 435 | } 436 | clearTimeout(timeout) 437 | if (timedOut) { 438 | return 439 | } 440 | } 441 | 442 | // Return status to HomeKit. 443 | if (this._writeResponse) { 444 | try { 445 | callback(null, result) 446 | } catch (error) { this.warn(error) } 447 | } else { 448 | try { 449 | callback() 450 | } catch (error) { this.warn(error) } 451 | } 452 | 453 | // Update persisted value in ~/.homebridge/accessories/cachedAccessories. 454 | this._serviceDelegate._context[this._key] = value 455 | 456 | // Inform service delegate. 457 | this.emit('didSet', value, true) 458 | } 459 | } 460 | 461 | export { CharacteristicDelegate } 462 | -------------------------------------------------------------------------------- /lib/Colour.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/Colour.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Colour conversions. 7 | *
See {@link Colour}. 8 | * @name Colour 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { Colour } from 'hb-lib-tools/Colour' 13 | -------------------------------------------------------------------------------- /lib/CommandLineParser.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/CommandLineParser.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Parser and validator for command-line arguments. 7 | *
See {@link CommandLineParser}. 8 | * @name CommandLineParser 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { CommandLineParser } from 'hb-lib-tools/CommandLineParser' 13 | -------------------------------------------------------------------------------- /lib/CommandLineTool.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/CommandLineTool.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Command-line tool. 7 | *
See {@link CommandLineTool}. 8 | * @name CommandLineTool 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { CommandLineTool } from 'hb-lib-tools/CommandLineTool' 13 | -------------------------------------------------------------------------------- /lib/CustomHomeKitTypes.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/CustomeHomeKitTypes.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | const regExps = { 7 | uuid: /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/, 8 | uuidPrefix: /^[0-9A-F]{1,8}$/, 9 | uuidSuffix: /^-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/ 10 | } 11 | 12 | let hap 13 | let hapCharacteristics 14 | let hapServices 15 | 16 | /** Abstract superclass for {@link EveHomeKitTypes} 17 | * and {@link MyHomeKitTypes}. 18 | *
See {@link CustomHomeKitTypes}. 19 | * @name CustomHomeKitTypes 20 | * @type {Class} 21 | * @memberof module:homebridge-lib 22 | */ 23 | 24 | /** Abstract superclass for {@link EveHomeKitTypes} and {@link MyHomeKitTypes}. 25 | * 26 | * `CustomHomeKitTypes` creates and manages a collection of: 27 | * - Subclasses of {@link Service}, for custom HomeKit service types; and 28 | * - Subclasses of {@link Characteristic}, for custom HomeKit characteristic 29 | * types. 30 | * 31 | * Plugins access these subclasses through 32 | * {@link CustomHomeKitTypes#Services} and 33 | * {@link CustomHomeKitTypes#Characteristics}. 34 | * 35 | * Plugins can create these subclasses through 36 | * {@link CustomHomeKitTypes#createServiceClass} and 37 | * {@link CustomHomeKitTypes#createCharacteristicClass} 38 | * @abstract 39 | */ 40 | class CustomHomeKitTypes { 41 | /** Creates a new instance of `CustomHomeKitTypes`. 42 | * @param {!API} homebridge - Homebridge API. 43 | */ 44 | constructor (homebridge) { 45 | hap = homebridge.hap 46 | this._Services = {} 47 | this._Characteristics = {} 48 | } 49 | 50 | /** Valid HomeKit admin-only access. 51 | * @type {Object} 52 | * @readonly 53 | */ 54 | get Access () { return Object.freeze(Object.assign({}, hap.Access)) } 55 | 56 | /** Valid HomeKit characteristic formats. 57 | * @type {Object} 58 | * @readonly 59 | */ 60 | get Formats () { return Object.freeze(Object.assign({}, hap.Formats)) } 61 | 62 | /** Valid HomeKit characteristic permissions. 63 | * @type {Object} 64 | * @readonly 65 | */ 66 | get Perms () { return Object.freeze(Object.assign({}, hap.Perms)) } 67 | 68 | /** Standard HomeKit characteristic units. 69 | * @type {Object} 70 | * @readonly 71 | */ 72 | get Units () { return Object.freeze(Object.assign({}, hap.Units)) } 73 | 74 | /** {@link Characteristic} subclasses for custom HomeKit characteristics. 75 | * @abstract 76 | * @type {Object.} 77 | * @readonly 78 | */ 79 | get Characteristics () { return this._Characteristics } 80 | 81 | /** {@link Service} subclasses for custom HomeKit services. 82 | * @abstract 83 | * @type {Object.} 84 | * @readonly 85 | */ 86 | get Services () { return this._Services } 87 | 88 | /** @link Characteristic} subclasses for standard HomeKit characteristics. 89 | * @type {Object} 90 | * @readonly 91 | */ 92 | get hapCharacteristics () { 93 | if (hapCharacteristics == null) { 94 | hapCharacteristics = {} 95 | Object.keys(hap.Characteristic).sort().filter((key) => { 96 | return regExps.uuid.test(hap.Characteristic[key].UUID) 97 | }).forEach((key) => { 98 | hapCharacteristics[key] = hap.Characteristic[key] 99 | }) 100 | Object.freeze(hapCharacteristics) 101 | } 102 | return hapCharacteristics 103 | } 104 | 105 | /** {@link Service} subclasses for custom HomeKit services. 106 | * @type {Object} 107 | * @readonly 108 | */ 109 | get hapServices () { 110 | if (hapServices == null) { 111 | hapServices = {} 112 | Object.keys(hap.Service).sort().filter((key) => { 113 | return regExps.uuid.test(hap.Service[key].UUID) 114 | }).forEach((key) => { 115 | hapServices[key] = hap.Service[key] 116 | }) 117 | Object.freeze(hapServices) 118 | } 119 | return hapServices 120 | } 121 | 122 | /** Creates a new subclass of {@link Characteristic} 123 | * for a custom HomeKit characteristic. 124 | * 125 | * The newly created subclass is stored under `key` in 126 | * {@link CustomHomeKitTypes#Characteristics}. 127 | * 128 | * @final 129 | * @param {!string} key - Key for the Characteristic subclass. 130 | * @param {!string} uuid - Custom characteristic UUID. 131 | * @param {Props} props - Custom characteristic properties. 132 | * @param {string} [displayName=key] - Name displayed in HomeKit. 133 | * @returns {Class} The new {@link Characteristic} subclass. 134 | * @throws {TypeError} When a parameter has an invalid type. 135 | * @throws {RangeError} When a parameter has an invalid value. 136 | * @throws {SyntaxError} On duplicate key. 137 | */ 138 | createCharacteristicClass (key, uuid, props, displayName = key) { 139 | if (typeof key !== 'string') { 140 | throw new TypeError('key: not a string') 141 | } 142 | if (key === '') { 143 | throw new RangeError('key: invalid empty string') 144 | } 145 | if (this._Characteristics[key] != null) { 146 | throw new SyntaxError(`${key}: duplicate key`) 147 | } 148 | if (!regExps.uuid.test(uuid)) { 149 | throw new RangeError(`uuid: ${uuid}: invalid UUID`) 150 | } 151 | 152 | this._Characteristics[key] = class extends hap.Characteristic { 153 | constructor () { 154 | super(displayName, uuid) 155 | this.setProps(props) 156 | this.value = this.getDefaultValue() 157 | } 158 | } 159 | this._Characteristics[key].UUID = uuid 160 | return this._Characteristics[key] 161 | } 162 | 163 | /** Creates a new subclass of {@link Service} 164 | * for a custom HomeKit service. 165 | * 166 | * The newly created subclass is stored under 167 | * {@link CustomHomeKitTypes#Services}. 168 | * 169 | * @final 170 | * @param {!string} key - Key for the Service. 171 | * @param {!string} uuid - UUID for the Service. 172 | * @param {Class[]} Characteristics - {@link Characteristic} 173 | * subclasses for pre-defined characteristics. 174 | * @param {Class[]} [OptionalCharacteristics=[]] - 175 | * {@link Characteristic} subclasses for optional characteristics. 176 | * @returns {Class} The new {@link Service} subclass. 177 | * @throws {TypeError} When a parameter has an invalid type. 178 | * @throws {RangeError} When a parameter has an invalid value. 179 | * @throws {SyntaxError} On duplicate key. 180 | */ 181 | createServiceClass (key, uuid, Characteristics, OptionalCharacteristics = []) { 182 | if (typeof key !== 'string') { 183 | throw new TypeError('key: not a string') 184 | } 185 | if (key === '') { 186 | throw new RangeError('key: invalid empty string') 187 | } 188 | if (this._Services[key] != null) { 189 | throw new SyntaxError(`${key}: duplicate key`) 190 | } 191 | if (!regExps.uuid.test(uuid)) { 192 | throw new RangeError(`uuid: ${uuid}: invalid UUID`) 193 | } 194 | 195 | this._Services[key] = class extends hap.Service { 196 | constructor (displayName, subtype) { 197 | super(displayName, uuid, subtype) 198 | for (const Characteristic of Characteristics) { 199 | this.addCharacteristic(Characteristic) 200 | } 201 | for (const Characteristic of OptionalCharacteristics) { 202 | this.addOptionalCharacteristic(Characteristic) 203 | } 204 | } 205 | } 206 | this._Services[key].UUID = uuid 207 | return this._Services[key] 208 | } 209 | 210 | /** Return the full HAP UUID. 211 | * @final 212 | * @param {!string} id - The short HAP UUID. 213 | * @param {?string} [suffix='-0000-1000-8000-0026BB765291'] - The suffix for 214 | * the long UUID.
215 | * The default value is used by the standard HomeKit services and 216 | * characteristics, as defined by Apple. 217 | * @returns {!string} The full HAP UUID. 218 | * @throws {TypeError} When a parameter has an invalid type. 219 | * @throws {RangeError} When a parameter has an invalid value. 220 | */ 221 | static uuid (id, suffix = '-0000-1000-8000-0026BB765291') { 222 | if (typeof id !== 'string') { 223 | throw new TypeError('id: not a string') 224 | } 225 | if (!regExps.uuidPrefix.test(id)) { 226 | throw new RangeError(`id: ${id}: invalid id`) 227 | } 228 | if (typeof suffix !== 'string') { 229 | throw new TypeError('suffix: not a string') 230 | } 231 | if (!regExps.uuidSuffix.test(suffix)) { 232 | throw new RangeError(`suffix: ${suffix}: invalid suffix`) 233 | } 234 | return ('00000000' + id).slice(-8) + suffix 235 | } 236 | } 237 | 238 | export { CustomHomeKitTypes } 239 | -------------------------------------------------------------------------------- /lib/Delegate.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/Delegate.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { EventEmitter } from 'node:events' 7 | 8 | import { HttpClient } from 'hb-lib-tools/HttpClient' 9 | 10 | import { Platform } from 'homebridge-lib/Platform' 11 | 12 | /** Abstract superclass for {@link Platform}, {@link AccessoryDelegate}, 13 | * {@link ServiceDelegate}, and {@link CharacteristicDelegate}. 14 | *
See {@link Delegate}. 15 | * @name Delegate 16 | * @type {Class} 17 | * @memberof module:homebridge-lib 18 | */ 19 | 20 | /** Abstract superclass for {@link Platform}, {@link AccessoryDelegate}, 21 | * {@link ServiceDelegate}, and {@link CharacteristicDelegate}. 22 | * 23 | * `Delegate` provides basic functions for logging and error handling, 24 | * for accessing HAP-NodeJS classes, and for event handling. 25 | * `Delegate` extends [EventEmitter](https://nodejs.org/dist/latest-v14.x/docs/api/events.html#events_class_eventemitter). 26 | * @abstract 27 | * @extends EventEmitter 28 | */ 29 | class Delegate extends EventEmitter { 30 | /** Create a new `Delegate` instance. 31 | * @abstract 32 | * @param {?Platform} platform - Reference to the corresponding platform 33 | * plugin instance.
34 | * Must be non-null, except for instances of `Platform`. 35 | * @param {?string} name - The name used to prefix log and error messages. 36 | */ 37 | constructor (platform, name) { 38 | super() 39 | if (platform == null) { 40 | platform = this 41 | } 42 | if (!(platform instanceof Platform)) { 43 | throw new TypeError('platform: not a Platform') 44 | } 45 | this._platform = platform 46 | this.name = name 47 | } 48 | 49 | /** HomeKit accessory property values. 50 | * @type {object} 51 | * @property {Object} Categories - 52 | * Valid HomeKit accessory categories. 53 | * @readonly 54 | */ 55 | get Accessory () { return this._platform.Accessory } 56 | 57 | /** HomeKit characteristic property values. 58 | * 59 | * @type {object} 60 | * @property {Object} Formats - 61 | * Valid HomeKit characteristic formats. 62 | * @property {Object} Perms - 63 | * Valid HomeKit characteristic permissions. 64 | * @property {Object} Units - 65 | * Standard HomeKit characteristic units. 66 | * @readonly 67 | */ 68 | get Characteristic () { return this._platform.Characteristic } 69 | 70 | /** Subclasses of {@link Characteristic} for HomeKit characteristic types. 71 | * @type {object} 72 | * @property {Object} eve - Subclasses for custom HomeKit 73 | * characteristic types used by Eve. 74 | *
See {@link EveHomeKitTypes#Characteristics}. 75 | * @property {Object} hap - Subclasses for standard HomeKit 76 | * characteristic types. 77 | * @property {Object} my - Subclasses for my custom HomeKit 78 | * characteristic typess. 79 | *
See {@link MyHomeKitTypes#Characteristics}. 80 | * @readonly 81 | */ 82 | get Characteristics () { return this._platform.Characteristics } 83 | 84 | /** Subclasses of {@link Service} for HomeKit service types. 85 | * @type {object} 86 | * @property {Object} eve - Subclasses for custom HomeKit 87 | * service types used by Eve. 88 | *
See {@link EveHomeKitTypes#Services}. 89 | * @property {Object} hap - Subclasses for standard HomeKit 90 | * service types. 91 | * @property {Object} my - Subclasses for my custom HomeKit 92 | * characteristic typess. 93 | *
See {@link MyHomeKitTypes#Services}. 94 | * @readonly 95 | */ 96 | get Services () { return this._platform.Services } 97 | 98 | /** Current log level. 99 | * 100 | * The log level determines what type of messages are printed: 101 | * 102 | * 0. Print error and warning messages. 103 | * 1. Print error, warning, and log messages. 104 | * 2. Print error, warning, log, and debug messages. 105 | * 3. Print error, warning, log, debug, and verbose debug messages. 106 | * 3. Print error, warning, log, debug, verbose debug, and very verbose 107 | * debug messages. 108 | * 109 | * Note that debug messages (level 2, 3 and 4) are only printed when 110 | * Homebridge was started with the `-D` or `--debug` command line option. 111 | * 112 | * The log level defaults at 2. 113 | * 114 | * @type {!integer} 115 | * @readonly 116 | */ 117 | get logLevel () { 118 | return 2 119 | } 120 | 121 | /** The name used to prefix log and error messages. 122 | * @type {string} 123 | */ 124 | get name () { 125 | return this._name 126 | } 127 | 128 | set name (name) { 129 | if (name != null && typeof name !== 'string') { 130 | throw new TypeError(`${name}: not a string`) 131 | } 132 | if (name === '') { 133 | throw new RangeError(`${name}: invalid name`) 134 | } 135 | this._name = name 136 | } 137 | 138 | /** The name prefix for log messages. 139 | * @type {string} 140 | */ 141 | get _namePrefix () { 142 | return this._name == null ? '' : this._name + ': ' 143 | } 144 | 145 | /** Reference to the corresponding platform plugin instance. 146 | * 147 | * For instances of `Platform`, it returns `this`. 148 | * @type {!Platform} 149 | * @readonly 150 | */ 151 | get platform () { 152 | return this._platform 153 | } 154 | 155 | /** Print a debug message to Homebridge standard output. 156 | *
The message is printed only, when the current log level >= 2 and when 157 | * Homebridge was started with the `-D` or `--debug` command line option. 158 | * @param {string|Error} format - The printf-style message or an instance of 159 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 160 | * @param {...string} args - Arguments to the printf-style message. 161 | */ 162 | debug (format, ...args) { 163 | this._platform._message( 164 | 'debug', this.logLevel, this._namePrefix, format, ...args 165 | ) 166 | } 167 | 168 | /** Safely emit an event, catching any errors. 169 | * @param {!string} eventName - The name of the event. 170 | * @param {...string} args - Arguments to the event. 171 | */ 172 | emit (eventName, ...args) { 173 | try { 174 | super.emit(eventName, ...args) 175 | } catch (error) { 176 | this.error(error) 177 | } 178 | } 179 | 180 | /** Print an error message to Homebridge standard error output. 181 | * @param {string|Error} format - The printf-style message or an instance of 182 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 183 | * @param {...string} args - Arguments to the printf-style message. 184 | */ 185 | error (format, ...args) { 186 | if (format instanceof HttpClient.HttpError) { 187 | // Error already emitted by HttpClient. 188 | return 189 | } 190 | this._platform._message( 191 | 'error', this.logLevel, this._namePrefix, format, ...args 192 | ) 193 | } 194 | 195 | /** Print an error message to Homebridge standard error output and shutdown 196 | * Homebridge. 197 | * @param {string|Error} format - The printf-style message or an instance of 198 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 199 | * @param {...string} args - Arguments to the printf-style message. 200 | */ 201 | fatal (format, ...args) { 202 | this._platform._message( 203 | 'fatal', this.logLevel, this._namePrefix, format, ...args 204 | ) 205 | if (!this._platform._shuttingDown) { 206 | process.kill(process.pid, 'SIGTERM') 207 | } 208 | } 209 | 210 | /** Print a log message to Homebridge standard output. 211 | *
The message is printed only, when the current log level >= 1. 212 | * @param {string|Error} format - The printf-style message or an instance of 213 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 214 | * @param {...string} args - Arguments to the printf-style message. 215 | */ 216 | log (format, ...args) { 217 | this._platform._message( 218 | 'log', this.logLevel, this._namePrefix, format, ...args 219 | ) 220 | } 221 | 222 | /** Print a verbose debug message to Homebridge standard output. 223 | *
The message is printed only, when the current log level >= 3 and when 224 | * Homebridge was started with the `-D` or `--debug` command line option. 225 | * @param {string|Error} format - The printf-style message or an instance of 226 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 227 | * @param {...string} args - Arguments to the printf-style message. 228 | */ 229 | vdebug (format, ...args) { 230 | this._platform._message( 231 | 'vdebug', this.logLevel, this._namePrefix, format, ...args 232 | ) 233 | } 234 | 235 | /** Print a very verbose debug message to Homebridge standard output. 236 | *
The message is printed only, when the current log level >= 4 and when 237 | * Homebridge was started with the `-D` or `--debug` command line option. 238 | * @param {string|Error} format - The printf-style message or an instance of 239 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 240 | * @param {...string} args - Arguments to the printf-style message. 241 | */ 242 | vvdebug (format, ...args) { 243 | this._platform._message( 244 | 'vvdebug', this.logLevel, this._namePrefix, format, ...args 245 | ) 246 | } 247 | 248 | /** Print a warning message to Homebridge standard error output. 249 | * @param {string|Error} format - The printf-style message or an instance of 250 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 251 | * @param {...string} args - Arguments to the printf-style message. 252 | */ 253 | warn (format, ...args) { 254 | this._platform._message( 255 | 'warning', this.logLevel, this._namePrefix, format, ...args 256 | ) 257 | } 258 | } 259 | 260 | export { Delegate } 261 | -------------------------------------------------------------------------------- /lib/EveHomeKitTypes.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/EveHomeKitTypes.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { CustomHomeKitTypes } from 'homebridge-lib/CustomHomeKitTypes' 7 | 8 | // Return long Eve UUID. 9 | function uuid (id) { 10 | if (typeof id !== 'string' || id.length !== 3) { 11 | throw new TypeError(`${id}: illegal id`) 12 | } 13 | // UUID range used by Eve 14 | return `E863F${id}-079E-48FF-8F27-9C2605A29F52` 15 | } 16 | 17 | /** Custom HomeKit services and characteristics used by 18 | * [Eve](https://www.evehome.com/en) accessories and by the 19 | * [Eve app](https://www.evehome.com/en/eve-app). 20 | *
See {@link EveHomeKitTypes}. 21 | * @name EveHomeKitTypes 22 | * @type {Class} 23 | * @memberof module:homebridge-lib 24 | */ 25 | 26 | /** Custom HomeKit Services and Characteristic used by 27 | * [Eve](https://www.evehome.com/en) accessories and by the 28 | * [Eve app](https://www.evehome.com/en/eve-app). 29 | * 30 | * For more info, see the 31 | * [Wiki](https://github.com/simont77/fakegato-history/wiki/Services-and-characteristics-for-Elgato-Eve-devices) 32 | * of Simone Tisa's 33 | * [`fakegato-history`](https://github.com/simont77/fakegato-history) 34 | * repository or the related 35 | * [Gist](https://gist.github.com/simont77/3f4d4330fa55b83f8ca96388d9004e7d). 36 | * @extends CustomHomeKitTypes 37 | */ 38 | class EveHomeKitTypes extends CustomHomeKitTypes { 39 | /** Start time for Eve history (2001-01-01T00:00:00Z). 40 | * @type {integer} 41 | * @readonly 42 | */ 43 | static get epoch () { return new Date('2001-01-01T00:00:00Z') / 1000 } 44 | 45 | /** Create custom HomeKit Services and Characteristics used by Eve. 46 | * @param {object} homebridge - API object from homebridge 47 | */ 48 | constructor (homebridge) { 49 | super(homebridge) 50 | 51 | /** @member EveHomeKitTypes#Characteristics 52 | * @property {Class} AirParticulateDensity - Deprecated. use `VOCLevel` 53 | * instead. 54 | * @property {Class} AirPressure - Air pressure (in hPa). 55 | *
Used by: Eve Weather. 56 | * @property {Class} ClosedDuration - Duration (in seconds) that door has 57 | * been closed. 58 | *
Used by: Eve Door. 59 | * @property {Class} Clouds - Cloud coverage (in %). 60 | *
Used by: weather station. 61 | * @property {Class} ColorTemperature - Colour temperature in mired. 62 | *
Used by: Hue bridge, before `hap.ColorTemperature` was defined. 63 | * @property {Class} ColorTemperatureKelvin - Color temperature in K. 64 | *
Used by: Nanoleaf. 65 | * @property {Class} Condition - Weather condition (as text). 66 | *
Used by: weather station. 67 | * @property {Class} ConditionCategory - Weather condition 68 | * (as numberic code). 69 | *
Used by: weather station. 70 | * @property {Class} ConfigCommand - Used by Eve app to set configuration. 71 | *
Used by: `History` service. 72 | * @property {Class} ConfigData - Used by Eve app to read configuration. 73 | *
Used by: `History` service. 74 | * @property {Class} Consumption - Current electric power (in W). 75 | *
Used by: Eve Energy. 76 | * @property {Class} CurrentConsumption - Deprecated. use `Consumption` 77 | * instead. 78 | * @property {Class} CurrentTemperature - Current temperature (in °C). 79 | * This is the same characterisic as `hap.CurrentTemperature`, but with 80 | * a minimum value of -40°C instead of 0°C (or -270°C). 81 | * @property {Class} Day - Weekday for forecast. 82 | *
Used by: weather station. 83 | * @property {Class} DewPoint - Dew point (in °C). 84 | *
Used by: weather station. 85 | * @property {Class} Duration - Duration (in s) that Motion sensor reports 86 | * motion. 87 | *
Used by: Eve Motion. 88 | *
Shown by Eve app in accessory _Settings_ screen. 89 | * @property {Class} ElectricCurrent - Electric current (in A). 90 | *
Used by: Eve Energy. 91 | * @property {Class} Elevation - Elevation (in m) to calibrate air 92 | * pressure. 93 | *
Used by: Eve Weather. 94 | *
Shown by Eve app in accessory _Settings_ screen. 95 | * @property {Class} HistoryEntries - Used by accessory to return history 96 | * entries. 97 | *
Used by: `History` service. 98 | * @property {Class} HistoryRequest - Used by Eve app to request history 99 | * entries. 100 | *
Used by: `History` service. 101 | * @property {Class} HistoryStatus - Used by accessory signal new history 102 | * entries. 103 | *
Used by: `History` service. 104 | * @property {Class} LastActivation - Time (in seconds since epoch) of 105 | * last event. 106 | *
Used by: Eve Door, Eve Motion. 107 | * @property {Class} MaximumWindSpeed - Maximum wind speed (in m/s). 108 | *
Used by: weather station. 109 | * @property {Class} MinimumTemperature - Minimum temperature (in °C). 110 | *
Used by: weather station. 111 | * @property {Class} ObservationTime - Time of observation. 112 | *
Used by: weather station. 113 | * @property {Class} OpenDuration - Duration (in seconds) that door has 114 | * been open. 115 | *
Used by: Eve Door. 116 | * @property {Class} Ozone - Ozone level (in DU). 117 | *
Used by: weather station. 118 | * @property {Class} ProgramCommand - Used for programming schedules - 119 | * details unknown. 120 | *
Used by: Eve Thermo. 121 | * @property {Class} ProgramData - Used for programming schedules - 122 | * details unknown. 123 | *
Used by: Eve Thermo. 124 | * @property {Class} Rain - Rain (as boolean). 125 | *
Used by: weather station. 126 | * @property {Class} Rain1h - Rain (in mm) during past hour. 127 | *
Used by: weather station. 128 | * @property {Class} Rain24h - Rain (in mm) during past 24 hours. 129 | *
Used by: weather station. 130 | * @property {Class} RainProbability - Probability of rain (in %). 131 | *
Used by: weather station. 132 | * @property {Class} ResetTotal - Time (as seconds since epoch) of 133 | * last reset. 134 | *
Used by: `History` service. 135 | * @property {Class} Sensitivity - Motion sensor sensitivity. 136 | *
Used by: Eve Motion. 137 | *
Shown by Eve app in accessory _Settings_ screen. 138 | * @property {Class} SetTime - Used to sync time with accessory. 139 | *
Used by: `History` service. 140 | * @property {Class} Snow - Snow (as boolean). 141 | *
Used by: weather station. 142 | * @property {Class} TimesOpened - Number of times the door was opened. 143 | *
Used by: Eve Door. 144 | * @property {Class} TotalConsumption - Life-time electric consumption 145 | * (in KWh). 146 | *
Used by: Eve Energy. 147 | * @property {Class} UvIndex - UV index. 148 | *
Used by: weather station. 149 | * @property {Class} ValvePosition - Current radiator valve position 150 | * (in %). 151 | *
Used by: Eve Thermo. 152 | * @property {Class} Visibility - Visibility (in km). 153 | *
Used by: weather station. 154 | * @property {Class} VOCLevel - Volatile Organic Compound level (in ppb). 155 | *
Used by: Eve Room (1st gen). 156 | * @property {Class} Voltage - Electric voltage (in V). 157 | *
Used by: Eve Energy. 158 | * @property {Class} WindDirection - Wind direction (as text). 159 | *
Used by: weather station. 160 | * @property {Class} WindSpeed - Wind speed (in m/s). 161 | *
Used by: weather station. 162 | */ 163 | 164 | // ========================================================================= 165 | 166 | // The following custom characteristics are defined by Eve. 167 | // These are listed in order of UUID. 168 | 169 | this.createCharacteristicClass('Voltage', uuid('10A'), { 170 | format: this.Formats.FLOAT, 171 | unit: 'V', 172 | minValue: 0, 173 | maxValue: 380, 174 | minStep: 0.1, 175 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 176 | }) 177 | 178 | this.createCharacteristicClass('AirParticulateDensity', uuid('10B'), { 179 | format: this.Formats.FLOAT, 180 | unit: 'ppm', 181 | minValue: 0, 182 | maxValue: 5000, 183 | minStep: 1, 184 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 185 | }, 'VOC Level') 186 | 187 | this.createCharacteristicClass('VOCLevel', uuid('10B'), { 188 | format: this.Formats.INT16, 189 | unit: 'ppb', 190 | minValue: 5, 191 | maxValue: 5000, 192 | minStep: 5, 193 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 194 | }, 'VOC Level') 195 | 196 | this.createCharacteristicClass('TotalConsumption', uuid('10C'), { 197 | format: this.Formats.FLOAT, 198 | unit: 'kWh', 199 | minValue: 0, 200 | maxValue: 1000000, 201 | minStep: 0.01, 202 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 203 | }, 'Total Consumption') 204 | 205 | this.createCharacteristicClass('Consumption', uuid('10D'), { 206 | format: this.Formats.FLOAT, 207 | unit: 'W', 208 | minValue: 0, 209 | maxValue: 12000, 210 | minStep: 0.1, 211 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 212 | }, 'Consumption') 213 | 214 | this.createCharacteristicClass('CurrentConsumption', uuid('10D'), { 215 | format: this.Formats.FLOAT, 216 | unit: 'W', 217 | minValue: 0, 218 | maxValue: 12000, 219 | minStep: 0.1, 220 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 221 | }, 'Consumption') 222 | 223 | this.createCharacteristicClass('AirPressure', uuid('10F'), { 224 | format: this.Formats.FLOAT, 225 | unit: 'hPa', 226 | minValue: 700, 227 | maxValue: 1100, 228 | minStep: 0.1, 229 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 230 | }, 'Air Pressure') 231 | 232 | this.createCharacteristicClass('ResetTotal', uuid('112'), { 233 | format: this.Formats.UINT32, 234 | unit: this.Units.seconds, // since 2001/01/01 235 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE], 236 | adminOnlyAccess: [this.Access.WRITE] 237 | }, 'Reset Total') 238 | 239 | this.createCharacteristicClass('HistoryStatus', uuid('116'), { 240 | format: this.Formats.DATA, 241 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.HIDDEN] 242 | }, 'History Status') 243 | 244 | this.createCharacteristicClass('HistoryEntries', uuid('117'), { 245 | format: this.Formats.DATA, 246 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.HIDDEN] 247 | }, 'History Entries') 248 | 249 | this.createCharacteristicClass('OpenDuration', uuid('118'), { 250 | format: this.Formats.UINT32, 251 | unit: this.Units.SECONDS, // since last reset 252 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE] 253 | }, 'Open Duration') 254 | 255 | this.createCharacteristicClass('ClosedDuration', uuid('119'), { 256 | format: this.Formats.UINT32, 257 | unit: this.Units.SECONDS, // since last reset 258 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE] 259 | }, 'Closed Duration') 260 | 261 | this.createCharacteristicClass('LastActivation', uuid('11A'), { 262 | format: this.Formats.UINT32, 263 | unit: this.Units.SECONDS, // since last reset 264 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 265 | }, 'Last Activation') 266 | 267 | this.createCharacteristicClass('HistoryRequest', uuid('11C'), { 268 | format: this.Formats.DATA, 269 | perms: [this.Perms.PAIRED_WRITE, this.Perms.HIDDEN] 270 | }, 'History Request') 271 | 272 | this.createCharacteristicClass('ConfigCommand', uuid('11D'), { 273 | format: this.Formats.DATA, 274 | perms: [this.Perms.PAIRED_WRITE, this.Perms.HIDDEN] 275 | }, 'Config Command') 276 | 277 | // On various Eve devices - presumably for firmware upgrade. 278 | this.createCharacteristicClass('Char11E', uuid('11E'), { 279 | format: this.Formats.DATA, 280 | perms: [this.Perms.PAIRED_READ, this.Perms.PAIRED_WRITE, this.Perms.HIDDEN] 281 | }, 'Eve 11E') 282 | 283 | this.createCharacteristicClass('Sensitivity', uuid('120'), { 284 | format: this.Formats.UINT8, 285 | minValue: 0, 286 | maxValue: 7, 287 | validValues: [0, 4, 7], 288 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE], 289 | adminOnlyAccess: [this.Access.WRITE] 290 | }) 291 | this.Characteristics.Sensitivity.HIGH = 0 292 | this.Characteristics.Sensitivity.MEDIUM = 4 293 | this.Characteristics.Sensitivity.LOW = 7 294 | 295 | this.createCharacteristicClass('SetTime', uuid('121'), { 296 | format: this.Formats.DATA, 297 | perms: [this.Perms.PAIRED_WRITE, this.Perms.HIDDEN] 298 | }) 299 | 300 | this.createCharacteristicClass('ElectricCurrent', uuid('126'), { 301 | format: this.Formats.FLOAT, 302 | unit: 'A', 303 | minValue: 0, 304 | maxValue: 48, 305 | minStep: 0.01, 306 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 307 | }, 'Electric Current') 308 | 309 | this.createCharacteristicClass('TimesOpened', uuid('129'), { 310 | format: this.Formats.UINT32, 311 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 312 | }, 'Times Opened') 313 | 314 | this.createCharacteristicClass('ProgramCommand', uuid('12C'), { 315 | format: this.Formats.DATA, 316 | perms: [this.Perms.PAIRED_WRITE] 317 | }, 'Program Command') 318 | 319 | this.createCharacteristicClass('Duration', uuid('12D'), { 320 | format: this.Formats.UINT16, 321 | unit: this.Units.SECONDS, 322 | minValue: 5, 323 | maxValue: 15 * 3600, 324 | validValues: [ 325 | 5, 10, 20, 30, 326 | 1 * 60, 2 * 60, 3 * 60, 5 * 60, 10 * 60, 20 * 60, 30 * 60, 327 | 1 * 3600, 2 * 3600, 3 * 3600, 5 * 3600, 10 * 3600, 12 * 3600, 15 * 3600 328 | ], 329 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE], 330 | adminOnlyAccess: [this.Access.WRITE] 331 | }) 332 | this.Characteristics.Duration.VALID_VALUES = [ 333 | 5, 10, 20, 30, 334 | 1 * 60, 2 * 60, 3 * 60, 5 * 60, 10 * 60, 20 * 60, 30 * 60, 335 | 1 * 3600, 2 * 3600, 3 * 3600, 5 * 3600, 10 * 3600, 12 * 3600, 15 * 3600 336 | ] 337 | 338 | this.createCharacteristicClass('ValvePosition', uuid('12E'), { 339 | format: this.Formats.UINT8, 340 | unit: this.Units.PERCENTAGE, 341 | minValue: 0, 342 | maxValue: 100, 343 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 344 | }, 'Valve Position') 345 | 346 | this.createCharacteristicClass('ProgramData', uuid('12F'), { 347 | format: this.Formats.DATA, 348 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 349 | }, 'Program Data') 350 | 351 | this.createCharacteristicClass('Elevation', uuid('130'), { 352 | format: this.Formats.INT, 353 | unit: 'm', 354 | minValue: -450, 355 | maxValue: 9000, 356 | minStep: 1, 357 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE], 358 | adminOnlyAccess: [this.Access.WRITE] 359 | }) 360 | 361 | this.createCharacteristicClass('ConfigData', uuid('131'), { 362 | format: this.Formats.DATA, 363 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 364 | }, 'Config Data') 365 | 366 | this.createCharacteristicClass('ElgatoDeviceStatus', uuid('134'), { 367 | format: this.Formats.UINT32, 368 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 369 | }) 370 | this.Characteristics.ElgatoDeviceStatus.SMOKE_DETECTED = 1 << 0 371 | this.Characteristics.ElgatoDeviceStatus.HEAT_DETECTED = 1 << 1 372 | this.Characteristics.ElgatoDeviceStatus.ALARM_TEST_ACTIVE = 1 << 2 373 | this.Characteristics.ElgatoDeviceStatus.SMOKE_CHAMBER = 1 << 9 374 | this.Characteristics.ElgatoDeviceStatus.SMOKE_SENSOR_DEACTIVATED = 1 << 14 375 | this.Characteristics.ElgatoDeviceStatus.FLASH_STATUS_LED = 1 << 15 376 | this.Characteristics.ElgatoDeviceStatus.ALARM_PAUSED = 1 << 24 377 | this.Characteristics.ElgatoDeviceStatus.ALARM_MUTED = 1 << 25 378 | 379 | this.createCharacteristicClass('WeatherTrend', uuid('136'), { 380 | format: this.Formats.UINT8, 381 | minValue: 0, 382 | maxValue: 15, 383 | minStep: 1, 384 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 385 | }, 'Weather Trend') 386 | this.Characteristics.WeatherTrend.BLANK = 0 // also: 2, 8, 10 387 | this.Characteristics.WeatherTrend.SUN = 1 // also: 9 388 | this.Characteristics.WeatherTrend.CLOUDS_SUN = 3 // also: 11 389 | this.Characteristics.WeatherTrend.RAIN = 4 // also: 5, 6, 7 390 | this.Characteristics.WeatherTrend.RAIN_WIND = 12 // also: 13, 14, 15 391 | 392 | // On various Eve devices - presumably for firmware upgrade. 393 | this.createCharacteristicClass('Char158', uuid('158'), { 394 | format: this.Formats.DATA, 395 | perms: [this.Perms.PAIRED_READ, this.Perms.PAIRED_WRITE, this.Perms.HIDDEN] 396 | }, 'Eve 158') 397 | 398 | // ========================================================================= 399 | 400 | // The following custom characteristics are supported by the Eve app. 401 | // These are listed in alphabetical order. 402 | 403 | this.createCharacteristicClass( 404 | 'Clouds', '64392FED-1401-4F7A-9ADB-1710DD6E3897', { 405 | format: this.Formats.UINT8, 406 | unit: this.Units.PERCENTAGE, 407 | minValue: 0, 408 | maxValue: 100, 409 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 410 | } 411 | ) 412 | 413 | this.createCharacteristicClass( 414 | 'ColorTemperature', 'E887EF67-509A-552D-A138-3DA215050F46', { 415 | format: this.Formats.UINT16, 416 | unit: 'mired', 417 | minValue: 153, 418 | maxValue: 500, 419 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE] 420 | }, 'Color Temperature' 421 | ) 422 | 423 | this.createCharacteristicClass( 424 | 'ColorTemperatureKelvin', 'A18E5901-CFA1-4D37-A10F-0071CEEEEEBD', { 425 | format: this.Formats.UINT16, 426 | unit: 'K', 427 | minValue: 2000, // ct: 500 428 | maxValue: 6536, // ct: 153 429 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY, this.Perms.PAIRED_WRITE] 430 | }, 'Color Temperature' 431 | ) 432 | 433 | this.createCharacteristicClass( 434 | 'Condition', 'CD65A9AB-85AD-494A-B2BD-2F380084134D', { 435 | format: this.Formats.STRING, 436 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 437 | } 438 | ) 439 | 440 | this.createCharacteristicClass( 441 | 'ConditionCategory', 'CD65A9AB-85AD-494A-B2BD-2F380084134C', { 442 | format: this.Formats.UINT16, 443 | mavValue: 999, 444 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 445 | }, 'Condition Category' 446 | ) 447 | 448 | this.createCharacteristicClass( 449 | 'CurrentTemperature', this.hapCharacteristics.CurrentTemperature.UUID, { 450 | format: this.Formats.FLOAT, 451 | unit: this.Units.CELSIUS, 452 | minValue: -40, 453 | maxValue: 100, 454 | minStep: 0.1, 455 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 456 | }, 'Current Temperature' 457 | ) 458 | 459 | this.createCharacteristicClass( 460 | 'Day', '57F1D4B2-0E7E-4307-95B5-808750E2C1C7', { 461 | format: this.Formats.STRING, 462 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 463 | } 464 | ) 465 | 466 | this.createCharacteristicClass( 467 | 'DewPoint', '095C46E2-278E-4E3C-B9E7-364622A0F501', { 468 | format: this.Formats.FLOAT, 469 | unit: this.Units.CELSIUS, 470 | minValue: -40, 471 | maxValue: 100, 472 | minStep: 0.1, 473 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 474 | }, 'Dew Point' 475 | ) 476 | 477 | this.createCharacteristicClass( 478 | 'MaximumWindSpeed', '6B8861E5-D6F3-425C-83B6-069945FFD1F1', { 479 | format: this.Formats.FLOAT, 480 | unit: 'm/s', 481 | minValue: 0, 482 | maxValue: 150, 483 | minStep: 0.1, 484 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 485 | }, 'Maximum Wind Speed' 486 | ) 487 | 488 | this.createCharacteristicClass( 489 | 'MinimumTemperature', '707B78CA-51AB-4DC9-8630-80A58F07E419', { 490 | format: this.Formats.FLOAT, 491 | unit: this.Units.CELSIUS, 492 | minValue: -40, 493 | maxValue: 100, 494 | minStep: 0.1, 495 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 496 | }, 'Minimum Temperature' 497 | ) 498 | 499 | this.createCharacteristicClass( 500 | 'ObservationTime', '234FD9F1-1D33-4128-B622-D052F0C402AF', { 501 | format: this.Formats.STRING, 502 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 503 | }, 'Observation Time' 504 | ) 505 | 506 | this.createCharacteristicClass( 507 | 'Ozone', 'BBEFFDDD-1BCD-4D75-B7CD-B57A90A04D13', { 508 | format: this.Formats.UINT16, 509 | unit: 'DU', 510 | minValue: 0, 511 | maxValue: 500, 512 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 513 | } 514 | ) 515 | 516 | this.createCharacteristicClass( 517 | 'Rain', 'F14EB1AD-E000-4EF4-A54F-0CF07B2E7BE7', { 518 | format: this.Formats.BOOL, 519 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 520 | } 521 | ) 522 | 523 | this.createCharacteristicClass( 524 | 'Rain1h', '10C88F40-7EC4-478C-8D5A-BD0C3CCE14B7', { 525 | format: this.Formats.UINT16, 526 | unit: 'mm', 527 | minValue: 0, 528 | maxValue: 200, 529 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 530 | }, 'Rain Last Hour' 531 | ) 532 | 533 | this.createCharacteristicClass( 534 | 'Rain24h', 'CCC04890-565B-4376-B39A-3113341D9E0F', { 535 | format: this.Formats.UINT16, 536 | unit: 'mm', 537 | minValue: 0, 538 | maxValue: 2000, 539 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 540 | }, 'Total Rain' 541 | ) 542 | 543 | this.createCharacteristicClass( 544 | 'RainProbability', 'FC01B24F-CF7E-4A74-90DB-1B427AF1FFA3', { 545 | format: this.Formats.UINT8, 546 | unit: this.Units.PERCENTAGE, 547 | minValue: 0, 548 | maxValue: 100, 549 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 550 | }, 'Rain Probability' 551 | ) 552 | 553 | this.createCharacteristicClass( 554 | 'Snow', 'F14EB1AD-E000-4CE6-BD0E-384F9EC4D5DD', { 555 | format: this.Formats.BOOL, 556 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 557 | } 558 | ) 559 | 560 | this.createCharacteristicClass( 561 | 'UvIndex', '05BA0FE0-B848-4226-906D-5B64272E05CE', { 562 | format: this.Formats.UINT8, 563 | minValue: 0, 564 | maxValue: 10, 565 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 566 | }, 'UV Index' 567 | ) 568 | 569 | this.createCharacteristicClass( 570 | 'Visibility', 'D24ECC1E-6FAD-4FB5-8137-5AF88BD5E857', { 571 | format: this.Formats.UINT8, 572 | minValue: 0, 573 | maxValue: 100, 574 | unit: 'km', 575 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 576 | } 577 | ) 578 | 579 | this.createCharacteristicClass( 580 | 'WindDirection', '46F1284C-1912-421B-82F5-EB75008B167E', { 581 | format: this.Formats.STRING, 582 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 583 | }, 'Wind Direction' 584 | ) 585 | 586 | this.createCharacteristicClass( 587 | 'WindSpeed', '49C8AE5A-A3A5-41AB-BF1F-12D5654F9F41', { 588 | format: this.Formats.FLOAT, 589 | unit: 'm/s', 590 | minValue: 0, 591 | maxValue: 150, 592 | minStep: 0.1, 593 | perms: [this.Perms.PAIRED_READ, this.Perms.NOTIFY] 594 | }, 'Wind Speed' 595 | ) 596 | 597 | // ========================================================================= 598 | 599 | // The following custom services are defined by Eve. 600 | // These are listed in order of UUID. 601 | 602 | /** @member EveHomeKitTypes#Services 603 | * @property {Service} AirPressureSensor - Used by: Eve Degree. 604 | * @property {Service} ContactSensor - Used by: Eve Door. 605 | * @property {Service} History - Used for displaying history. 606 | * @property {Service} MotionSensor - Used by: Eve Motion. 607 | * @property {Service} Outlet - Used by: Eve Energy. 608 | * @property {Service} TemperatureSensor - Used by: Eve Degree, Eve Room. 609 | * @property {Service} Thermostat - Used by: Eve Thermo. 610 | * @property {Service} Weather - Used by: Eve Weather. 611 | */ 612 | 613 | this.createServiceClass('Weather', uuid('001'), [ 614 | this.Characteristics.CurrentTemperature 615 | ], [ 616 | this.hapCharacteristics.CurrentRelativeHumidity, 617 | this.Characteristics.AirPressure 618 | ]) 619 | 620 | this.createServiceClass('History', uuid('007'), [ 621 | this.Characteristics.HistoryStatus, // 116 622 | this.Characteristics.HistoryEntries, // 117 623 | this.Characteristics.HistoryRequest, // 11C 624 | this.Characteristics.SetTime // 121 625 | ], [ 626 | this.Characteristics.ResetTotal // 112 627 | // this.Characteristics.ConfigCommand, // 11D 628 | // this.Characteristics.ConfigData, // 131 629 | // this.Characteristics.Char11E, // presumably for firmware upgrade 630 | // this.Characteristics.Char158, // presumably for firmware upgrade 631 | ]) 632 | 633 | this.createServiceClass('Consumption', uuid('008'), [], [ 634 | this.Characteristics.Voltage, // 10A 635 | this.Characteristics.TotalConsumption, // 10C 636 | this.Characteristics.Consumption, // 10D 637 | this.Characteristics.ElectricCurrent, // 126 638 | this.hapCharacteristics.StatusFault, 639 | this.hapCharacteristics.LockPhysicalControls 640 | ]) 641 | 642 | this.createServiceClass('AirPressureSensor', uuid('00A'), [ 643 | this.Characteristics.AirPressure, 644 | this.Characteristics.Elevation 645 | ]) 646 | 647 | // ========================================================================= 648 | 649 | // The following services are used by Eve. 650 | // These are listed in alphabetical order. 651 | 652 | this.createServiceClass( 653 | 'ContactSensor', this.hapServices.ContactSensor.UUID, [ 654 | this.hapCharacteristics.ContactSensorState, 655 | this.Characteristics.TimesOpened, 656 | this.Characteristics.OpenDuration, 657 | this.Characteristics.ClosedDuration, 658 | this.Characteristics.LastActivation 659 | ] 660 | ) 661 | 662 | this.createServiceClass( 663 | 'MotionSensor', this.hapServices.MotionSensor.UUID, [ 664 | this.hapCharacteristics.MotionDetected, 665 | this.Characteristics.Sensitivity, 666 | this.Characteristics.Duration, 667 | this.Characteristics.LastActivation 668 | ] 669 | ) 670 | 671 | this.createServiceClass( 672 | 'Outlet', this.hapServices.Outlet.UUID, [ 673 | this.hapCharacteristics.On, 674 | this.hapCharacteristics.InUse, 675 | this.Characteristics.Voltage, 676 | this.Characteristics.ElectricCurrent, 677 | this.Characteristics.Consumption, 678 | this.Characteristics.TotalConsumption 679 | ] 680 | ) 681 | 682 | this.createServiceClass( 683 | 'TemperatureSensor', this.hapServices.TemperatureSensor.UUID, [ 684 | this.Characteristics.CurrentTemperature, 685 | this.hapCharacteristics.TemperatureDisplayUnits 686 | ] 687 | ) 688 | 689 | this.createServiceClass('Thermostat', this.hapServices.Thermostat.UUID, [ 690 | this.hapCharacteristics.CurrentHeatingCoolingState, 691 | this.hapCharacteristics.TargetHeatingCoolingState, 692 | this.Characteristics.CurrentTemperature, 693 | this.hapCharacteristics.TargetTemperature, 694 | this.hapCharacteristics.TemperatureDisplayUnits, 695 | this.Characteristics.ValvePosition 696 | ], [ 697 | this.Characteristics.ProgramCommand, 698 | this.Characteristics.ProgramData 699 | ]) 700 | } 701 | } 702 | 703 | export { EveHomeKitTypes } 704 | -------------------------------------------------------------------------------- /lib/HttpClient.js: -------------------------------------------------------------------------------- 1 | /** HTTP client. 2 | *
See {@link HttpClient}. 3 | * @name HttpClient 4 | * @type {Class} 5 | * @memberof module:homebridge-lib 6 | */ 7 | export { HttpClient } from 'hb-lib-tools/HttpClient' 8 | -------------------------------------------------------------------------------- /lib/JsonFormatter.js: -------------------------------------------------------------------------------- 1 | /** JSON formatter. 2 | *
See {@link JsonFormatter}. 3 | * @name JsonFormatter 4 | * @type {Class} 5 | * @memberof module:homebridge-lib 6 | */ 7 | export { JsonFormatter } from 'hb-lib-tools/JsonFormatter' 8 | -------------------------------------------------------------------------------- /lib/OptionParser.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/OptionParser.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Parser and validator for options and other parameters. 7 | *
See {@link OptionParser}. 8 | * @name OptionParser 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { OptionParser } from 'hb-lib-tools/OptionParser' 13 | -------------------------------------------------------------------------------- /lib/Platform.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/Platform.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { once } from 'node:events' 7 | import { writeFile, unlink } from 'node:fs/promises' 8 | import { Server, STATUS_CODES } from 'node:http' 9 | import { createRequire } from 'node:module' 10 | import { format, promisify } from 'node:util' 11 | import zlib from 'node:zlib' 12 | 13 | import { HttpClient } from 'hb-lib-tools/HttpClient' 14 | import { SystemInfo } from 'hb-lib-tools/SystemInfo' 15 | import { UpnpClient } from 'hb-lib-tools/UpnpClient' 16 | 17 | import { Delegate } from 'homebridge-lib/Delegate' 18 | import { EveHomeKitTypes } from 'homebridge-lib/EveHomeKitTypes' 19 | import { MyHomeKitTypes } from 'homebridge-lib/MyHomeKitTypes' 20 | import { semver } from 'homebridge-lib/semver' 21 | 22 | import { formatError, recommendedNodeVersion } from 'homebridge-lib' 23 | 24 | const require = createRequire(import.meta.url) 25 | const libPackageJson = require('../package.json') 26 | 27 | const uuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/ 28 | const gzip = promisify(zlib.gzip) 29 | 30 | const context = { 31 | libName: libPackageJson.name, 32 | libVersion: libPackageJson.version, 33 | nodeVersion: process.version.slice(1), 34 | recommendedNodeVersion: recommendedNodeVersion(libPackageJson), 35 | // recommendedHomebridgeVersion: 36 | // semver.minVersion(libPackageJson.engines.homebridge).toString(), 37 | saveInterval: 3600, 38 | checkInterval: 7 * 24 * 3600, 39 | driftDebugThreshold: 250, 40 | driftWarningThreshold: 2500 41 | } 42 | 43 | /** Homebridge dynamic platform plugin. 44 | *
See {@link Platform}. 45 | * @name Platform 46 | * @type {Class} 47 | * @memberof module:homebridge-lib 48 | */ 49 | 50 | /** Homebridge dynamic platform plugin. 51 | * 52 | * `Platform` provides the following features to a platform plugin: 53 | * - Check the versions of NodeJS and Homebridge; 54 | * - Check whether a newer version of the plugin has been published to the NPM 55 | * registry; 56 | * - Handle the administration of the HomeKit accessories exposed by 57 | * the plugin through Homebridge; 58 | * - Persist HomeKit accessories across Homebridge restarts; 59 | * - Support for device polling by providing a heartbeat; 60 | * - Support for UPnP device discovery; 61 | * - Support dynamic configuration through the Homebridge UI. 62 | * @abstract 63 | * @extends Delegate 64 | */ 65 | class Platform extends Delegate { 66 | /** Load the platform plugin. 67 | * 68 | * Called by Homebridge, through the plugin's `index.js`, when loading the 69 | * plugin from the plugin directory, typically `/usr/lib/node_modules`. 70 | * @static 71 | * @param {!API} homebridge - Homebridge 72 | * [API](https://github.com/nfarina/homebridge/blob/master/lib/api.js). 73 | * @param {!object} packageJson - The contents of the plugin's `package.json`. 74 | * @param {!string} platformName - The name of the platform plugin, as used 75 | * in Homebridge's `config.json`. 76 | * @param {!Platform} Platform - The constructor of the platform plugin. 77 | */ 78 | static loadPlatform (homebridge, packageJson, platformName, Platform) { 79 | if (context.homebridge == null) { 80 | context.homebridge = homebridge 81 | context.homebridgeVersion = homebridge.serverVersion 82 | context.PlatformAccessory = homebridge.platformAccessory 83 | context.recommendedHomebridgeVersion = 84 | semver.minVersion(libPackageJson.engines.homebridge).toString() 85 | 86 | const hap = { 87 | Services: {}, 88 | Characteristics: {} 89 | } 90 | Object.keys(homebridge.hap.Service).sort().filter((key) => { 91 | return uuid.test(homebridge.hap.Service[key].UUID) 92 | }).forEach((key) => { 93 | hap.Services[key] = homebridge.hap.Service[key] 94 | }) 95 | Object.keys(homebridge.hap.Characteristic).sort().filter((key) => { 96 | return uuid.test(homebridge.hap.Characteristic[key].UUID) 97 | }).forEach((key) => { 98 | hap.Characteristics[key] = homebridge.hap.Characteristic[key] 99 | }) 100 | const eve = new EveHomeKitTypes(homebridge) 101 | const my = new MyHomeKitTypes(homebridge) 102 | 103 | context.Accessory = Object.freeze({ 104 | Categories: Object.freeze(Object.assign({}, homebridge.hap.Categories)) 105 | }) 106 | context.Services = Object.freeze({ 107 | hap: Object.freeze(hap.Services), 108 | eve: Object.freeze(eve.Services), 109 | my: Object.freeze(my.Services) 110 | }) 111 | context.Characteristic = Object.freeze({ 112 | Access: Object.freeze(Object.assign({}, homebridge.hap.Access)), 113 | Formats: Object.freeze(Object.assign({}, homebridge.hap.Formats)), 114 | Perms: Object.freeze(Object.assign({}, homebridge.hap.Perms)), 115 | Units: Object.freeze(Object.assign({}, homebridge.hap.Units)) 116 | }) 117 | context.Characteristics = Object.freeze({ 118 | hap: Object.freeze(hap.Characteristics), 119 | eve: Object.freeze(eve.Characteristics), 120 | my: Object.freeze(my.Characteristics) 121 | }) 122 | } 123 | context[Platform.name] = { packageJson, platformName } 124 | // console.log( 125 | // '%s v%s, node v%s, homebridge v%s, %s v%s', 126 | // packageJson.name, packageJson.version, context.nodeVersion, 127 | // context.homebridgeVersion, context.libName, context.libVersion 128 | // ) 129 | homebridge.registerPlatform( 130 | packageJson.name, platformName, Platform, true 131 | ) 132 | } 133 | 134 | get Accessory () { return context.Accessory } 135 | 136 | get Services () { return context.Services } 137 | 138 | get Characteristic () { return context.Characteristic } 139 | 140 | get Characteristics () { return context.Characteristics } 141 | 142 | /** Content of the plugin's package.json file. 143 | * @type {object} 144 | * @readonly 145 | */ 146 | get packageJson () { return this._myContext.packageJson } 147 | 148 | /** Create a new instance of the platform plugin. 149 | * 150 | * Called by Homebridge when initialising the plugin from `config.json`. 151 | * Note that only one instance of a dynamic platform plugin can be created. 152 | * @param {!logger} log - Instance of Homebridge 153 | * [logger](https://github.com/nfarina/homebridge/blob/master/lib/logger.js) 154 | * for the plugin. 155 | * @param {?object} configJson - The contents of the platform object from 156 | * Homebridge's `config.json`, or `null` when the plugin isn't included 157 | * in config.json. 158 | * @param {!API} homebridge - Homebridge 159 | * [API](https://github.com/nfarina/homebridge/blob/master/lib/api.js). 160 | */ 161 | constructor (log, configJson, homebridge) { 162 | super() 163 | this._log = log 164 | this._configJson = configJson 165 | this._homebridge = homebridge 166 | this._myContext = context[this.constructor.name] 167 | this._platformName = this._myContext.platformName 168 | this._pluginName = this._myContext.packageJson.name 169 | this._pluginVersion = this._myContext.packageJson.version 170 | 171 | this._accessories = {} 172 | this._accessoryDelegates = {} 173 | 174 | if (this._myContext.platform != null) { 175 | this.fatal( 176 | 'config.json: duplicate entry for %s platform', 177 | this._myContext.platformName 178 | ) 179 | } 180 | this._myContext.platform = this 181 | this._identify() 182 | 183 | this._homebridge 184 | .on('didFinishLaunching', this._main.bind(this)) 185 | .on('shutdown', this._shutdown.bind(this)) 186 | process.on('exit', this._exit.bind(this)) 187 | } 188 | 189 | // ===== Main ================================================================ 190 | 191 | // Main platform function. 192 | // Called by homebridge after restoring accessories from cache. 193 | async _main () { 194 | /** System information. 195 | * @type {SystemInfo} 196 | * @readonly 197 | */ 198 | this.systemInfo = new SystemInfo() 199 | this.systemInfo 200 | .on('error', (error) => { this.warn(error) }) 201 | .on('exec', (command) => { this.debug('exec: %s', command) }) 202 | .on('readFile', (filename) => { this.debug('read file: %s', filename) }) 203 | await this.systemInfo.init() 204 | this.log('hardware: %s', this.systemInfo.hwInfo.prettyName) 205 | this.log('os: %s', this.systemInfo.osInfo.prettyName) 206 | this._heartbeatStart = new Date() 207 | setTimeout(() => { this._beat(-1) }, 1000) 208 | this.on('exit', () => { this._flushCachedAccessories() }) 209 | 210 | const n = Object.keys(this._accessories).length 211 | if (n > 0) { 212 | this.log('restored %d accessories from cache', n) 213 | } 214 | if (this.listenerCount('upnpDeviceAlive') > 0) { 215 | this._upnpMonitor.listen() 216 | } 217 | if (this.listenerCount('upnpDeviceFound') > 0) { 218 | this._upnpMonitor.search() 219 | } 220 | if (typeof this.onUiRequest === 'function') { 221 | try { 222 | await this._createUiServer() 223 | } catch (error) { this.error(error) } 224 | } 225 | await once(this, 'initialised') 226 | this._flushCachedAccessories() 227 | for (const id in this._accessories) { 228 | if (this._accessoryDelegates[id] == null) { 229 | const accessory = this._accessories[id] 230 | this.log( 231 | '%s: remove stale %s%s accessory %s', accessory.context.name, 232 | accessory.context.className, 233 | accessory.context.version == null 234 | ? '' 235 | : ' v' + accessory.context.version, 236 | accessory.context.id 237 | ) 238 | this._removeAccessory(accessory) 239 | } 240 | } 241 | } 242 | 243 | /** Create a debug dump file. 244 | * 245 | * The dump file is a gzipped json file containing 246 | * - The hardware and software environment of the server running Homebridge; 247 | * - The versions of the plugin and of homebridge-lib; 248 | * - The contents of config.json; 249 | * - Any plugin-specific information. 250 | * 251 | * The file is created in the Homebridge user directory, and named after 252 | * the plugin. 253 | * @param {*} dumpInfo - Plugin-specific information. 254 | */ 255 | async createDumpFile (dumpInfo = {}) { 256 | const result = { 257 | hardware: this.systemInfo.hwInfo.prettyName, 258 | os: this.systemInfo.osInfo.prettyName, 259 | node: context.nodeVersion, 260 | homebridge: context.homebridgeVersion 261 | } 262 | result[this._pluginName] = this._pluginVersion 263 | result[context.libName] = context.libVersion 264 | result.configJson = this._configJson 265 | const filename = this._homebridge.user.storagePath() + '/' + 266 | this._pluginName + '.json.gz' 267 | try { 268 | const data = await gzip(JSON.stringify(Object.assign(result, dumpInfo))) 269 | await writeFile(filename, data) 270 | this.log('created debug dump file %s', filename) 271 | } catch (error) { 272 | this.error('%s: %s', filename, error) 273 | } 274 | } 275 | 276 | // Write `cachedAccessories` to disk. 277 | _flushCachedAccessories () { 278 | this.debug('flush cachedAccessories') 279 | this._homebridge.updatePlatformAccessories([]) 280 | } 281 | 282 | // Called every second. 283 | _beat (beat) { 284 | beat += 1 285 | const drift = new Date() - this._heartbeatStart - 1000 * (beat + 1) 286 | if (this._shuttingDown) { 287 | this.debug('last heartbeat %d, drift %d', beat, drift) 288 | return 289 | } 290 | if (drift < -context.driftDebugThreshold || drift > context.driftDebugThreshold) { 291 | if (drift < -context.driftWarningThreshold || drift > context.driftWarningThreshold) { 292 | this.warn('heartbeat %d, drift %d', beat, drift) 293 | } else { 294 | this.debug('heartbeat %d, drift %d', beat, drift) 295 | } 296 | } 297 | setTimeout(() => { 298 | this._beat(beat) 299 | }, 1000 - drift) 300 | 301 | if (beat % context.saveInterval === 30) { 302 | this._flushCachedAccessories() 303 | } 304 | 305 | if (beat % context.checkInterval === 0) { 306 | this._checkLatest(this._pluginName, this._pluginVersion) 307 | // this._checkLatest(context.libName, context.libVersion) 308 | } 309 | 310 | /** Emitted every second. 311 | * @event Platform#heartbeat 312 | * @param {number} beat - The sequence number of this heartbeat. 313 | */ 314 | this.emit('heartbeat', beat) 315 | for (const id in this._accessoryDelegates) { 316 | if (this._accessoryDelegates[id].heartbeatEnabled) { 317 | /** Emitted every second, when `heartbeatEnabled` has been set. 318 | * @event AccessoryDelegate#heartbeat 319 | * @param {number} beat - The sequence number of this heartbeat. 320 | */ 321 | this._accessoryDelegates[id].emit('heartbeat', beat) 322 | } 323 | } 324 | } 325 | 326 | // Called by homebridge when shutting down. 327 | _shutdown () { 328 | if (this._shuttingDown) { 329 | return 330 | } 331 | this._shuttingDown = true 332 | this.removeAllListeners('upnpDeviceAlive') 333 | this.removeAllListeners('upnpDeviceFound') 334 | if (this._ui?.abortController != null) { 335 | this._ui.abortController.abort() 336 | } 337 | for (const id in this._accessoryDelegates) { 338 | /** Emitted when Homebridge is shutting down. 339 | * 340 | * On receiving this event, the plugin should cleanup (close connections, 341 | * flush peristent storage, ...). 342 | * @event AccessoryDelegate#shutdown 343 | */ 344 | this._accessoryDelegates[id].emit('shutdown') 345 | } 346 | /** Emitted when Homebridge is shutting down. 347 | * 348 | * On receiving this event, the plugin should cleanup (close connections, 349 | * flush peristent storage, ...). 350 | * @event Platform#shutdown 351 | */ 352 | this.emit('shutdown') 353 | } 354 | 355 | // Called by NodeJS when process is exiting. 356 | _exit () { 357 | /** Emitted when Homebridge is exiting. 358 | * 359 | * Note: asynchronous calls made when handling this event will not be executed. 360 | * @event Platform#exit 361 | */ 362 | this.emit('exit') 363 | } 364 | 365 | // Issue an identity message. 366 | _identify () { 367 | this.log( 368 | '%s v%s, node v%s, homebridge v%s, %s v%s', 369 | this._pluginName, this._pluginVersion, context.nodeVersion, 370 | context.homebridgeVersion, context.libName, context.libVersion 371 | ) 372 | if (context.nodeVersion !== context.recommendedNodeVersion) { 373 | this.warn( 374 | 'recommended version: node v%s', context.recommendedNodeVersion 375 | ) 376 | } 377 | if (context.homebridgeVersion !== context.recommendedHomebridgeVersion) { 378 | this.warn( 379 | 'recommended version: homebridge v%s', 380 | context.recommendedHomebridgeVersion 381 | ) 382 | } 383 | const n = Object.keys(this._accessories).length 384 | if (n > 0) { 385 | this.log('exposing %d accessories', n) 386 | } 387 | this.debug('config.json: %j', this._configJson) 388 | } 389 | 390 | // Check the NPM registry for the latest version of this plugin. 391 | async _checkLatest (name, version) { 392 | try { 393 | if (this.npmRegistry == null) { 394 | this.npmRegistry = new HttpClient({ 395 | https: true, 396 | host: 'registry.npmjs.org', 397 | json: true, 398 | maxSockets: 1 399 | }) 400 | this.npmRegistry 401 | .on('error', (error) => { 402 | this.log( 403 | 'npm registry: request %d: %s %s', error.request.id, 404 | error.request.method, error.request.resource 405 | ) 406 | this.warn('npm registry: request %d: %s', error.request.id, error) 407 | }) 408 | .on('request', (request) => { 409 | this.debug( 410 | 'npm registry: request %d: %s %s', request.id, 411 | request.method, request.resource 412 | ) 413 | this.vdebug( 414 | 'npm registry: request %d: %s %s', request.id, 415 | request.method, request.url 416 | ) 417 | }) 418 | .on('response', (response) => { 419 | this.vdebug( 420 | 'npm registry: request %d: response: %j', response.request.id, 421 | response.body 422 | ) 423 | this.debug( 424 | 'npm registry: request %d: %d %s', response.request.id, 425 | response.statusCode, response.statusMessage 426 | ) 427 | }) 428 | } 429 | const { body } = await this.npmRegistry.get( 430 | '/' + name + '/latest', { Accept: 'application/json' }) 431 | if (body?.version != null) { 432 | if (body.version !== version) { 433 | this.warn('latest version: %s v%s', name, body.version) 434 | } else { 435 | this.debug('latest version: %s v%s', name, body.version) 436 | } 437 | } 438 | } catch (error) { 439 | if (error.request == null) { 440 | this.error(error) 441 | } 442 | } 443 | } 444 | 445 | // ===== Handle Accessories ================================================== 446 | 447 | /** Configure an accessory, after it has been restored from peristent 448 | * storage. 449 | * 450 | * Called by homebridge when restoring peristed accessories, typically from 451 | * `~/.homebridge/accessories/cachedAccessories`. 452 | * @method 453 | * @param {!PlatformAccessory} accessory - The restored Homebridge 454 | * [PlatformAccessory](https://github.com/nfarina/homebridge/blob/master/lib/platformAccessory.js). 455 | */ 456 | configureAccessory (accessory) { 457 | const className = accessory.context.className 458 | const version = accessory.context.version 459 | const id = accessory.context.id 460 | const name = accessory.context.name 461 | const context = accessory.context.context 462 | this.debug('%s: cached %s v%s %s', name, className, version, id) 463 | this.vdebug('%s: cached %s v%s %s: %j', name, className, version, id, context) 464 | this._accessories[id] = accessory 465 | // Fix homebridge overwrites firmware with package version. 466 | const uuid = this.Services.hap.AccessoryInformation.UUID 467 | if (accessory.context[uuid].firmware != null) { 468 | accessory.getService(this.Services.hap.AccessoryInformation) 469 | .getCharacteristic(this.Characteristics.hap.FirmwareRevision) 470 | .updateValue(accessory.context[uuid].firmware) 471 | } 472 | /** Emitted when Homebridge has restored an accessory from peristed 473 | * storage. 474 | * 475 | * On receiving this event, the plugin should restore the accessory 476 | * delegate. 477 | * @event Platform#accessoryRestored 478 | * @param {!string} className - The name of the 479 | * {@link AccessoryDelegate#className class} of the accessory delegate. 480 | * @param {!string} version - The version of the plugin that stored the 481 | * cached accessory. 482 | * @param {!string} id - The accessory ID. 483 | * @param {!string} name - The accessory name. 484 | * @param {object} context - The accessory 485 | * {@link AccessoryDelegate#context context}. 486 | */ 487 | this.emit('accessoryRestored', className, version, id, name, context) 488 | } 489 | 490 | // Get or create accessory. 491 | _getAccessory (delegate, params) { 492 | const className = delegate.constructor.name 493 | const id = params.id 494 | const name = params.name 495 | let accessory = this._accessories[id] 496 | if (accessory == null) { 497 | const category = params.category 498 | this.debug('%s: create %s %s', name, className, id) 499 | const uuid = this._homebridge.hap.uuid.generate(params.id).toUpperCase() 500 | accessory = new context.PlatformAccessory(name, uuid, category) 501 | this._accessories[id] = accessory 502 | accessory.displayName = name 503 | accessory.context = { 504 | context: {} 505 | } 506 | delegate.once('initialised', () => { 507 | try { 508 | if (params.externalAccessory) { 509 | this._homebridge.publishExternalAccessories( 510 | this._pluginName, [accessory] 511 | ) 512 | } else { 513 | this._homebridge.registerPlatformAccessories( 514 | this._pluginName, this._platformName, [accessory] 515 | ) 516 | } 517 | } catch (error) { 518 | try { 519 | // Make sure the accessory won't be persisted, since it will fail 520 | // to be exposed again on restore, causing `configureAccessory()` 521 | // not to be called. 522 | this._homebridge.unregisterPlatformAccessories( 523 | this._pluginName, this._platformName, [accessory] 524 | ) 525 | } catch (error) {} 526 | /** Emitted when associated accessory could not be exposed. 527 | * @event AccessoryDelegate#exposeError 528 | * @param {Error} error - The error trying to expose the accessory. 529 | */ 530 | delegate.emit('exposeError', error) 531 | } 532 | }) 533 | } 534 | this._accessoryDelegates[id] = delegate 535 | return accessory 536 | } 537 | 538 | // Remove accessory. 539 | _removeAccessory (accessory) { 540 | const className = accessory.context.className 541 | const id = accessory.context.id 542 | const name = accessory.context.name 543 | // const context = accessory.context.context 544 | const historyFile = accessory.context.historyFile 545 | if (historyFile) { 546 | this.debug('remove history file %s', historyFile) 547 | unlink(historyFile, (error) => { 548 | if (error) { 549 | this.warn(error) 550 | } 551 | }) 552 | } 553 | this.debug('%s: remove %s %s', name, className, id) 554 | try { 555 | this._homebridge.unregisterPlatformAccessories( 556 | this._pluginName, this._platformName, [accessory] 557 | ) 558 | } catch (error) {} 559 | delete this._accessoryDelegates[id] 560 | delete this._accessories[id] 561 | } 562 | 563 | // ===== UPnP Device Discovery =============================================== 564 | 565 | /** Configure UPnP discovery. 566 | * 567 | * @param {!object} config - ... 568 | * @param {?string} config.class - Filter on UPnP device class. 569 | * Default `upnp:rootdevice`. Use `ssdp:all` for all device classes. 570 | * @param {?string} config.host - UPnP address and port. 571 | * Default: `239.255.255.250:1900`. 572 | * @param {function} config.filter - Filter on UPnP message content. 573 | * The function takes the message as argument and returns a boolean. 574 | * Default: `(message) => { return true }`, return all messages. 575 | * @param {integer} config.timeout - Timeout (in seconds) for UPnP search. 576 | * Default: `5`. 577 | */ 578 | upnpConfig (config) { 579 | if (this._upnpMonitor != null) { 580 | throw new SyntaxError('upnpConfig(): already called') 581 | } 582 | this._upnpMonitor = new UpnpClient(config) 583 | this._upnpMonitor 584 | .on('error', (error) => { 585 | this.error('upnp: error') 586 | this.error(error) 587 | }) 588 | .on('listening', (host) => { 589 | this.debug('upnp: listening on %s', host) 590 | }) 591 | .on('searching', (host) => { 592 | this.debug('upnp: searching on %s', host) 593 | }) 594 | .on('searchDone', () => { 595 | this.debug('upnp: search done') 596 | }) 597 | .on('deviceAlive', (address, message) => { 598 | // this.debug('upnp: device %s is alive: %j', address, message) 599 | /** Emitted when a UPnP device sends an alive message. 600 | * @event Platform#upnpDeviceAlive 601 | * @param {string} address - The device's IP address. 602 | * @param {object} message - The contents of the alive message. 603 | */ 604 | this.emit('upnpDeviceAlive', address, message) 605 | }) 606 | .on('deviceFound', (address, message) => { 607 | // this.debug('upnp: found device %s: %j', address, message) 608 | /** Emitted when a UPnP device responds to a search request. 609 | * @event Platform#upnpDeviceFound 610 | * @param {string} address - The device's IP address. 611 | * @param {object} message - The contents of the search response message. 612 | */ 613 | this.emit('upnpDeviceFound', address, message) 614 | }) 615 | } 616 | 617 | // ===== Dynamic Configuration through Homebridge UI ========================= 618 | 619 | /** Handler for requests from the Homebridge Plugin UI Server. 620 | * @function Platform#onUiRequest 621 | * @async 622 | * @abstract 623 | * @param {string} method - The request method. 624 | * @param {string} resource - The request resource. 625 | * @param {*} body - The request body. 626 | * @returns {*} - The response body. 627 | */ 628 | 629 | // Create HTTP server for Homebridge Plugin UI Settings. 630 | async _createUiServer () { 631 | this._ui = {} 632 | this._ui.server = new Server() 633 | this._ui.server 634 | .on('listening', () => { 635 | this._ui.port = this._ui.server.address().port 636 | this.log('ui server: listening on http://127.0.0.1:%d/', this._ui.port) 637 | for (const id in this._accessoryDelegates) { 638 | this._accessoryDelegates[id].values.uiPort = this._ui.port 639 | } 640 | }) 641 | .on('error', (error) => { this.error(error) }) 642 | .on('close', () => { 643 | this.debug('ui server: closed port %d', this._ui.port) 644 | }) 645 | .on('request', async (request, response) => { 646 | let buffer = '' 647 | request.on('data', (data) => { buffer += data }) 648 | request.on('end', async () => { 649 | try { 650 | if (buffer !== '') { 651 | try { 652 | request.body = JSON.parse(buffer) 653 | } catch (error) { 654 | this.log( 655 | 'ui request %s: %s %s %s', ++this._ui.requestId, 656 | request.method, request.url, buffer 657 | ) 658 | this.warn('ui request %d: %s', this._ui.requestId, error.message) 659 | response.writeHead(400) // Bad Request 660 | response.end() 661 | return 662 | } 663 | this.debug( 664 | 'ui request %s: %s %s %j', ++this._ui.requestId, 665 | request.method, request.url, request.body 666 | ) 667 | } else { 668 | this.debug( 669 | 'ui request %s: %s %s', ++this._ui.requestId, 670 | request.method, request.url 671 | ) 672 | } 673 | const { status, body } = 674 | request.method === 'GET' && request.url === '/ping' 675 | ? { status: 200, body: 'pong' } 676 | : await this.onUiRequest( 677 | request.method, request.url, request.body 678 | ) 679 | this.debug( 680 | 'ui request %d: %d %s', this._ui.requestId, 681 | status, STATUS_CODES[status] 682 | ) 683 | if (status === 200) { 684 | this.vdebug('ui request %d: response: %j', this._ui.requestId, body) 685 | response.writeHead(status, { 'Content-Type': 'application/json' }) 686 | response.end(JSON.stringify(body)) 687 | } else { 688 | response.writeHead(status) 689 | response.end() 690 | } 691 | } catch (error) { 692 | this.warn('ui request %d: %s', this._ui.requestId, error) 693 | response.writeHead(500) // Internal Server Error 694 | response.end() 695 | } 696 | }) 697 | }) 698 | this._ui.abortController = new AbortController() // eslint-disable-line no-undef 699 | this._ui.requestId = 0 700 | this._ui.server.listen({ 701 | port: 0, 702 | host: '127.0.0.1', 703 | signal: this._ui.abortController.signal 704 | }) 705 | await once(this._ui.server, 'listening') 706 | } 707 | 708 | // ===== Logging ============================================================= 709 | 710 | // Do the heavy lifting for debug(), error(), fatal(), log(), and warn(), 711 | // taking into account errors vs exceptions. 712 | _message (level, logLevel, namePrefix, ...args) { 713 | let message 714 | 715 | // If last argument is Error convert it to string. 716 | if (args.length > 0) { 717 | let lastArg = args.pop() 718 | if (lastArg instanceof Error) { 719 | lastArg = formatError(lastArg, true) 720 | } 721 | args.push(lastArg) 722 | } 723 | 724 | // Format message. 725 | if (args[0] == null) { 726 | message = '' 727 | } else if (typeof (args[0]) === 'string') { 728 | message = format(...args) 729 | } else { 730 | throw new TypeError('format: not a string or instance of Error') 731 | } 732 | 733 | // Output message using homebridge's log function. 734 | switch (level) { 735 | case 'debug': 736 | case 'vdebug': 737 | case 'vvdebug': 738 | if (logLevel >= { debug: 2, vdebug: 3, vvdebug: 4 }[level]) { 739 | message = namePrefix + message 740 | this._log.debug(message) 741 | } 742 | break 743 | case 'log': 744 | if (logLevel >= 1) { 745 | message = namePrefix + message 746 | this._log(message) 747 | } 748 | break 749 | case 'warning': 750 | message = namePrefix + 'warning: ' + message 751 | this._log.warn(message) 752 | break 753 | default: 754 | message = namePrefix + level + ': ' + message 755 | this._log.error(message) 756 | break 757 | } 758 | } 759 | } 760 | 761 | export { Platform } 762 | -------------------------------------------------------------------------------- /lib/PropertyDelegate.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/PropertyDelegate.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate' 7 | import { Delegate } from 'homebridge-lib/Delegate' 8 | 9 | /** Delegate of a property of a HomeKit accessory or service. 10 | *
See {@link PropertyDelegate}. 11 | * @name PropertyDelegate 12 | * @type {Class} 13 | * @memberof module:homebridge-lib 14 | */ 15 | 16 | /** Delegate of a property of a delegate of a HomeKit accessory or HomeKit 17 | * service. 18 | * 19 | * A property delegate manages a property value that: 20 | * - Is persisted across homebridge restarts; 21 | * - Can be monitored through homebridge's log output; 22 | * - Can be monitored programmatically, through 23 | * {@link PropertyDelegate#event:didSet didSet} events. 24 | * 25 | * @extends Delegate 26 | */ 27 | class PropertyDelegate extends Delegate { 28 | /** Instantiate a property delegate. 29 | * 30 | * Note that instances are normally created by invoking 31 | * {@link AccessoryDelegate#addPropertyDelegate addPropertyDelegate()}. 32 | * @param {!AccessoryDelegate|ServiceDelegate} delegate - Reference to the 33 | * delegate of the corresponding HomeKit accessory or service. 34 | * @param {!object} params - Parameters of the property delegate. 35 | * @param {!string} params.key - The key for the property delegate.
36 | * Needs to be unique with parent delegate. 37 | * @param {?boolean} params.silent - Suppress set log messages. 38 | // * @param {!type} params.type - The type of the property value. 39 | * @param {?*} params.value - The initial value of the property.
40 | * Only used when the property delegate is created for the first time. 41 | * Otherwise, the value is restored from persistent storage. 42 | * @param {?string} params.unit - The unit of the value of the property. 43 | * @throws {TypeError} When a parameter has an invalid type. 44 | * @throws {RangeError} When a parameter has an invalid value. 45 | * @throws {SyntaxError} When a mandatory parameter is missing or an 46 | * optional parameter is not applicable. 47 | */ 48 | constructor (parent, params = {}) { 49 | if (!(parent instanceof AccessoryDelegate)) { 50 | throw new TypeError('parent: not an AccessoryDelegate') 51 | } 52 | super(parent.platform, parent.name + ': ' + params.key) 53 | if (typeof params.key !== 'string') { 54 | throw new TypeError('params.key: not a string') 55 | } 56 | this._parent = parent 57 | this._key = params.key 58 | this._log = params.silent ? this.debug : this.log 59 | // this._type = params.type 60 | this._unit = params.unit ?? '' 61 | 62 | // Set initial value. 63 | if (this.value == null && params.value != null) { 64 | this.value = params.value 65 | } 66 | } 67 | 68 | /** Destroy the propery delegate. 69 | * @params {boolean} [delegateOnly=false] - Destroy the delegate, but keep the 70 | * associated value in context. 71 | */ 72 | _destroy (delegateOnly = false) { 73 | this.vdebug('destroy') 74 | this.removeAllListeners() 75 | if (delegateOnly) { 76 | return 77 | } 78 | delete this._parent._context[this.key] 79 | } 80 | 81 | /** Current log level (of the associated accessory or service delegate). 82 | * 83 | * The log level determines what type of messages are printed: 84 | * 85 | * 0. Print error and warning messages. 86 | * 1. Print error, warning, and log messages. 87 | * 2. Print error, warning, log, and debug messages. 88 | * 3. Print error, warning, log, debug, and verbose debug messages. 89 | * 90 | * Note that debug messages (level 2 and 3) are only printed when 91 | * Homebridge was started with the `-D` or `--debug` command line option. 92 | * 93 | * @type {!integer} 94 | * @readonly 95 | */ 96 | get logLevel () { 97 | return this._parent.logLevel 98 | } 99 | 100 | get _namePrefix () { 101 | return this._parent._namePrefix + this._key + ': ' 102 | } 103 | 104 | validate (value) { 105 | // Todo: check value against type. 106 | return { value, s: '' } 107 | } 108 | 109 | /** Value of associated Characteristic. 110 | */ 111 | get value () { 112 | return this._parent._context[this._key] 113 | } 114 | 115 | set value (v) { 116 | const { value, s } = this.validate(v) 117 | 118 | // Check for actual change. 119 | if (value === this.value) { 120 | return 121 | } 122 | 123 | // Issue info message that property value has been set. 124 | if (this.value == null) { 125 | this._log('set to %j%s%s', value, this._unit, s) 126 | } else { 127 | this._log( 128 | 'set to %j%s%s (from %j%s)', value, this._unit, s, this.value, this._unit 129 | ) 130 | } 131 | 132 | // Update persisted value in ~/.homebridge/accessories/cachedAccessories. 133 | this._parent._context[this._key] = value 134 | 135 | /** Emitted when property value has changed. 136 | * @event PropertyDelegate#didSet 137 | * @param {*} value - The new property value. 138 | */ 139 | this.emit('didSet', value) 140 | } 141 | } 142 | 143 | export { PropertyDelegate } 144 | -------------------------------------------------------------------------------- /lib/ServiceDelegate.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/ServiceDelegate.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate' 7 | import { CharacteristicDelegate } from 'homebridge-lib/CharacteristicDelegate' 8 | import { Delegate } from 'homebridge-lib/Delegate' 9 | 10 | /** Delegate of a HomeKit service. 11 | *
See {@link ServiceDelegate}. 12 | * @name ServiceDelegate 13 | * @type {Class} 14 | * @memberof module:homebridge-lib 15 | */ 16 | 17 | /** Delegate of a HomeKit service. 18 | * 19 | * This delegate sets up a HomeKit service with the following HomeKit 20 | * characteristic: 21 | * 22 | * key | Characteristic 23 | * ---------------- | ----------------------------------- 24 | * `name` | `Characteristics.hap.Name` 25 | * `configuredName` | `Characteristics.hap.ConfiguredName` 26 | * @abstract 27 | * @extends Delegate 28 | */ 29 | class ServiceDelegate extends Delegate { 30 | /** Create a new instance of a HomeKit service delegate. 31 | * 32 | * When the associated HomeKit service was restored from persistent 33 | * storage, it is linked to the new delegate. Otherwise a new HomeKit 34 | * service will be created, using the values from `params`. 35 | * @param {!AccessoryDelegate} accessoryDelegate - Reference to the 36 | * associated HomeKit accessory delegate. 37 | * @param {!object} params - Properties of the HomeKit service.
38 | * Next to the fixed properties below, `params` also contains the value for 39 | * each key specified in {@link ServiceDelegate#characteristics characteristics}. 40 | * @param {!string} params.name - The (Siri) name of the service. 41 | * Also used to prefix log and error messages. 42 | * @param {!Service} params.Service - The type of the HomeKit service. 43 | * @param {?string} params.subtype - The subtype of the HomeKit service. 44 | * Needs to be specified when the accessory has multuple services of the 45 | * same type. 46 | * @params {?boolean} params.primaryService - This is the primary service 47 | * for the accessory. 48 | * @params {?ServiceDelegate} params.linkedServiceDelegate - The delegate 49 | * of the service this service links to. 50 | * @params {?boolean} params.hidden - Hidden service. 51 | */ 52 | constructor (accessoryDelegate, params = {}) { 53 | if (!(accessoryDelegate instanceof AccessoryDelegate)) { 54 | throw new TypeError('parent: not a AccessoryDelegate') 55 | } 56 | if (params.name == null) { 57 | throw new SyntaxError('params.name: missing') 58 | } 59 | super(accessoryDelegate.platform, params.name) 60 | if ( 61 | typeof params.Service !== 'function' || 62 | typeof params.Service.UUID !== 'string' 63 | ) { 64 | throw new TypeError('params.Service: not a Service') 65 | } 66 | this._accessoryDelegate = accessoryDelegate 67 | this._accessory = this._accessoryDelegate._accessory 68 | this._key = params.Service.UUID 69 | if (params.subtype != null) { 70 | this._key += '.' + params.subtype 71 | } 72 | 73 | // Get or create associated Service. 74 | this._service = params.subtype == null 75 | ? this._accessory.getService(params.Service) 76 | : this._accessory.getServiceById(params.Service, params.subtype) 77 | if (this._service == null) { 78 | this._service = this._accessory.addService( 79 | new params.Service(null, params.subtype) 80 | ) 81 | } 82 | this._accessoryDelegate._linkServiceDelegate(this) 83 | this._service.setPrimaryService(!!params.primaryService) 84 | if (params.linkedServiceDelegate != null) { 85 | params.linkedServiceDelegate._service.addLinkedService(this._service) 86 | } 87 | this._service.setHiddenService(!!params.hidden) 88 | 89 | // Setup persisted storage in ~/.homebridge/accessories/cachedAccessories. 90 | if (this._accessory.context[this._key] == null) { 91 | this._accessory.context[this._key] = {} 92 | } 93 | this._context = this._accessory.context[this._key] 94 | 95 | // Setup shortcut for characteristic values. 96 | this._values = {} // by key 97 | 98 | // Setup characteristics 99 | this._characteristicDelegates = {} // by key 100 | this._characteristics = {} // by uuid 101 | 102 | this.addCharacteristicDelegate({ 103 | key: 'name', 104 | Characteristic: this.Characteristics.hap.Name, 105 | value: params.name 106 | }) 107 | this.name = this.values.name 108 | if (this._service.constructor.name !== 'AccessoryInformation') { 109 | this.addCharacteristicDelegate({ 110 | key: 'configuredName', 111 | Characteristic: this.Characteristics.hap.ConfiguredName, 112 | props: { 113 | perms: [ 114 | this.Characteristic.Perms.PAIRED_READ, 115 | this.Characteristic.Perms.NOTIFY, 116 | this.Characteristic.Perms.PAIRED_WRITE, 117 | this.Characteristic.Perms.HIDDEN 118 | ] 119 | }, 120 | value: params.name, 121 | setter: (value) => { 122 | if (value == null || value === '') { 123 | throw new RangeError('cannot be empty') 124 | } 125 | } 126 | }).on('didSet', (value) => { 127 | this.name = value 128 | this.values.name = value 129 | if (params.primaryService) { 130 | accessoryDelegate.values.name = value 131 | } 132 | }) 133 | } 134 | 135 | this.once('initialised', () => { 136 | const staleCharacteristics = [] 137 | for (const uuid in this._service.characteristics) { 138 | const characteristic = this._service.characteristics[uuid] 139 | if (this._characteristics[characteristic.UUID] == null) { 140 | staleCharacteristics.push(characteristic) 141 | } 142 | } 143 | for (const characteristic of staleCharacteristics) { 144 | this.log('remove stale characteristic %s', characteristic.displayName) 145 | this._service.removeCharacteristic(characteristic) 146 | } 147 | const staleKeys = [] 148 | for (const key in this._context) { 149 | if (key !== 'context' && this._characteristicDelegates[key] == null) { 150 | staleKeys.push(key) 151 | } 152 | } 153 | for (const key of staleKeys) { 154 | this.log('remove stale value %s', key) 155 | delete this._context[key] 156 | } 157 | }) 158 | } 159 | 160 | /** Destroy service delegate and associated HomeKit service. 161 | * @params {boolean} [delegateOnly=false] - Destroy the delegate, but keep the 162 | * associated HomeKit service (including context). 163 | */ 164 | destroy (delegateOnly = false) { 165 | this.debug('destroy %s (%s)', this._key, this.constructor.name) 166 | this._accessoryDelegate._unlinkServiceDelegate(this, delegateOnly) 167 | this.removeAllListeners() 168 | for (const key in this._characteristicDelegates) { 169 | this._characteristicDelegates[key]._destroy(delegateOnly) 170 | } 171 | if (delegateOnly) { 172 | this._service.removeAllListeners() 173 | return 174 | } 175 | this._accessory.removeService(this._service) 176 | delete this._accessory.context[this._key] 177 | } 178 | 179 | /** Add a HomeKit characteristic delegate to the HomeKit service delegate. 180 | * 181 | * The characteristic delegate manages a value that: 182 | * - Is persisted across homebridge restarts; 183 | * - Can be monitored through homebridge's log output; 184 | * - Can be monitored programmatially through `didSet` events; and 185 | * - Mirrors the value of the optionally associated HomeKit characteristic. 186 | * 187 | * This value is accessed through {@link ServiceDelegate#values values}. 188 | * The delegate is returned, but can also be accessed through 189 | * {@link ServiceDelegate#characteristicDelegate characteristicDelegate()}. 190 | * 191 | * When the associated HomeKit characteristic was restored from persistent 192 | * storage, it is linked to the new delegate. Otherwise a new HomeKit 193 | * charactertistic will be created, using the values from `params`. 194 | * @param {!object} params - Properties of the HomeKit characteristic. 195 | * @param {!string} params.key - The key for the characteristic. 196 | * @param {?*} params.value - The initial value when the characteristic 197 | * is added. 198 | * @param {?boolean} params.silent - Suppress set log messages. 199 | * @param {?Characteristic} params.Characteristic - The type of the 200 | * characteristic, from {@link Delegate#Characteristic Characteristic}. 201 | * @param {?object} params.props - The properties of the HomeKit 202 | * characteristic.
203 | * Overrides the properties from the characteristic type. 204 | * @param {?string} params.unit - The unit of the value of the HomeKit 205 | * characteristic.
206 | * Overrides the unit from the characteristic type. 207 | * @param {?function} params.getter - Asynchronous function to be invoked 208 | * when HomeKit reads the characteristic value.
209 | * This must be an `async` function returning a `Promise` to the new 210 | * characteristic value. 211 | * @param {?function} params.setter - Asynchronous function to be invoked 212 | * when HomeKit writes the characteristic value.
213 | * This must be an `async` function returning a `Promise` that resolves 214 | * when the corresonding device value has been updated. 215 | * @returns {CharacteristicDelegate} 216 | * @throws {TypeError} When a parameter has an invalid type. 217 | * @throws {RangeError} When a parameter has an invalid value. 218 | * @throws {SyntaxError} When a mandatory parameter is missing or an 219 | * optional parameter is not applicable. 220 | */ 221 | addCharacteristicDelegate (params = {}) { 222 | if (typeof params.key !== 'string') { 223 | throw new TypeError(`params.key: ${params.key}: invalid key`) 224 | } 225 | if (params.key === '') { 226 | throw new RangeError(`params.key: ${params.key}: invalid key`) 227 | } 228 | const key = params.key 229 | if (this.values[key] !== undefined) { 230 | throw new SyntaxError(`${key}: duplicate key`) 231 | } 232 | 233 | const characteristicDelegate = new CharacteristicDelegate( 234 | this, params 235 | ) 236 | this._characteristicDelegates[key] = characteristicDelegate 237 | if (params.Characteristic != null) { 238 | this._characteristics[params.Characteristic.UUID] = true 239 | } 240 | 241 | // Create shortcut for characteristic value. 242 | Object.defineProperty(this.values, key, { 243 | configurable: true, // make sure we can delete it again 244 | writeable: true, 245 | get () { return characteristicDelegate.value }, 246 | set (value) { characteristicDelegate.value = value } 247 | }) 248 | 249 | return characteristicDelegate 250 | } 251 | 252 | removeCharacteristicDelegate (key) { 253 | delete this.values[key] 254 | const characteristicDelegate = this._characteristicDelegates[key] 255 | if (characteristicDelegate._characteristic != null) { 256 | const characteristic = characteristicDelegate._characteristic 257 | delete this._characteristics[characteristic.UUID] 258 | } 259 | characteristicDelegate._destroy() 260 | delete this._characteristicDelegates[key] 261 | delete this._context[key] 262 | } 263 | 264 | /** Values of the HomeKit characteristics for the HomeKit service. 265 | * 266 | * Contains the key of each characteristic added by 267 | * {@link ServiceDelegate#addCharacteristic addCharacteristic}. 268 | * When the value is written, the value of the corresponding HomeKit 269 | * characteristic is updated; when the characteristic value is changed from 270 | * HomeKit, this value is updated. 271 | * @type {object} 272 | */ 273 | get values () { 274 | return this._values 275 | } 276 | 277 | /** Returns the HomeKit characteristic delegate correspondig to the key. 278 | * @param {!string} key - The key for the characteristic. 279 | * returns {CharacteristicDelegate} 280 | */ 281 | characteristicDelegate (key) { 282 | return this._characteristicDelegates[key] 283 | } 284 | 285 | /** The corrresponding HomeKit accessory delegate. 286 | * @type {AccessoryDelegate} 287 | */ 288 | get accessoryDelegate () { 289 | return this._accessoryDelegate 290 | } 291 | 292 | /** Service context to be persisted across Homebridge restarts. 293 | * @type {object} 294 | */ 295 | get context () { 296 | if (this._context.context == null) { 297 | this._context.context = {} 298 | } 299 | return this._context.context 300 | } 301 | 302 | /** Current log level (of the associated accessory delegate). 303 | * 304 | * The log level determines what type of messages are printed: 305 | * 306 | * 0. Print error and warning messages. 307 | * 1. Print error, warning, and log messages. 308 | * 2. Print error, warning, log, and debug messages. 309 | * 3. Print error, warning, log, debug, and verbose debug messages. 310 | * 311 | * Note that debug messages (level 2 and 3) are only printed when 312 | * Homebridge was started with the `-D` or `--debug` command line option. 313 | * 314 | * @type {!integer} 315 | * @readonly 316 | */ 317 | get logLevel () { 318 | return this._accessoryDelegate.logLevel 319 | } 320 | } 321 | 322 | export { ServiceDelegate } 323 | -------------------------------------------------------------------------------- /lib/ServiceDelegate/AccessoryInformation.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/ServiceDelegate/AccessoryInformation.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 7 | 8 | /** Class for an _AccessoryInformation_ service delegate. 9 | * 10 | * This delegate sets up a `Services.hap.AccessoryInformation` HomeKit service 11 | * with the following HomeKit characteristics: 12 | * 13 | * key | Characteristic | isOptional 14 | * -------------- | -------------------------------------- | ---------- 15 | * `name` | `Characteristics.hap.Name` | 16 | * `id` | `Characteristics.hap.SerialNumber` | 17 | * `manufacturer` | `Characteristics.hap.Manufacturer` | 18 | * `model` | `Characteristics.hap.Model` | 19 | * `firmware` | `Characteristics.hap.FirmwareRevision` | 20 | * `hardware` | `Characteristics.hap.HardwareRevision` | Y 21 | * `software` | `Characteristics.hap.SoftwareRevision` | Y 22 | * @extends ServiceDelegate 23 | * @memberof ServiceDelegate 24 | */ 25 | class AccessoryInformation extends ServiceDelegate { 26 | /** Create a new instance of an _AccessoryInformation_ service delegate. 27 | * @param {!AccessoryDelegate} accessoryDelegate - The delegate of the 28 | * corresponding HomeKit accessory. 29 | * @param {!object} params - The parameters for the 30 | * _AccessoryInformation_ HomeKit service. 31 | * @param {!string} params.name - Initial value for 32 | * `Characteristics.hap.Name`. Also used to prefix log and error messages. 33 | * @param {!string} params.id - Initial value for 34 | * `Characteristics.hap.SerialNumber`. 35 | * @param {!string} params.manufacturer - Initial value for 36 | * `Characteristics.hap.Manufacturer`. 37 | * @param {!string} params.model - Initial value for 38 | * `Characteristics.hap.Model`. 39 | * @param {!string} params.firmware - Initial value for 40 | * `Characteristics.hap.FirmwareRevision`. 41 | * @param {?string} params.hardware - Initial value for 42 | * `Characteristics.hap.HardwareRevision`. 43 | * @param {?string} params.software - Initial value for 44 | * `Characteristics.hap.SoftwareRevision`. 45 | */ 46 | constructor (accessoryDelegate, params = {}) { 47 | params.name = accessoryDelegate.name 48 | params.Service = accessoryDelegate.Services.hap.AccessoryInformation 49 | super(accessoryDelegate, params) 50 | this.addCharacteristicDelegate({ 51 | key: 'id', 52 | Characteristic: this.Characteristics.hap.SerialNumber, 53 | value: params.id 54 | }) 55 | this.addCharacteristicDelegate({ 56 | key: 'identify', 57 | Characteristic: this.Characteristics.hap.Identify 58 | }) 59 | this.addCharacteristicDelegate({ 60 | key: 'manufacturer', 61 | Characteristic: this.Characteristics.hap.Manufacturer, 62 | value: params.manufacturer 63 | }) 64 | this.addCharacteristicDelegate({ 65 | key: 'model', 66 | Characteristic: this.Characteristics.hap.Model, 67 | value: params.model 68 | }) 69 | this.addCharacteristicDelegate({ 70 | key: 'firmware', 71 | Characteristic: this.Characteristics.hap.FirmwareRevision, 72 | value: params.firmware 73 | }) 74 | if (params.hardware != null || this._context.hardware != null) { 75 | this.addCharacteristicDelegate({ 76 | key: 'hardware', 77 | Characteristic: this.Characteristics.hap.HardwareRevision, 78 | value: params.hardware 79 | }) 80 | } 81 | if (params.software != null || this._context.software != null) { 82 | this.addCharacteristicDelegate({ 83 | key: 'software', 84 | Characteristic: this.Characteristics.hap.SoftwareRevision, 85 | value: params.software 86 | }) 87 | } 88 | 89 | accessoryDelegate.propertyDelegate('name') 90 | .on('didSet', (value) => { 91 | this.values.configuredName = value 92 | }) 93 | } 94 | 95 | addCharacteristicDelegate (params = {}) { 96 | const delegate = super.addCharacteristicDelegate(params) 97 | Object.defineProperty(this.accessoryDelegate.values, params.key, { 98 | configurable: true, // make sure we can delete it again 99 | writeable: true, 100 | get () { return delegate.value }, 101 | set (value) { delegate.value = value } 102 | }) 103 | return delegate 104 | } 105 | 106 | removeCharacteristicDelegate (key) { 107 | delete this.accessoryDelegate.values[key] 108 | super.removeCharacteristicDelegate(key) 109 | } 110 | } 111 | 112 | ServiceDelegate.AccessoryInformation = AccessoryInformation 113 | -------------------------------------------------------------------------------- /lib/ServiceDelegate/Battery.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/ServiceDelegate/Battery.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 7 | 8 | /** Class for a _Battery_ service delegate. 9 | * 10 | * This delegate sets up a `Services.hap.Battery` HomeKit service 11 | * with the following HomeKit characteristics: 12 | * 13 | * key | Characteristic | isOptional 14 | * ------------------ | -------------------------------------- | ---------- 15 | * `name` | `Characteristics.hap.Name` | 16 | * `batteryLevel` | `Characteristics.hap.BatteryLevel` | Y 17 | * `chargingState` | `Characteristics.hap.ChargingState` | Y 18 | * `statusLowBattery` | `Characteristics.hap.StatusLowBattery` | 19 | * `lowBatteryThreshold`| `Characteristics.my.LowBatteryThreshold` | Y 20 | * 21 | * Depending on the capabilities of the device, the `Battery` service should 22 | * be configured: 23 | * - With only `statusLowBattery`, for devices that only report status low 24 | * battery; 25 | * - With `statusLowBattery` and `batteryLevel`, for devices that report both 26 | * battery level and status low battery; 27 | * - With `statusLowBattery`, `batteryLevel`, and `lowBatteryThreshold`, for 28 | * devices that only report battery level. 29 | * 30 | * @extends ServiceDelegate 31 | * @memberof ServiceDelegate 32 | */ 33 | class Battery extends ServiceDelegate { 34 | /** Create a new instance of an _Battery_ service delegate. 35 | * @param {!AccessoryDelegate} accessoryDelegate - The delegate of the 36 | * corresponding HomeKit accessory. 37 | * @param {object} params - The parameters for the 38 | * _AccessoryInformation_ HomeKit service. 39 | * @param {?integer} [params.batteryLevel=100] - Initial value for 40 | * `Characteristics.hap.BatteryLevel`. 41 | *
When set to `undefined`, the `BatteryLevel` characteristic is not exposed. 42 | * @param {?integer} [params.chargingState=NOT_CHARGING] - Initial value for 43 | * `Characteristics.hap.ChargingState`. 44 | *
When set to `undefined`, the `ChargingState` characteristic is not exposed. 45 | * @param {integer} [params.statusLowBattery=BATTERY_LEVEL_NORMAL] - Initial 46 | * value for `Characteristics.hap.StatusLowBattery`. 47 | * @param {?integer} [params.lowBatteryThreshold=20] - Initial value for 48 | * low battery threshold. 49 | *
When set to `undefined`, the value of `StatusLowBattery` is not derived 50 | * from the value of `BatteryLevel`. 51 | */ 52 | constructor (accessoryDelegate, params = {}) { 53 | params.name = accessoryDelegate.name + ' Battery' 54 | params.Service = accessoryDelegate.Services.hap.Battery 55 | super(accessoryDelegate, params) 56 | if (params.batteryLevel !== undefined) { 57 | this.addCharacteristicDelegate({ 58 | key: 'batteryLevel', 59 | Characteristic: this.Characteristics.hap.BatteryLevel, 60 | unit: '%', 61 | value: params.batteryLevel ?? 100 62 | }).on('didSet', (value) => { 63 | this.updateStatusLowBattery() 64 | }) 65 | } 66 | if (params.chargingState !== undefined) { 67 | this.addCharacteristicDelegate({ 68 | key: 'chargingState', 69 | Characteristic: this.Characteristics.hap.ChargingState, 70 | value: params.chargingState ?? 71 | this.Characteristics.hap.ChargingState.NOT_CHARGING 72 | // this.Characteristics.hap.ChargingState.NOT_CHARGEABLE 73 | }) 74 | } 75 | this.addCharacteristicDelegate({ 76 | key: 'statusLowBattery', 77 | Characteristic: this.Characteristics.hap.StatusLowBattery, 78 | value: params.statusLowBattery ?? 79 | this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL 80 | }) 81 | if (params.lowBatteryThreshold !== undefined) { 82 | this.addCharacteristicDelegate({ 83 | key: 'lowBatteryThreshold', 84 | // Characteristic: this.Characteristics.my.LowBatteryThreshold, 85 | unit: '%', 86 | value: params.lowBatteryThreshold ?? 20 87 | }).on('didSet', (value) => { 88 | this.updateStatusLowBattery() 89 | }) 90 | } 91 | 92 | accessoryDelegate.propertyDelegate('name') 93 | .on('didSet', (value) => { 94 | this.values.configuredName = value + ' Battery' 95 | }) 96 | } 97 | 98 | updateStatusLowBattery () { 99 | this.values.statusLowBattery = 100 | this.values.batteryLevel <= this.values.lowBatteryThreshold 101 | ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW 102 | : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL 103 | } 104 | } 105 | 106 | ServiceDelegate.Battery = Battery 107 | -------------------------------------------------------------------------------- /lib/ServiceDelegate/Dummy.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/ServiceDelegate/Dummy.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 7 | 8 | /** Class for a delegate for a dummy _StatelessProgrammableSwitch_ service. 9 | * 10 | * This delegate sets up a dummy `Services.hap.StatelessProgrammableSwitch` 11 | * HomeKit service with the following HomeKit characteristics: 12 | * 13 | * key | Characteristic | isOptional 14 | * ------------------------- | --------------------------------------------- | ---------- 15 | * `name` | `Characteristics.hap.Name` | 16 | * `programmableSwitchEvent` | `Characteristics.hap.ProgrammableSwitchEvent` | 17 | * 18 | * Including the dummy service prevents Home from showing a _Not Supported_ 19 | * tile, and causes Home on iOS14 to show the _Favorite_ setting. 20 | * @extends ServiceDelegate 21 | * @memberof ServiceDelegate 22 | */ 23 | class Dummy extends ServiceDelegate { 24 | constructor (accessoryDelegate, params = {}) { 25 | params.name = accessoryDelegate.name 26 | params.Service = accessoryDelegate.Services.hap.StatelessProgrammableSwitch 27 | super(accessoryDelegate, params) 28 | 29 | this.addCharacteristicDelegate({ 30 | key: 'programmableSwitchEvent', 31 | Characteristic: this.Characteristics.hap.ProgrammableSwitchEvent, 32 | props: { 33 | minValue: this.Characteristics.hap.ProgrammableSwitchEvent.SINGLE_PRESS, 34 | maxValue: this.Characteristics.hap.ProgrammableSwitchEvent.SINGLE_PRESS 35 | } 36 | }) 37 | 38 | accessoryDelegate.propertyDelegate('name') 39 | .on('didSet', (value) => { 40 | this.values.configuredName = value 41 | }) 42 | } 43 | } 44 | 45 | ServiceDelegate.Dummy = Dummy 46 | -------------------------------------------------------------------------------- /lib/ServiceDelegate/History.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/ServiceDelegate/History.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | // 6 | // The logic for handling Eve history was inspired by Simone Tisa's 7 | // fakagato-history repository, copyright © 2017 simont77. 8 | // See https://github.com/simont77/fakegato-history. 9 | 10 | import { CharacteristicDelegate } from 'homebridge-lib/CharacteristicDelegate' 11 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 12 | 13 | // Eve history keeps time as # seconds since epoch of 2001/01/01. 14 | // @type {integer} 15 | const epoch = Math.round(new Date('2001-01-01T00:00:00Z').valueOf() / 1000) 16 | 17 | const defaultMemorySize = 6 * 24 * 7 * 4 // 4 weeks of 1 entry per 10 minutes 18 | 19 | // Convert date (in # seconds since NodeJS epoch) to string. 20 | // @param {integer} d - Seconds since NodeJS epoch. 21 | // @returns {string} Human readable date string. 22 | function dateToString (d) { 23 | return new Date(1000 * d).toString().slice(0, 24) 24 | } 25 | 26 | /** Abstract superclass for type of data point in Eve History. 27 | * 28 | * An Eve history service supports up to seven data points, corresponding to 29 | * an associated characteristic value. These data points are defined by tag and 30 | * length in the fingerprint reported through `eve.HistoryStatus`. 31 | * Different history entries can contain a different combination of data points. 32 | * @abstract 33 | * @memberof ServiceDelegate.History 34 | */ 35 | class HistoryValue { 36 | /** Create a new data point type. 37 | * @param {ServiceDelegate.History} parent - The delegate for the `eve.History` 38 | * service. 39 | * @param {CharactertisticDelegate} delegate - The delefate for the associated 40 | * characteristic. 41 | */ 42 | constructor (parent, delegate) { 43 | if (!(parent instanceof History)) { 44 | throw new TypeError('parent: not a ServiceDelegate.History') 45 | } 46 | if (!(delegate instanceof CharacteristicDelegate)) { 47 | throw new TypeError('delegate: not a CharacteristicDelegate') 48 | } 49 | this._parent = parent 50 | this._delegate = delegate 51 | } 52 | 53 | /** The 1-byte tag, identifying the data point to Eve. 54 | * @type {integer} 55 | */ 56 | get tag () { } 57 | 58 | /** The 1-byte length of a data point value. 59 | * @type {integer} 60 | */ 61 | get length () { return 2 } 62 | 63 | /** The 1-letter id, identifying a data point in the Eve history service delegate. 64 | * @type {string} 65 | */ 66 | get id () { } 67 | 68 | /** Add the data point to an entry 69 | * @params {Object} entry - The history entry. 70 | */ 71 | prepareEntry (entry) { entry[this.id] = this._delegate.value } 72 | 73 | /** Write the data point from an entry to a buffer. 74 | * @params {Object} entry - The history entry. 75 | * @params {Buffer} buffer - The buffer to write to. 76 | * @params {integer} offset - The offset within the buffer. 77 | */ 78 | writeEntry (entry, buffer, offset) { buffer.writeUInt16LE(entry[this.id], offset) } 79 | } 80 | 81 | class TemperatureValue extends HistoryValue { 82 | get tag () { return 0x01 } 83 | get id () { return 't' } 84 | prepareEntry (entry) { entry[this.id] = this._delegate.value * 100 } 85 | writeEntry (entry, buffer, offset) { buffer.writeInt16LE(entry[this.id], offset) } 86 | } 87 | 88 | class HumidityValue extends HistoryValue { 89 | get tag () { return 0x02 } 90 | get id () { return 'h' } 91 | prepareEntry (entry) { entry[this.id] = this._delegate.value * 100 } 92 | } 93 | 94 | class AirPressureValue extends HistoryValue { 95 | get tag () { return 0x03 } 96 | get id () { return 'a' } 97 | prepareEntry (entry) { entry[this.id] = this._delegate.value * 10 } 98 | } 99 | 100 | class VocLevelValue extends HistoryValue { 101 | get tag () { return 0x04 } 102 | get id () { return 'r' } 103 | } 104 | 105 | class ContactValue extends HistoryValue { 106 | get tag () { return 0x06 } 107 | get length () { return 1 } 108 | get id () { return 'c' } 109 | prepareEntry (entry) { entry[this.id] = this._delegate.value } 110 | writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) } 111 | 112 | constructor (parent, delegate) { 113 | super(parent, delegate) 114 | delegate.on('didSet', (value) => { 115 | const now = History.now() 116 | const entry = { time: now } 117 | entry[this.id] = this._delegate.value 118 | parent.addEntry(entry) 119 | parent.lastContactDelegate.value = parent.lastActivationValue(now) 120 | parent.timesOpenedDelegate.value += value 121 | }) 122 | parent.resetTotalHandlers.push((value) => { 123 | parent.timesOpenedDelegate.value = 0 124 | }) 125 | } 126 | } 127 | 128 | // Eve history entries for _Total Consumption_ actually contain the (average) power, 129 | // in 0.1 W, since the previous entry. Eve computes the displayed consumption in Wh. 130 | 131 | // Device delivers _Total Consumption_ in kWh. 132 | // We compute average power, and, optionally, update _Consumption_ (power). 133 | class TotalConsumptionValue extends HistoryValue { 134 | get tag () { return 0x07 } 135 | get id () { return 'p' } 136 | 137 | constructor (parent, delegate) { 138 | super(parent, delegate) 139 | parent.addCharacteristicDelegate({ 140 | key: 'consumption', 141 | unit: ' kWh', 142 | value: delegate.value, 143 | silent: true 144 | }) 145 | parent.addCharacteristicDelegate({ 146 | key: 'time', 147 | value: History.now(), 148 | silent: true 149 | }) 150 | } 151 | 152 | prepareEntry (entry) { 153 | if (this._delegate.value < this._parent.values.consumption) { 154 | // Total consumption has been reset 155 | this._parent.values.consumption = 0 156 | } 157 | const consumption = 158 | (this._delegate.value - this._parent.values.consumption) * 1000 * 3600 // Ws 159 | const period = entry.time - this._parent.values.time // s 160 | const power = consumption / period // W 161 | entry[this.id] = Math.round(power * 10) // 0.1 W 162 | if (this._parent.computedConsumptionDelegate != null) { 163 | this._parent.computedConsumptionDelegate.value = Math.round(power * 10) / 10 // W 164 | } 165 | this._parent.values.consumption = this._delegate.value 166 | this._parent.values.time = entry.time 167 | } 168 | } 169 | 170 | // Device delivers _Consumption_ (power) in W. 171 | // We compute _Total Consumption_ and average power, and update _Total Consumption_. 172 | class ConsumptionValue extends HistoryValue { 173 | get tag () { return 0x07 } 174 | get id () { return 'p' } 175 | 176 | constructor (parent, delegate) { 177 | super(parent, delegate) 178 | this._consumption = 0 // Ws 179 | this._period = 0 // s 180 | this._totalConsumption = parent.computedTotalConsumptionDelegate.value * 1000 * 3600 // Ws 181 | this._power = delegate.value // W 182 | this._time = History.now() 183 | delegate.on('didSet', (value) => { 184 | this.updateRunningTotals() 185 | this._power = value // W 186 | }) 187 | parent.resetTotalHandlers.push((value) => { 188 | this._totalConsumption = 0 // Ws 189 | parent.computedTotalConsumptionDelegate.value = 0 // kWh 190 | }) 191 | } 192 | 193 | prepareEntry (entry) { 194 | this.updateRunningTotals(entry.time) 195 | 196 | const power = this._period === 0 ? 0 : this._consumption / this._period // W 197 | entry[this.id] = Math.round(power * 10) // 0.1 W 198 | const totalConsumption = this._totalConsumption / (1000 * 3600) // kWh 199 | this._parent.computedTotalConsumptionDelegate.value = 200 | Math.round(totalConsumption * 100) / 100 // kWh 201 | 202 | this._consumption = 0 203 | this._period = 0 204 | } 205 | 206 | updateRunningTotals (now = History.now()) { 207 | const period = now - this._time // s 208 | const delta = this._power * period // Ws 209 | this._consumption += delta // Ws 210 | this._period += period // s 211 | this._totalConsumption += delta // Ws 212 | this._time = now 213 | } 214 | } 215 | 216 | // Device delivers (running) average _Consumption_ (power) in W. 217 | class AverageConsumptionValue extends HistoryValue { 218 | get tag () { return 0x07 } 219 | get id () { return 'p' } 220 | prepareEntry (entry) { entry[this.id] = this._delegate.value * 10 } 221 | } 222 | 223 | class OnValue extends HistoryValue { 224 | get tag () { return 0x0E } 225 | get length () { return 1 } 226 | get id () { return 'o' } 227 | prepareEntry (entry) { entry[this.id] = this._delegate.value ? 1 : 0 } 228 | writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) } 229 | 230 | constructor (parent, delegate) { 231 | super(parent, delegate) 232 | delegate.on('didSet', (value) => { 233 | const now = History.now() 234 | const entry = { time: now } 235 | entry[this.id] = this._delegate.value ? 1 : 0 236 | parent.addEntry(entry) 237 | parent.lastOnDelegate.value = parent.lastActivationValue(now) 238 | }) 239 | } 240 | } 241 | 242 | class ValvePositionValue extends HistoryValue { 243 | get tag () { return 0x10 } 244 | get length () { return 1 } 245 | get id () { return 'v' } 246 | writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) } 247 | } 248 | 249 | class TargetTemperatureValue extends HistoryValue { 250 | get tag () { return 0x11 } 251 | get id () { return 's' } 252 | prepareEntry (entry) { entry[this.id] = this._delegate.value * 100 } 253 | writeEntry (entry, buffer, offset) { buffer.writeInt16LE(entry[this.id], offset) } 254 | } 255 | 256 | class MotionValue extends HistoryValue { 257 | get tag () { return 0x1C } 258 | get length () { return 1 } 259 | get id () { return 'm' } 260 | prepareEntry (entry) { entry[this.id] = this._delegate.value ? 1 : 0 } 261 | writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) } 262 | 263 | constructor (parent, delegate) { 264 | super(parent, delegate) 265 | delegate.on('didSet', (value) => { 266 | const now = History.now() 267 | const entry = { time: now } 268 | entry[this.id] = this._delegate.value ? 1 : 0 269 | parent.addEntry(entry) 270 | parent.lastMotionDelegate.value = parent.lastActivationValue(now) 271 | }) 272 | } 273 | } 274 | 275 | class VocDensityValue extends HistoryValue { 276 | get tag () { return 0x22 } 277 | get id () { return 'q' } 278 | } 279 | 280 | class ButtonValue extends HistoryValue { 281 | get tag () { return 0x24 } 282 | get length () { return 1 } 283 | get id () { return 'b' } 284 | writeEntry (entry, buffer, offset) { buffer.writeInt8(entry[this.id], offset) } 285 | } 286 | 287 | class LightLevelValue extends HistoryValue { 288 | get tag () { return 0x30 } 289 | get id () { return 'l' } 290 | prepareEntry (entry) { entry[this.id] = Math.round(this._delegate.value) } 291 | } 292 | 293 | // Types of delegates for characteristics for history data points. 294 | const historyValueTypes = { 295 | temperature: TemperatureValue, 296 | humidity: HumidityValue, 297 | vocLevel: VocLevelValue, 298 | airPressure: AirPressureValue, 299 | contact: ContactValue, 300 | totalConsumption: TotalConsumptionValue, 301 | consumption: ConsumptionValue, 302 | averageConsumption: AverageConsumptionValue, 303 | on: OnValue, 304 | valvePosition: ValvePositionValue, 305 | targetTemperature: TargetTemperatureValue, 306 | motion: MotionValue, 307 | vocDensity: VocDensityValue, 308 | button: ButtonValue, 309 | lightLevel: LightLevelValue 310 | } 311 | 312 | // Types of delegates for characteristic maintained by Eve history. 313 | const historyDerivedTypes = { 314 | lastContact: 'contact', 315 | timesOpened: 'contact', 316 | lastMotion: 'motion', 317 | lastOn: 'on', 318 | lastLightOn: 'lightOn', 319 | computedConsumption: 'totalConsumption', 320 | computedTotalConsumption: 'consumption' 321 | } 322 | 323 | /** Class for an Eve _History_ service delegate. 324 | * 325 | * This delegate sets up a `Services.eve.History` HomeKit service 326 | * with keys for the following HomeKit characteristics: 327 | * 328 | * key | Characteristic 329 | * ---------------- | ---------------------------------- 330 | * `name` | `Characteristics.hap.Name` 331 | * `historyRequest` | `Characteristics.eve.HistoryRequest` 332 | * `setTime` | `Characteristics.eve.SetTime` 333 | * `historyStatus` | `Characteristics.eve.HistoryStatus` 334 | * `historyEntries` | `Characteristics.eve.HistoryEntries` 335 | * @abstract 336 | * @extends ServiceDelegate 337 | * @memberof ServiceDelegate 338 | */ 339 | class History extends ServiceDelegate { 340 | /** Create a new instance of an Eve _History_ service delegate. 341 | * @param {!AccessoryDelegate} accessoryDelegate - The delegate of the 342 | * corresponding HomeKit accessory. 343 | * @param {!object} params - The parameters for the _History_ HomeKit service. 344 | * @param {?CharacteristicDelegate} params.contactDelegate - The 345 | * `Characteristics.hap.ContactSensorState` characteristic delegate 346 | * for a `hap.ContactSensor` service. 347 | * @param {?CharacteristicDelegate} params.lastContactDelegate - The 348 | * `Characteristics.eve.LastActivation` characteristic delegate 349 | * for a `hap.ContactSensor` service. 350 | * @param {?CharacteristicDelegate} params.timesOpenedDelegate - The 351 | * `Characteristics.eve.TimesOpened` characteristic delegate 352 | * for a `hap.ContactSensor` service. 353 | * @param {?CharacteristicDelegate} params.motionDelegate - The 354 | * `Characteristics.hap.MotionDetected` characteristic delegate 355 | * for a `hap.MotionSensor` service. 356 | * @param {?CharacteristicDelegate} params.lastMotionDelegate - The 357 | * `Characteristics.eve.LastActivation` characteristic delegate 358 | * for a `hap.MotionSensor` service. 359 | * @param {?CharacteristicDelegate} params.lightLevelDelegate - The 360 | * `Characteristics.hap.CurrentAmbientLightLevel` characteristic delegate 361 | * for a `hap.LightLevelSensor` service. 362 | * @param {?CharacteristicDelegate} params.temperatureDelegate - The 363 | * `Characteristics.hap.CurrentTemperature` characteristic delegate 364 | * for a `hap.TemperatureSensor`, `eve.Weather`, or `hap.Thermostat` service. 365 | * @param {?CharacteristicDelegate} params.humidityDelegate - The 366 | * `Characteristics.hap.CurrentRelativeHumidity` characteristic delegate 367 | * for a `hap.HumiditySensor` or `eve.Weather` service. 368 | * @param {?CharacteristicDelegate} params.airPressureDelegate - The 369 | * `Characteristics.eve.AirPressure` characteristic delegate 370 | * for an `eve.AirPressureSensor` or `eve.Weather` service. 371 | * @param {?CharacteristicDelegate} params.vocDensityDelegate - The 372 | * `Characteristics.hap.VOCDensity` characteristic delegate 373 | * for a `hap.AirQualilitySensor` service. 374 | * @param {?CharacteristicDelegate} params.targetTemperatureDelegate - The 375 | * `Characteristics.hap.TargetTemperature` characteristic delegate 376 | * for a `hap.Thermostat` service. 377 | * @param {?CharacteristicDelegate} params.valvePositionDelegate - The 378 | * `Characteristics.eve.ValvePosition` characteristic delegate 379 | * for a `hap.Thermostat` service. 380 | * @param {?CharacteristicDelegate} params.onDelegate - The 381 | * `Characteristics.hap.On` characteristic delegate 382 | * for a `hap.Outlet` or a `hap.Switch` service. 383 | * @param {?CharacteristicDelegate} params.lastOnDelegate - The 384 | * `Characteristics.eve.LastActivation` characteristic delegate 385 | * for a `hap.Outlet` or a `hap.Switch` service. 386 | * @param {?CharacteristicDelegate} params.consumptionDelegate - The 387 | * `Characteristics.eve.Consumption` characteristic delegate 388 | * for a `hap.Outlet` or `eve.Consumption` service 389 | * for a device that reports power. 390 | * @param {?CharacteristicDelegate} params.computedTotalConsumptionDelegate - The 391 | * `Characteristics.eve.TotalConsumption` characteristic delegate 392 | * for a `hap.Outlet` or `eve.Consumption` service 393 | * for a device that reports power, but not total consumption. 394 | * @param {?CharacteristicDelegate} params.totalConsumptionDelegate - The 395 | * `Characteristics.eve.TotalConsumption` characteristic delegate 396 | * for a `hap.Outlet` or `eve.Consumption` service 397 | * for a device that reports total consumption. 398 | * @param {?CharacteristicDelegate} params.computedConsumptionDelegate - The 399 | * `Characteristics.eve.Consumption` characteristic delegate 400 | * for a `hap.Outlet` or `eve.Consumption` service 401 | * for a device that reports total consumption but not power. 402 | * @param {?CharacteristicDelegate} params.avarageConsumptionDelegate - The 403 | * `Characteristics.eve.Consumption` characteristic delegate 404 | * for a `hap.Outlet` or `eve.Consumption` service 405 | * for a device that reports runing average for power. 406 | * @param {?CharacteristicDelegate} params.lightOnDelegate - The 407 | * `Characteristics.hap.On` characteristic delegate 408 | * for a `hap.Lightbulb` service. 409 | * @param {?CharacteristicDelegate} params.lastLightOnDelegate - A 410 | * `Characteristics.eve.LastActivation` characteristic delegate 411 | * for a `hap.Lightbulb` service. 412 | * @param {integer} [params.memorySize=4032] - The memory size, in number of 413 | * history entries. The default is 4 weeks of 1 entry per 10 minutes. 414 | * @param {?boolean} params.config - Expose config. 415 | */ 416 | constructor (accessoryDelegate, params = {}) { 417 | params.name = accessoryDelegate.name + ' History' 418 | params.Service = accessoryDelegate.Services.eve.History 419 | params.hidden = true 420 | super(accessoryDelegate, params) 421 | this.resetTotalHandlers = [] 422 | 423 | const delegates = [] 424 | let i = 0 425 | for (const param of Object.keys(params)) { 426 | if (param.endsWith('Delegate')) { 427 | if (params[param] == null) { 428 | continue 429 | } 430 | if (!(params[param] instanceof CharacteristicDelegate)) { 431 | throw new TypeError(`params.${param}: not a CharacteristicDelegate`) 432 | } 433 | const key = param.slice(0, -8) 434 | if (historyDerivedTypes[key] != null) { 435 | if (params[historyDerivedTypes[key] + 'Delegate'] == null) { 436 | throw new SyntaxError( 437 | `params.${param}: missing params.${historyDerivedTypes[key]}Delegate` 438 | ) 439 | } 440 | this[param] = params[param] 441 | continue 442 | } 443 | if (key === 'lightOn') { 444 | if (!(params.lastLightOnDelegate instanceof CharacteristicDelegate)) { 445 | throw new SyntaxError(`params.${param}: missing params.lastLightOnDelegate`) 446 | } 447 | params[param].on('didSet', (value) => { 448 | const now = History.now() 449 | params.lastLightOnDelegate.value = this.lastActivationValue(now) 450 | this.addEntry({ time: now }) 451 | }) 452 | continue 453 | } 454 | if (historyValueTypes[key] == null) { 455 | throw new SyntaxError(`params.${param}: invalid parameter`) 456 | } 457 | if (++i > 7) { 458 | throw new SyntaxError(`params.${param}: more than 7 history values`) 459 | } 460 | delegates.push({ key, param }) 461 | } 462 | } 463 | const fingerPrint = Buffer.alloc(15) 464 | let offset = 0 465 | fingerPrint.writeUInt8(i, offset); offset++ 466 | this._valueTypes = [] 467 | for (const { key, param } of delegates) { 468 | const valueType = new historyValueTypes[key](this, params[param]) 469 | fingerPrint.writeUInt8(valueType.tag, offset); offset++ 470 | fingerPrint.writeUInt8(valueType.length, offset); offset++ 471 | this._valueTypes.push(valueType) 472 | } 473 | this.fingerPrint = fingerPrint.slice(0, offset) 474 | this.debug('fingerPrint: 0x%s', this.fingerPrint.toString('hex').toUpperCase()) 475 | const memorySize = i === 0 476 | ? 0 477 | : params.memorySize == null ? defaultMemorySize : params.memorySize 478 | 479 | this._transfer = false 480 | 481 | this.addCharacteristicDelegate({ 482 | key: 'history', 483 | silent: true 484 | }) 485 | if (this.values.history?.initialTime > History.now()) { 486 | this.warn('resetting history after time travel from %s', dateToString(this.values.history.initialTime)) 487 | this.values.history = null 488 | } 489 | this._h = this.values.history 490 | if (this._h == null) { 491 | this.values.history = { 492 | memorySize, 493 | firstEntry: 1, 494 | lastEntry: 1, 495 | entryOffset: 0, 496 | entries: [null, null], 497 | initialTime: History.now() 498 | } 499 | this._h = this.values.history 500 | } else if (this._h.memorySize !== memorySize) { 501 | this.values.history = { 502 | memorySize, 503 | firstEntry: this._h.lastEntry, 504 | lastEntry: this._h.lastEntry, 505 | entryOffset: this._h.lastEntry - 1, 506 | entries: [null, null], 507 | initialTime: this._h.initialTime 508 | } 509 | this._h = this.values.history 510 | } else { 511 | this.debug( 512 | 'restored %d history entries (%d to %d)', this._h.entries.length, 513 | this._h.firstEntry, this._h.lastEntry 514 | ) 515 | } 516 | 517 | this.addCharacteristicDelegate({ 518 | key: 'historyRequest', 519 | Characteristic: this.Characteristics.eve.HistoryRequest, 520 | value: params.historyRequest, 521 | setter: this._onSetHistoryRequest.bind(this), 522 | silent: true 523 | }) 524 | 525 | this.addCharacteristicDelegate({ 526 | key: 'setTime', 527 | Characteristic: this.Characteristics.eve.SetTime, 528 | value: params.setTime, 529 | silent: true 530 | }).on('didSet', (value) => { 531 | const buffer = Buffer.from(value, 'base64') 532 | this.vdebug('SetTime changed to %j', buffer.toString('hex')) 533 | const date = dateToString(buffer.readUInt32LE() + epoch) 534 | this.debug('SetTime changed to %s', date) 535 | }) 536 | 537 | this.addCharacteristicDelegate({ 538 | key: 'historyStatus', 539 | Characteristic: this.Characteristics.eve.HistoryStatus, 540 | value: params.historyStatus, 541 | silent: true 542 | }) 543 | 544 | this.addCharacteristicDelegate({ 545 | key: 'historyEntries', 546 | Characteristic: this.Characteristics.eve.HistoryEntries, 547 | value: params.historyEntries, 548 | getter: this._onGetEntries.bind(this), 549 | silent: true 550 | }) 551 | 552 | if (this.resetTotalHandlers.length > 0) { 553 | this.addCharacteristicDelegate({ 554 | key: 'resetTotal', 555 | Characteristic: this.Characteristics.eve.ResetTotal 556 | }).on('didSet', (value) => { 557 | for (const handler of this.resetTotalHandlers) { 558 | handler(value) 559 | } 560 | }) 561 | } 562 | 563 | if (params.config) { 564 | this.addCharacteristicDelegate({ 565 | key: 'configCommand', 566 | Characteristic: this.Characteristics.eve.ConfigCommand, 567 | setter: this._onSetConfig.bind(this) 568 | // silent: true 569 | }) 570 | 571 | this.addCharacteristicDelegate({ 572 | key: 'configData', 573 | Characteristic: this.Characteristics.eve.ConfigData, 574 | getter: this._onGetConfig.bind(this) 575 | // silent: true 576 | }) 577 | } 578 | 579 | accessoryDelegate.propertyDelegate('name') 580 | .on('didSet', (value) => { 581 | this.values.configuredName = value + ' History' 582 | }) 583 | 584 | this._accessoryDelegate.heartbeatEnabled = true 585 | this._accessoryDelegate 586 | .once('heartbeat', (beat) => { 587 | this._historyBeat = (beat % 600) + 5 588 | }) 589 | .on('heartbeat', (beat) => { 590 | if (beat % 600 === this._historyBeat) { 591 | const entry = { time: History.now() } 592 | for (const valueType of this._valueTypes) { 593 | valueType.prepareEntry(entry) 594 | } 595 | this.addEntry(entry) 596 | } 597 | }) 598 | .on('shutdown', () => { 599 | this.debug( 600 | 'saved %d history entries (%d to %d)', this._h.entries.length, 601 | this._h.firstEntry, this._h.lastEntry 602 | ) 603 | }) 604 | } 605 | 606 | addLastOnDelegate (onDelegate, lastOnDelegate) { 607 | if (!(onDelegate instanceof CharacteristicDelegate)) { 608 | throw new TypeError('onDelegate: not a CharacteristicDelegate') 609 | } 610 | if (!(lastOnDelegate instanceof CharacteristicDelegate)) { 611 | throw new TypeError('lastOnDelegate: not a CharacteristicDelegate') 612 | } 613 | onDelegate.on('didSet', (value) => { 614 | lastOnDelegate.value = this.lastActivationValue() 615 | }) 616 | } 617 | 618 | /** Return current time as # seconds since NodeJS epoch. 619 | * @returns {integer} # seconds since NodeJS epoch. 620 | */ 621 | static now () { return Math.round(new Date().valueOf() / 1000) } 622 | 623 | /** Convert date intp `Characteristics.eve.LastActivation` characteristic value. 624 | * @param {integer} date - Seconds since NodeJS epoch. 625 | * @returns {integer} Value for last activation. 626 | */ 627 | lastActivationValue (date = History.now()) { return date - this._h.initialTime } 628 | 629 | /** Convert a history entry to a buffer. 630 | * @abstract 631 | * @param {object} entry - The entry. 632 | * @returns {Buffer} A Buffer with the values from the entry. 633 | */ 634 | entryToBuffer (entry) { 635 | const buffer = Buffer.alloc(16) 636 | let bitmap = 0 637 | let offset = 1 638 | for (const i in this._valueTypes) { 639 | try { 640 | const valueType = this._valueTypes[i] 641 | if (entry[valueType.id] != null) { 642 | bitmap |= 0x01 << i 643 | valueType.writeEntry(entry, buffer, offset) 644 | offset += valueType.length 645 | } 646 | } catch (error) { this.warn(error) } 647 | } 648 | buffer.writeUInt8(bitmap, 0) 649 | return buffer.slice(0, offset) 650 | } 651 | 652 | /** Add an entry to the history. 653 | * @param {object} entry - The entry. 654 | */ 655 | addEntry (entry) { 656 | if (this._h.memorySize > 0) { 657 | if (this._h.lastEntry - this._h.entryOffset >= this._h.memorySize) { 658 | this._h.firstEntry++ 659 | } 660 | this._h.lastEntry++ 661 | const index = (this._h.lastEntry - this._h.entryOffset) % this._h.memorySize 662 | this.debug( 663 | 'History Entries: set entry %d (index %d) to %j', 664 | this._h.lastEntry, index, entry 665 | ) 666 | this._h.entries[index] = entry 667 | } 668 | 669 | this.debug('set History Status to %d .. %d', this._h.firstEntry, this._h.lastEntry) 670 | const buffer = Buffer.alloc(1024) 671 | let offset = 0 672 | buffer.writeUInt32LE(entry.time - this._h.initialTime, offset); offset += 4 673 | buffer.writeUInt32LE(0, offset); offset += 4 674 | buffer.writeUInt32LE(this._h.initialTime - epoch, offset); offset += 4 675 | this.fingerPrint.copy(buffer, offset); offset += this.fingerPrint.length 676 | buffer.writeUInt16LE(this._h.lastEntry - this._h.firstEntry + 1, offset); offset += 2 677 | buffer.writeUInt16LE(this._h.memorySize, offset); offset += 2 678 | buffer.writeUInt32LE(this._h.firstEntry, offset); offset += 4 679 | buffer.writeUInt32LE(0, offset); offset += 4 680 | buffer.writeUInt8(1, offset); offset += 1 681 | buffer.writeUInt8(1, offset); offset += 1 682 | const value = buffer.slice(0, offset) 683 | this.values.historyStatus = value.toString('base64') 684 | } 685 | 686 | async _onSetHistoryRequest (value) { 687 | const buffer = Buffer.from(value, 'base64') 688 | this.vdebug('History Request changed to %j', buffer.toString('hex')) 689 | const entry = buffer.readUInt32LE(2) 690 | this.debug( 691 | 'History Request changed to %d (%d to %d)', entry, 692 | this._h.firstEntry, this._h.lastEntry 693 | ) 694 | this._currentEntry = Math.max(this._h.firstEntry, entry) 695 | this._transfer = true 696 | } 697 | 698 | async _onGetEntries () { 699 | if (this._currentEntry > this._h.lastEntry || !this._transfer) { 700 | this.debug('History Entries: no entry') 701 | this.vdebug('History Entries: send data: 00') 702 | this._transfer = false 703 | return Buffer.from('00', 'hex').toString('base64') 704 | } 705 | 706 | const buffer = Buffer.alloc(1024) 707 | let offset = 0 708 | for (let i = 0; i < 11; i++) { 709 | const index = this._h.memorySize === 0 710 | ? 1 711 | : (this._currentEntry - this._h.entryOffset) % this._h.memorySize 712 | if (this._currentEntry === this._h.firstEntry) { 713 | this.debug( 714 | 'History Entries (%d/11): entry %d (index: %d): %j (%s)', 715 | i, this._currentEntry, index, { initialTime: this._h.initialTime - epoch }, 716 | dateToString(this._h.initialTime) 717 | ) 718 | buffer.writeUInt8(21, offset); offset += 1 719 | buffer.writeUInt32LE(this._currentEntry, offset); offset += 4 720 | buffer.write('0100000081', offset, 'hex'); offset += 5 721 | buffer.writeUInt32LE(this._h.initialTime - epoch, offset); offset += 4 722 | buffer.write('00000000000000', offset, 'hex'); offset += 7 723 | } else { 724 | const entry = this._h.entries[index] 725 | this.debug( 726 | 'History Entries (%d/11): entry %d (index: %d): %j (%s)', 727 | i, this._currentEntry, index, entry, dateToString(entry.time) 728 | ) 729 | const b = this.entryToBuffer(entry) 730 | buffer.writeUInt8(b.length + 9, offset); offset += 1 731 | buffer.writeUInt32LE(this._currentEntry, offset); offset += 4 732 | buffer.writeUInt32LE(entry.time - this._h.initialTime, offset); offset += 4 733 | b.copy(buffer, offset); offset += b.length 734 | } 735 | this._currentEntry++ 736 | if (this._currentEntry > this._h.lastEntry) { 737 | break 738 | } 739 | } 740 | const value = buffer.slice(0, offset) 741 | this.vdebug('History Entries: send data: %s', value.toString('hex')) 742 | return value.toString('base64') 743 | } 744 | 745 | async _onSetConfig (value) { 746 | const buffer = Buffer.from(value, 'base64') 747 | this.vdebug('Config Request changed to %j', buffer.toString('hex')) 748 | } 749 | 750 | async _onGetConfig () { 751 | return Buffer.from('D200', 'hex').toString('base64') 752 | } 753 | } 754 | 755 | ServiceDelegate.History = History 756 | -------------------------------------------------------------------------------- /lib/ServiceDelegate/ServiceLabel.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/ServiceDelegate/ServiceLabel.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2017-2025 Erik Baauw. All rights reserved. 5 | 6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate' 7 | 8 | /** Class for a _ServiceLabel_ service delegate. 9 | * 10 | * This delegate sets up a `Services.hap.ServiceLabel` HomeKit service 11 | * with the following HomeKit characteristics: 12 | * 13 | * key | Characteristic | isOptional 14 | * -------------- | ------------------------------------------- | ---------- 15 | * `name` | `Characteristics.hap.Name` | 16 | * `namespace` | `Characteristics.hap.ServiceLabelNamespace` | 17 | * @extends ServiceDelegate 18 | * @memberof ServiceDelegate 19 | */ 20 | class ServiceLabel extends ServiceDelegate { 21 | /** Create a new instance of an _ServiceLabel_ service delegate. 22 | * @param {!AccessoryDelegate} accessoryDelegate - The delegate of the 23 | * corresponding HomeKit accessory. 24 | * @param {!object} params - The parameters for the 25 | * _AccessoryInformation_ HomeKit service. 26 | * @param {!string} params.name - Initial value for 27 | * `Characteristics.hap.Name`. Also used to prefix log and error messages. 28 | * @param {!string} params.namespace - Initial value for 29 | * `Characteristics.hap.ServiceLabelNamespace`. 30 | */ 31 | constructor (accessoryDelegate, params = {}) { 32 | params.name = accessoryDelegate.name + ' Label' 33 | params.Service = accessoryDelegate.Services.hap.ServiceLabel 34 | super(accessoryDelegate, params) 35 | this.addCharacteristicDelegate({ 36 | key: 'namespace', 37 | Characteristic: this.Characteristics.hap.ServiceLabelNamespace, 38 | value: params.namespace 39 | }) 40 | 41 | accessoryDelegate.propertyDelegate('name') 42 | .on('didSet', (value) => { 43 | this.values.configuredName = value + ' Label' 44 | }) 45 | } 46 | } 47 | 48 | ServiceDelegate.ServiceLabel = ServiceLabel 49 | -------------------------------------------------------------------------------- /lib/SystemInfo.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/SystemInfo.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** System information. 7 | *
See {@link SystemInfo}. 8 | * @name SystemInfo 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { SystemInfo } from 'hb-lib-tools/SystemInfo' 13 | -------------------------------------------------------------------------------- /lib/UiServer.js: -------------------------------------------------------------------------------- 1 | // homebridge-deconz/homebridge-lib/UiServer.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2022-2025 Erik Baauw. All rights reserved. 5 | 6 | import { readFile } from 'node:fs/promises' 7 | import { join } from 'node:path' 8 | import { format } from 'node:util' 9 | 10 | // Somehow this causes Homebridge to crash with SIGTERM after ~9 seconds. 11 | import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils' 12 | 13 | import { formatError } from 'hb-lib-tools' 14 | import { chalk } from 'hb-lib-tools/chalk' 15 | import { HttpClient } from 'hb-lib-tools/HttpClient' 16 | 17 | /** Server for dynamic configuration settings through Homebridge UI. 18 | *
See {@link UiServer}. 19 | * @name UiServer 20 | * @type {Class} 21 | * @memberof module:homebridge-lib 22 | */ 23 | 24 | /** Server for handling Homebridge Plugin UI requests. 25 | * 26 | * See {@link https://github.com/homebridge/plugin-ui-utils plugin-ui-utils}. 27 | * 28 | * The Homebridge Plugin UI Server runs in a separate process, which is spawned 29 | * by the Homebridge UI when the plugin _Settings_ are opened. 30 | * It implements an {@link HttpClient} to connect to the HTTP server provided 31 | * by {@link Platform} to change plugin settings dynamically. 32 | * 33 | * `UiServer` implemensts the following requests, which are documented as events: 34 | * - {@link UiServer#event:get get} - Send a GET request to the plugin instance. 35 | * - {@link UiServer#event:put put} - Send a put request to the plugin instance. 36 | * - {@link UiServer#event:cachedAccessories cachedAccessories} - Get the 37 | * cachedAccessories for a (child) bridge instance. 38 | * @extends HomebridgePluginUiServer 39 | */ 40 | class UiServer extends HomebridgePluginUiServer { 41 | constructor () { 42 | super() 43 | this.clients = {} 44 | 45 | /** Do a GET request to the plugin instance. 46 | * @event UiServer#get 47 | * @type {object} 48 | * @property {integer} uiPort - The port of the plugin instance UI server. 49 | * @property {string} resource - The requested resource. 50 | * @returns {*} - The response body. 51 | */ 52 | this.onRequest('get', async (params) => { 53 | try { 54 | const { uiPort, path } = params 55 | const client = this._getClient(uiPort) 56 | const { body } = await client.get(path) 57 | return body 58 | } catch (error) { 59 | if (!(error instanceof HttpClient.HttpError)) { 60 | this.error(error) 61 | } 62 | } 63 | }) 64 | 65 | /** Do a PUT request to the plugin instance. 66 | * @event UiServer#put 67 | * @param {object} 68 | * @property {integer} uiPort - The port of the plugin instance UI server. 69 | * @property {string} resource - The requested resource. 70 | * @property {*} body - The body of the request. 71 | * @returns {HttpResponse} - The response. 72 | */ 73 | this.onRequest('put', async (params) => { 74 | try { 75 | const { uiPort, path, body } = params 76 | const client = this._getClient(uiPort) 77 | const response = await client.put(path, JSON.stringify(body)) 78 | return response 79 | } catch (error) { 80 | if (!(error instanceof HttpClient.HttpError)) { 81 | this.error(error) 82 | } 83 | } 84 | }) 85 | 86 | /** Get the cached accessories for a single (child) bridge instance. 87 | * 88 | * This endpoint is needed because `homebridge.getCachedAccessories()` from 89 | * `plugin-ui-utils` doesn't indicate to which child bridge an accessory 90 | * belongs. 91 | * @event UiServer#cachedAccessories 92 | * @type {object} 93 | * @property {?string} username - The virtual MAC address of the child 94 | * bridge. Use `null` for the main bridge. 95 | * @returns {Object} cachedAccessories - The cached accessories. 96 | */ 97 | this.onRequest('cachedAccessories', async (params) => { 98 | try { 99 | const { username } = params 100 | let fileName = 'cachedAccessories' 101 | if (username != null) { 102 | fileName += '.' + username.replace(/:/g, '').toUpperCase() 103 | } 104 | const fullFileName = join( 105 | this.homebridgeStoragePath, 106 | 'accessories', 107 | fileName 108 | ) 109 | const json = await readFile(fullFileName) 110 | const cachedAccessories = JSON.parse(json) 111 | return cachedAccessories 112 | } catch (error) { 113 | this.error(error) 114 | } 115 | return [] 116 | }) 117 | } 118 | 119 | _getClient (uiPort) { 120 | if (this.clients[uiPort] == null) { 121 | this.clients[uiPort] = new HttpClient({ 122 | host: 'localhost:' + uiPort, 123 | json: true, 124 | keepAlive: true 125 | }) 126 | this.clients[uiPort] 127 | .on('error', (error) => { 128 | this.warn('request %d: %s', error.request.id, error) 129 | }) 130 | .on('request', (request) => { 131 | if (request.body == null) { 132 | this.debug( 133 | 'request %d: %s %s', request.id, request.method, request.resource 134 | ) 135 | } else { 136 | this.debug( 137 | 'request %d: %s %s %j', request.id, 138 | request.method, request.resource, request.body 139 | ) 140 | } 141 | }) 142 | .on('response', (response) => { 143 | this.vdebug( 144 | 'request %d: response: %j', response.request.id, response.body 145 | ) 146 | this.debug( 147 | 'request %d: %s %s', response.request.id, 148 | response.statusCode, response.statusMessage 149 | ) 150 | }) 151 | } 152 | return this.clients[uiPort] 153 | } 154 | 155 | // ===== Logging ============================================================= 156 | 157 | /** Print debug message to stdout. 158 | * @param {string|Error} format - The printf-style message or an instance of 159 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 160 | * @param {...string} args - Arguments to the printf-style message. 161 | */ 162 | debug (format, ...args) { 163 | this._log({ chalk: chalk.grey }, format, ...args) 164 | } 165 | 166 | /** Print error message to stdout. 167 | * @param {string|Error} format - The printf-style message or an instance of 168 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 169 | * @param {...string} args - Arguments to the printf-style message. 170 | */ 171 | error (format, ...args) { 172 | this._log({ label: 'error', chalk: chalk.bold.red }, format, ...args) 173 | } 174 | 175 | /** Print log message to stdout. 176 | * @param {string|Error} format - The printf-style message or an instance of 177 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 178 | * @param {...string} args - Arguments to the printf-style message. 179 | */ 180 | log (format, ...args) { 181 | this._log({}, format, ...args) 182 | } 183 | 184 | /** Print verbose debug message to stdout. 185 | * @param {string|Error} format - The printf-style message or an instance of 186 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 187 | * @param {...string} args - Arguments to the printf-style message. 188 | */ 189 | vdebug (format, ...args) { 190 | this._log({ chalk: chalk.grey }, format, ...args) 191 | } 192 | 193 | /** Print very verbose debug message to stdout. 194 | * @param {string|Error} format - The printf-style message or an instance of 195 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 196 | * @param {...string} args - Arguments to the printf-style message. 197 | */ 198 | vvdebug (format, ...args) { 199 | this._log({ chalk: chalk.grey }, format, ...args) 200 | } 201 | 202 | /** Print warning message to stdout. 203 | * @param {string|Error} format - The printf-style message or an instance of 204 | * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). 205 | * @param {...string} args - Arguments to the printf-style message. 206 | */ 207 | warn (format, ...args) { 208 | this._log({ label: 'warning', chalk: chalk.yellow }, format, ...args) 209 | } 210 | 211 | // Do the heavy lifting for debug(), error(), fatal(), log(), and warn(), 212 | // taking into account the options, and errors vs exceptions. 213 | _log (params = {}, ...args) { 214 | const output = process.stdout 215 | let message = '' 216 | 217 | // If last argument is Error convert it to string. 218 | if (args.length > 0) { 219 | let lastArg = args.pop() 220 | if (lastArg instanceof Error) { 221 | lastArg = formatError(lastArg, true) 222 | } 223 | args.push(lastArg) 224 | } 225 | 226 | // Format message. 227 | if (args[0] == null) { 228 | message = '' 229 | } else if (typeof args[0] === 'string') { 230 | message = format(...args) 231 | } else { 232 | message = format('%o', ...args) 233 | } 234 | 235 | // Handle colours. 236 | if (params.chalk != null) { 237 | message = params.chalk(message) 238 | } 239 | 240 | output.write(message) 241 | } 242 | } 243 | 244 | export { UiServer } 245 | -------------------------------------------------------------------------------- /lib/UpnpClient.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/UpnpClient.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Universal Plug and Play client. 7 | *
See {@link UpnpClient}. 8 | * @name UpnpClient 9 | * @type {Class} 10 | * @memberof module:homebridge-lib 11 | */ 12 | export { UpnpClient } from 'hb-lib-tools/UpnpClient' 13 | -------------------------------------------------------------------------------- /lib/chalk.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/chalk.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Return the [`chalk`](https://github.com/chalk/chalk) module, 7 | * so plugins don't have to list this as a separate dependency. 8 | * @name chalk 9 | * @memberof module:homebridge-lib 10 | */ 11 | export { chalk } from 'hb-lib-tools/chalk' 12 | -------------------------------------------------------------------------------- /lib/semver.js: -------------------------------------------------------------------------------- 1 | // homebridge-lib/lib/semver.js 2 | // 3 | // Library for Homebridge plugins. 4 | // Copyright © 2020-2025 Erik Baauw. All rights reserved. 5 | 6 | /** Return the [`semver`](https://github.com/npm/node-semver) module, 7 | * so plugins don't have to list this as a separate dependency. 8 | * @name semver 9 | * @memberof module:homebridge-lib 10 | */ 11 | export { semver } from 'hb-lib-tools/semver' 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-lib", 3 | "description": "Library for homebridge plugins", 4 | "author": "Erik Baauw", 5 | "maintainers": [ 6 | "ebaauw" 7 | ], 8 | "license": "Apache-2.0", 9 | "version": "7.1.5", 10 | "keywords": [ 11 | "homekit", 12 | "homebridge" 13 | ], 14 | "type": "module", 15 | "main": "index.js", 16 | "exports": { 17 | ".": "./index.js", 18 | "./*": "./lib/*.js" 19 | }, 20 | "files": [ 21 | "index.js", 22 | "lib", 23 | "cli" 24 | ], 25 | "bin": { 26 | "hap": "cli/hap.js", 27 | "json": "cli/json.js", 28 | "sysinfo": "cli/sysinfo.js", 29 | "upnp": "cli/upnp.js" 30 | }, 31 | "engines": { 32 | "homebridge": "^1.9.0||^2.0.0-beta", 33 | "node": "22.15.0||^22||^20||^18" 34 | }, 35 | "dependencies": { 36 | "@homebridge/plugin-ui-utils": "~2.0.2", 37 | "hb-lib-tools": "~2.2.3" 38 | }, 39 | "scripts": { 40 | "prepare": "standard && rm -rf out && jsdoc -c jsdoc.json", 41 | "test": "standard" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/ebaauw/homebridge-lib.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/ebaauw/homebridge-lib/issues" 49 | }, 50 | "homepage": "https://github.com/ebaauw/homebridge-lib#readme" 51 | } 52 | --------------------------------------------------------------------------------