├── .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 | [](https://www.npmjs.com/package/homebridge-lib)
8 | [](https://www.npmjs.com/package/homebridge-lib)
9 | [](https://standardjs.com)
10 | [](https://github.com/ebaauw/homebridge-lib/issues)
11 | [](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 |
--------------------------------------------------------------------------------