├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── cli
├── deconz.js
├── otau.js
└── ui.js
├── config.schema.json
├── deconz.png
├── homebridge-ui
├── public
│ ├── homebridge-deconz.png
│ ├── index.html
│ ├── index.old.html
│ └── style.css
└── server.js
├── index.js
├── jsdoc.json
├── lib
├── Deconz
│ ├── Device.js
│ ├── Resource.js
│ └── index.js
├── DeconzAccessory
│ ├── AirPurifier.js
│ ├── Gateway.js
│ ├── Light.js
│ ├── Sensor.js
│ ├── Thermostat.js
│ ├── WarningDevice.js
│ ├── WindowCovering.js
│ └── index.js
├── DeconzPlatform.js
└── DeconzService
│ ├── AirPressure.js
│ ├── AirPurifier.js
│ ├── AirQuality.js
│ ├── Alarm.js
│ ├── AlarmSystem.js
│ ├── Battery.js
│ ├── Button.js
│ ├── CarbonMonoxide.js
│ ├── Consumption.js
│ ├── Contact.js
│ ├── Daylight.js
│ ├── Flag.js
│ ├── Gateway.js
│ ├── Humidity.js
│ ├── Label.js
│ ├── Leak.js
│ ├── Light.js
│ ├── LightLevel.js
│ ├── LightsResource.js
│ ├── Motion.js
│ ├── Outlet.js
│ ├── Power.js
│ ├── Schedule.js
│ ├── SensorsResource.js
│ ├── Smoke.js
│ ├── Status.js
│ ├── Switch.js
│ ├── Temperature.js
│ ├── Thermostat.js
│ ├── Valve.js
│ ├── WarningDevice.js
│ ├── WindowCovering.js
│ └── index.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 deCONZ
7 | [](https://www.npmjs.com/package/homebridge-deconz)
8 | [](https://www.npmjs.com/package/homebridge-deconz)
9 | [](https://discord.gg/zUhSZSNb4P)
10 | [](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
11 |
12 | [](https://github.com/ebaauw/homebridge-deconz/issues)
13 | [](https://github.com/ebaauw/homebridge-deconz/pulls)
14 | [](https://standardjs.com)
15 |
16 |
17 |
18 | ## Homebridge plugin for deCONZ
19 | Copyright © 2022-2025 Erik Baauw. All rights reserved.
20 |
21 | ### Introduction
22 | This [Homebridge](https://github.com/homebridge/homebridge) plugin exposes to Apple's [HomeKit](http://www.apple.com/ios/home/) ZigBee devices (lights, plugs, sensors, switches, ...) and virtual devices on a deCONZ gateway by dresden elektronik.
23 | Homebridge deCONZ communicates with deCONZ over its [REST API](https://dresden-elektronik.github.io/deconz-rest-doc/), provided by its [REST API plugin](https://github.com/dresden-elektronik/deconz-rest-plugin).
24 | It runs independently from the Phoscon web app, see [deCONZ for Dummies](https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/deCONZ-for-Dummies).
25 |
26 | Homebridge deCONZ is the successor of Homebridge Hue for exposing Zigbee devices connected to a deCONZ gateway.
27 | See [Future Development of Homebridge Hue](https://github.com/ebaauw/homebridge-hue/issues/1070) for more details.
28 |
29 | ### Prerequisites
30 | You need a deCONZ gateway to connect Homebridge deCONZ to your ZigBee devices (lights, plugs, sensors, switches, ...).
31 | For Zigbee communication, the deCONZ gateway requires a [ConBee II](https://phoscon.de/en/conbee2) or [Conbee](https://phoscon.de/en/conbee) USB stick, or a [RaspBee II](https://phoscon.de/en/raspbee2) or [RaspBee](https://phoscon.de/en/raspbee) Raspberry Pi shield.
32 | I recommend to run deCONZ with its GUI enabled, even on a headless system.
33 | When needed, you can access the deCONZ GUI over screen sharing.
34 |
35 | You need a server to run Homebridge.
36 | This can be anything running [Node.js](https://nodejs.org): from a Raspberry Pi, a NAS system, or an always-on PC running Linux, macOS, or Windows.
37 | I strongly recommend to use a standard Homebridge installation, see the [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki) for details.
38 | I recommend to run deCONZ and Homebridge deCONZ on the same server, avoiding any network latency between deCONZ and Homebridge deCONZ, and preventing any potential network issues.
39 | I strongly recommend to run Homebridge deCONZ in a separate [child bridge](https://github.com/homebridge/homebridge/wiki/Child-Bridges).
40 |
41 | To interact with HomeKit, you need an Apple device with Siri or a HomeKit app.
42 | Please note that Siri and Apple's [Home](https://support.apple.com/en-us/HT204893) app only provide limited HomeKit support.
43 | To use the full features of Homebridge deCONZ, you need another HomeKit app, like [Eve](https://www.evehome.com/en/eve-app) (free) or Matthias Hochgatterer's [Home+](https://hochgatterer.me/home/) (paid).
44 | As HomeKit uses mDNS (formally known as Bonjour) to discover Homebridge, the server running Homebridge must be on the same subnet as your Apple devices running HomeKit.
45 | Most cases of _Not Responding_ accessories are due to mDNS issues.
46 | For remote access and for HomeKit automations (incl. support for wireless switches), you need to setup an Apple TV (4th generation or later), HomePod, or iPad as [home hub](https://support.apple.com/en-us/HT207057).
47 | I recommend to use the latest released non-beta version of the Apple device OS: iOS, iPadOS, macOS, ...
48 | HomeKit doesn't seem to like using different Apple device OS versions.
49 |
50 | ### Configuration
51 | Most settings for Homebridge deCONZ, can be changed at run-time, including which devices to expose, how to expose these, and the level of logging.
52 | This keeps `config.json` extremely simple.
53 | Typically, you only need to specify the hostname and port of the deCONZ gateway(s) in `config.json`.
54 | See [`Configuration`](https://github.com/ebaauw/homebridge-deconz/wiki/Configuration) in the Wiki for details.
55 | I strongly recommended to run Homebridge deCONZ in a separate [child bridge](https://github.com/homebridge/homebridge/wiki/Child-Bridges).
56 |
57 | Homebridge deCONZ provides a Configuration API to change the run-time settings.
58 | These changes take effect immediately, and are persisted across Homebridge restarts.
59 | See [`Dynamic Configuration`](https://github.com/ebaauw/homebridge-deconz/wiki/Dynamic-Configuration) in the Wiki for details.
60 | For now, these dynamic settings are managed through the `ui` command-line tool.
61 | Eventually, Homebridge deCONZ might provide a configuration user interface to the Homebridge UI, using this configuration API.
62 |
63 | When it connects to a deCONZ gateway for the first time, Homebridge deCONZ will try to obtain an API key for two minutes, before exposing the gateway accessory.
64 | Unless Homebridge deCONZ runs on the same server as the deCONZ gateway, you need to unlock the gateway to allow Homebridge deCONZ to obtain an API key.
65 | If you don't, Homebridge deCONZ will give up, after two minutes.
66 | In this case, you need to set `expose` on the gateway dynamic settings, to retry obtaining an API key.
67 | Homebridge deCONZ will **not** retry to obtain an API key on Homebridge restart.
68 |
69 | Once it has obtained an API key, Homebridge deCONZ will expose all Zigbee devices connected to the gateway, by default.
70 | Use the dynamic settings to exclude devices from being exposed, to change how devices are exposed, and to expose virtual devices like groups or CLIP sensors.
71 | Homebridge deCONZ exposes a [gateway accessory](https://github.com/ebaauw/homebridge-deconz/wiki/Gateway-Accessory) for each deCONZ gateway.
72 | In Apple's Home app, this accessory looks like a wireless switch; you'll need another HomeKit app to use the other features of this accessory.
73 |
74 | Note that HomeKit doesn't like configuration changes.
75 | After adding or removing accessories, allow ample time for HomeKit to sync the changed configuration to all Apple devices.
76 |
77 | ### Command-Line Utilities
78 | Homebridge deCONZ includes the following command-line utilities:
79 | - `deconz`, to discover, monitor, and interact with deCONZ gateways.
80 | See the [`deconz` Command-Line Utility](https://github.com/ebaauw/homebridge-deconz/wiki/deconz-Command%E2%80%90Line-Utility) in the Wiki for more info.
81 | - `otau`, to download and analyse over-the-air-update firmware files for Zigbee devices.
82 | - `ui` to configure a running instance of Homebridge deCONZ.
83 | See [`Dynamic Configuration`](https://github.com/ebaauw/homebridge-deconz/wiki/Dynamic-Configuration) in the Wiki for more info.
84 |
85 | Each command-line tool takes a `-h` or `--help` argument to provide a brief overview of its functionality and command-line arguments.
86 |
87 | ### Troubleshooting
88 | Please check the [FAQ](https://github.com/ebaauw/homebridge-hue/wiki/FAQ) (for now still on Homebridge Hue Wiki).
89 |
90 | #### Check Dependencies
91 | If you run into Homebridge startup issues, please double-check what versions of Node.js and of Homebridge have been installed.
92 | Homebridge deCONZ has been developed and tested using the [latest LTS](https://nodejs.org/en/about/releases/) version of Node.js and the [latest](https://www.npmjs.com/package/homebridge) version of Homebridge.
93 | Other versions might or might not work - I simply don't have the bandwidth to test these.
94 |
95 | #### Run Homebridge deCONZ Solo
96 | If you run into Homebridge startup issues, please run Homebridge deCONZ in a separate [child bridge](https://github.com/homebridge/homebridge/wiki/Child-Bridges).
97 |
98 | #### Debug Log File
99 | Homebridge deCONZ outputs an info message to the Homebridge log, for each HomeKit characteristic value it sets and for each HomeKit characteristic value change notification it receives. Make sure that `logLevel` of the corresponding accessory is at least 1, to see these info messages.
100 |
101 | Homebridge deCONZ outpits a debug message to the Homebridge log, for each interaction with a deCONZ gateway.
102 | Make sure to run Homebridge in DEBUG mode, and that `logLevel` of the corresponding accessory is at least 2, to see these debug messages. Set `logLevel` to 3 to log the payload of the interaction with deCONZ as well.
103 |
104 | #### Debug Dump File
105 | To aid troubleshooting, on startup, Homebridge deCONZ dumps its environment, including its `config.json` settings, dynamic settings, and the full state of all gateways into a compresed json file, `homebridge-deconz.json.gz`.
106 | This file is located in the Homebridge user directory.
107 |
108 | #### Getting help
109 | If you have a question about Homebridge deCONZ, please post a message to the **#deconz** channel of the Homebridge community on [Discord](https://discord.gg/zUhSZSNb4P).
110 |
111 | If you encounter a problem with Homebridge deCONZ, please open an issue on [GitHub](https://github.com/ebaauw/homebridge-deconz/issues).
112 | Please attach a copy of `homebridge-deconz.json.gz` to the issue, see [**Debug Dump File**](#debug-dump-file).
113 | Please attach a copy of the (compressed) Homebridge log file to the issue, see [**Debug Log File**](#debug-log-file).
114 | Please do **not** copy/paste large amounts of log output.
115 |
116 | ### Contributing
117 | Sometimes I get the question how people can support my work on Homebridge deCONZ.
118 | I created Homebridge deCONZ as a hobby project, for my own use.
119 | I share it on GitHub so others might benefit, and to give back to the open source community, without whom Homebridge Hue wouldn't have been possible.
120 |
121 | Having said that, adding support for new devices, in Homebridge deCONZ, and in the deCONZ REST API plugin, is very hard without having physical access to the device.
122 | Since this is a hobby project, I cannot afford to spend money on devices I won't be using myself, just to integrate them for the benefit of others.
123 | I am happy to receive small donations in the form of new devices to integrate, or the money to buy these devices myself.
124 | I am also happy to return the devices afterwards, if you provide the shipping costs.
125 | Please contact me by email or on Discord for shipping details.
126 |
127 | ### Caveats
128 | Homebridge deCONZ is a hobby project of mine, provided as-is, with no warranty whatsoever. I've been running it successfully at my home since May 2023, replacing Homebridge Hue, but your mileage might vary.
129 |
--------------------------------------------------------------------------------
/cli/deconz.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // deconz.js
4 | // Copyright © 2018-2025 Erik Baauw. All rights reserved.
5 | //
6 | // Command line interface to deCONZ gateway.
7 |
8 | import { createRequire } from 'node:module'
9 |
10 | import { DeconzTool } from 'hb-deconz-tools/DeconzTool'
11 |
12 | const require = createRequire(import.meta.url)
13 | const packageJson = require('../package.json')
14 |
15 | new DeconzTool(packageJson).main()
16 |
--------------------------------------------------------------------------------
/cli/otau.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // otau.js
4 | // Copyright © 2023-2025 Erik Baauw. All rights reserved.
5 | //
6 | // Command line interface to deCONZ gateway.
7 |
8 | import { createRequire } from 'node:module'
9 |
10 | import { OtauTool } from 'hb-deconz-tools/OtauTool'
11 |
12 | const require = createRequire(import.meta.url)
13 | const packageJson = require('../package.json')
14 |
15 | new OtauTool(packageJson).main()
16 |
--------------------------------------------------------------------------------
/config.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "pluginAlias": "deCONZ",
3 | "pluginType": "platform",
4 | "singular": true,
5 | "customUi": false,
6 | "headerDisplay": "Homebridge plugin for deCONZ",
7 | "footerDisplay": "For a detailed description, see the [wiki](https://github.com/ebaauw/homebridge-deconz/wiki/Configuration).",
8 | "schema": {
9 | "type": "object",
10 | "properties": {
11 | "name": {
12 | "description": "Plugin name as displayed in the Homebridge log.",
13 | "type": "string",
14 | "required": true,
15 | "default": "deCONZ"
16 | },
17 | "forceHttp": {
18 | "description": "Use plain http instead of https.",
19 | "type": "boolean"
20 | },
21 | "hosts": {
22 | "title": "Gateways",
23 | "type": "array",
24 | "items": {
25 | "type": "string"
26 | }
27 | },
28 | "noResponse": {
29 | "description": "Report unreachable lights as No Response in HomeKit.",
30 | "type": "boolean"
31 | },
32 | "parallelRequests": {
33 | "description": "The number of ansynchronous requests Homebridge deCONZ sends in parallel to a deCONZ gateway. Default: 10.",
34 | "type": "integer",
35 | "minimum": 1,
36 | "maximum": 30
37 | },
38 | "stealth": {
39 | "description": "Stealth mode: don't make any calls to the Internet. Default: false.",
40 | "type": "boolean"
41 | },
42 | "timeout": {
43 | "description": "The timeout in seconds to wait for a response from a deCONZ gateway. Default: 5.",
44 | "type": "integer",
45 | "minimum": 1,
46 | "maximum": 30
47 | },
48 | "waitTimePut": {
49 | "description": "The time, in milliseconds, to wait after sending a PUT request, before sending the next PUT request. Default: 50.",
50 | "type": "integer",
51 | "minimum": 0,
52 | "maximum": 50
53 | },
54 | "waitTimePutGroup": {
55 | "description": "The time, in milliseconds, to wait after sending a PUT request to a group, before sending the next PUT request. Default: 1000.",
56 | "type": "integer",
57 | "minimum": 0,
58 | "maximum": 1000
59 | },
60 | "waitTimeResend": {
61 | "description": "The time, in milliseconds, to wait before resending a request after an ECONNRESET or http status 503 error. Default: 300.",
62 | "type": "integer",
63 | "minimum": 100,
64 | "maximum": 1000
65 | },
66 | "waitTimeReset": {
67 | "description": "The timeout in milliseconds, to wait before resetting a characteristic value. Default: 500.",
68 | "type": "integer",
69 | "minimum": 10,
70 | "maximum": 2000
71 | },
72 | "waitTimeUpdate": {
73 | "description": "The time, in milliseconds, to wait for a change from HomeKit to another characteristic for the same light or group, before updating the deCONZ gateway. Default: 100.",
74 | "type": "integer",
75 | "minimum": 0,
76 | "maximum": 500
77 | }
78 | }
79 | },
80 | "layout": [
81 | "name",
82 | {
83 | "key": "hosts",
84 | "type": "array",
85 | "items": {
86 | "title": "Gateway",
87 | "description": "Hostname and port of the deCONZ gateway. Leave empty to discover gateways.",
88 | "type": "string"
89 | }
90 | },
91 | {
92 | "type": "fieldset",
93 | "expandable": true,
94 | "title": "Advanced Settings",
95 | "description": "Don't change these, unless you understand what you're doing.",
96 | "items": [
97 | "forceHttp",
98 | "noResponse",
99 | "parallelRequests",
100 | "stealth",
101 | "timeout",
102 | "waitTimePut",
103 | "waitTimePutGroup",
104 | "waitTimeResend",
105 | "waitTimeReset",
106 | "waitTimeUpdate"
107 | ]
108 | }
109 | ]
110 | }
111 |
--------------------------------------------------------------------------------
/deconz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebaauw/homebridge-deconz/1d6ee605966dd906761bd5ee9e4dfb85542c69d2/deconz.png
--------------------------------------------------------------------------------
/homebridge-ui/public/homebridge-deconz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebaauw/homebridge-deconz/1d6ee605966dd906761bd5ee9e4dfb85542c69d2/homebridge-ui/public/homebridge-deconz.png
--------------------------------------------------------------------------------
/homebridge-ui/public/index.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Gateways
22 |
23 | Add
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ gateway.name }}
31 |
32 | {{ gateway.host }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Configure Gateway
54 |
55 | - {{ selectedGateway.name }}
56 |
57 |
58 |
59 |
60 | Connect
61 |
62 |
63 | Get API Key
64 |
65 |
66 |
67 |
296 |
--------------------------------------------------------------------------------
/homebridge-ui/public/index.old.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
505 |
--------------------------------------------------------------------------------
/homebridge-ui/public/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebaauw/homebridge-deconz/1d6ee605966dd906761bd5ee9e4dfb85542c69d2/homebridge-ui/public/style.css
--------------------------------------------------------------------------------
/homebridge-ui/server.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/homebridge-ui/server.js
2 | //
3 | // Homebridge plug-in for deCONZ.
4 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
5 |
6 | import { UiServer } from 'hb-lib-tools/UiServer'
7 | import { Discovery } from 'hb-deconz-tools/Discovery'
8 |
9 | class DeconzUiServer extends UiServer {
10 | constructor () {
11 | super()
12 |
13 | this.onRequest('discover', async (params) => {
14 | if (this.discovery == null) {
15 | this.discovery = new Discovery({
16 | // forceHttp: this.config.forceHttp,
17 | // timeout: this.config.timeout
18 | })
19 | this.discovery
20 | .on('error', (error) => {
21 | this.log(
22 | '%s: request %d: %s %s', error.request.name,
23 | error.request.id, error.request.method, error.request.resource
24 | )
25 | this.warn(
26 | '%s: request %d: %s', error.request.name, error.request.id, error
27 | )
28 | })
29 | .on('request', (request) => {
30 | this.debug(
31 | '%s: request %d: %s %s', request.name,
32 | request.id, request.method, request.resource
33 | )
34 | })
35 | .on('response', (response) => {
36 | this.debug(
37 | '%s: request %d: %d %s', response.request.name,
38 | response.request.id, response.statusCode, response.statusMessage
39 | )
40 | })
41 | .on('found', (name, id, address) => {
42 | this.debug('%s: found %s at %s', name, id, address)
43 | })
44 | .on('searching', (host) => {
45 | this.debug('upnp: listening on %s', host)
46 | })
47 | .on('searchDone', () => { this.debug('upnp: search done') })
48 | }
49 | const configs = await this.discovery.discover()
50 | return configs
51 | })
52 | this.ready()
53 | }
54 | }
55 |
56 | new DeconzUiServer() // eslint-disable-line no-new
57 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/index.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { createRequire } from 'node:module'
7 |
8 | import { DeconzPlatform } from './lib/DeconzPlatform.js'
9 |
10 | const require = createRequire(import.meta.url)
11 | const packageJson = require('./package.json')
12 |
13 | function main (homebridge) {
14 | DeconzPlatform.loadPlatform(homebridge, packageJson, 'deCONZ', DeconzPlatform)
15 | }
16 |
17 | export { main as default }
18 |
--------------------------------------------------------------------------------
/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 | "cli",
12 | "node_modules/hb-deconz-tools/index.js",
13 | "node_modules/hb-deconz-tools/lib",
14 | "node_modules/hb-lib-tools/lib/HttpClient.js"
15 | ]
16 | },
17 | "opts": {
18 | "recurse": true
19 | },
20 | "templates": {
21 | "monospaceLinks": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/Deconz/Device.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/Deconz/Device.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { Deconz } from './index.js'
7 |
8 | /** Delegate class for a Zigbee or virtual device on a deCONZ gateway.
9 | *
10 | * The deCONZ REST API exposes a Zigbee device using one or more resources.
11 | * These resources are linked to the device through the `uniqueid` in the
12 | * resource body.
13 | * Each supported device corresponds to a HomeKit accessory.
14 | * Each supported resource corresponds to a HomeKit service.
15 | * @memberof Deconz
16 | */
17 | class Device {
18 | /** Create a new instance of a delegate of a device, from a resource.
19 | *
20 | * @param {Deconz.Resource} resource - The resource.
21 | */
22 | constructor (resource) {
23 | /** The device ID.
24 | *
25 | * This is the {@link Deconz.Resource#id id} of the delegates
26 | * of all resources for the device.
27 | * @type {string}
28 | */
29 | this.id = resource.id
30 |
31 | /** The key of the delegate for the primary resource for the device in
32 | * {@link DeconzDevice#resourceBySubtype resourceBySubtype}
33 | *
34 | * This is the {@link DeconzDevice.Resource#subtype subtype} of the
35 | * HomeKit service corresponding to the primary resource.
36 | * @type {string}
37 | */
38 | this.primary = resource.subtype
39 |
40 | /** An array of keys of the delegates for the resources for the device in
41 | * {@link DeconzDevice#resourceBySubtype resourceBySubtype} by service name.
42 | *
43 | * These are the {@link DeconzDevice.Resource#subtype subtype} values of the
44 | * HomeKit service corresponding to the resource.
45 | * @type {Object.>}
46 | */
47 | this.subtypesByServiceName = {}
48 | this.subtypesByServiceName[resource.serviceName] = [resource.subtype]
49 |
50 | /** The delegates of the resources for the device, by subtype of the
51 | * corresponding HomeKit service.
52 | * @type {Object.}
53 | */
54 | this.resourceBySubtype = {}
55 | this.resourceBySubtype[resource.subtype] = resource
56 |
57 | /** Zigbee device vs virtual device.
58 | *
59 | * This is the {@link Deconz.Resource#zigbee zigbee} of the
60 | * delegates of all resources for the device.
61 | * @type {boolean}
62 | */
63 | this.zigbee = resource.zigbee
64 |
65 | /** Device has a resource with `config.battery` in their `body`.
66 | * @type {boolean}
67 | */
68 | this.hasBattery = resource.body.config?.battery !== undefined
69 | }
70 |
71 | /** The delegate of the primary resource of the device.
72 | * @type {Deconz.Resource}
73 | */
74 | get resource () { return this.resourceBySubtype[this.primary] }
75 |
76 | /** List of resource paths of the resources for the device.
77 | * @type {string[]}
78 | */
79 | get rpaths () {
80 | return Object.keys(this.resourceBySubtype || {}).map((subtype) => {
81 | return this.resourceBySubtype[subtype].rpath
82 | })
83 | }
84 |
85 | /** Add a {@link Deconz.Resource Resource}.
86 | *
87 | * Updates {@link Deconz.Device#resourceBySubtype resourceBySubtype},
88 | * {@link Deconz.Device#rpaths rpaths}, and, when the added resource
89 | * has a higher priority, {@link Deconz.Device#primary primary} and
90 | * {@link Deconz.Device#resource resource}.
91 | * @param {Deconz.Resource} resource - The resource.
92 | */
93 | addResource (resource) {
94 | const { body, id, prio, rtype, subtype, zigbee } = resource
95 | if (this.resourceBySubtype[subtype] != null) {
96 | const r = this.resourceBySubtype[subtype]
97 | throw new Error(
98 | `${resource.rpath}: duplicate uniqueid ${body.uniqueid} in ${r.rpath}`
99 | )
100 | }
101 | if (zigbee !== this.zigbee || (zigbee && id !== this.id)) {
102 | const r = this.resourceBySubtype[subtype]
103 | throw new SyntaxError(
104 | `${resource.rpath}: cannot combine ${r.rpath}`
105 | )
106 | }
107 | if (this.subtypesByServiceName[resource.serviceName] == null) {
108 | this.subtypesByServiceName[resource.serviceName] = [resource.subtype]
109 | } else {
110 | this.subtypesByServiceName[resource.serviceName].push(resource.subtype)
111 | }
112 | this.resourceBySubtype[subtype] = resource
113 | if (resource.body.config?.battery !== undefined) {
114 | this.hasBattery = true
115 | }
116 | const p = this.resourceBySubtype[this.primary]
117 | if (p.rtype === rtype && p.prio < prio) {
118 | this.primary = resource.subtype
119 | }
120 | }
121 | }
122 |
123 | Deconz.Device = Device
124 |
--------------------------------------------------------------------------------
/lib/Deconz/index.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/Deconz/index.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | /** Library to discover, monitor, and interact with a deCONZ gateway.
7 | * @hideconstructor
8 | */
9 | class Deconz {}
10 |
11 | export { Deconz }
12 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/AirPurifier.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/Thermostat.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzAccessory } from '../DeconzAccessory/index.js'
7 |
8 | class AirPurifier extends DeconzAccessory {
9 | /** Instantiate a delegate for an accessory corresponding to a device.
10 | * @param {DeconzAccessory.Gateway} gateway - The gateway.
11 | * @param {Deconz.Device} device - The device.
12 | */
13 | constructor (gateway, device, settings = {}) {
14 | super(gateway, device, gateway.Accessory.Categories.AIR_PURIFIER)
15 | this.identify()
16 |
17 | this.service = this.createService(device.resource, { primaryService: true })
18 |
19 | for (const subtype in device.resourceBySubtype) {
20 | const resource = device.resourceBySubtype[subtype]
21 | if (subtype === device.primary) {
22 | continue
23 | }
24 | this.createService(resource)
25 | }
26 |
27 | setImmediate(() => {
28 | this.debug('initialised')
29 | this.emit('initialised')
30 | })
31 | }
32 | }
33 |
34 | DeconzAccessory.AirPurifier = AirPurifier
35 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/Light.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/Light.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/History'
8 |
9 | import { DeconzAccessory } from '../DeconzAccessory/index.js'
10 |
11 | /** Delegate class for a HomeKit accessory, corresponding to a light device
12 | * or groups resource.
13 | * @extends DeconzAccessory
14 | * @memberof DeconzAccessory
15 | */
16 | class Light extends DeconzAccessory {
17 | /** Instantiate a delegate for an accessory corresponding to a device.
18 | * @param {DeconzAccessory.Gateway} gateway - The gateway.
19 | * @param {Deconz.Device} device - The device.
20 | */
21 | constructor (gateway, device, settings = {}) {
22 | super(gateway, device, gateway.Accessory.Categories.LIGHTBULB)
23 |
24 | this.identify()
25 |
26 | this.addPropertyDelegate({
27 | key: 'serviceName',
28 | value: device.resource.serviceName
29 | }).on('didSet', (value) => {
30 | gateway.context.settingsById[device.id].serviceName = value
31 | })
32 | gateway.context.settingsById[device.id].serviceName = this.values.serviceName
33 |
34 | this.service = this.createService(device.resource, {
35 | primaryService: true,
36 | serviceName: this.values.serviceName
37 | })
38 |
39 | for (const subtype in device.resourceBySubtype) {
40 | const resource = device.resourceBySubtype[subtype]
41 | if (subtype === device.primary) {
42 | continue
43 | }
44 | if (resource.rtype === 'lights') {
45 | this.createService(resource, { serviceName: this.values.serviceName })
46 | } else {
47 | this.createService(resource)
48 | }
49 | }
50 |
51 | const params = {}
52 |
53 | if (this.values.serviceName === 'Valve') {
54 | // No history
55 | } else if (
56 | this.servicesByServiceName[this.values.serviceName].length > 1 ||
57 | this.values.serviceName === 'Light'
58 | ) {
59 | params.lightOnDelegate = this.service.characteristicDelegate('on')
60 | params.lastLightOnDelegate = this.service.addCharacteristicDelegate({
61 | key: 'lastActivation',
62 | Characteristic: this.Characteristics.eve.LastActivation,
63 | silent: true
64 | })
65 | } else { // Outlet or Switch
66 | if (this.values.serviceName === 'Outlet') {
67 | this.service.addCharacteristicDelegate({
68 | key: 'lockPhysicalControls',
69 | Characteristic: this.Characteristics.hap.LockPhysicalControls
70 | })
71 | }
72 | params.onDelegate = this.service.characteristicDelegate('on')
73 | params.lastOnDelegate = this.service.addCharacteristicDelegate({
74 | key: 'lastActivation',
75 | Characteristic: this.Characteristics.eve.LastActivation,
76 | silent: true
77 | })
78 | }
79 |
80 | if (this.servicesByServiceName.Consumption?.length === 1) {
81 | const service = this.servicesByServiceName.Consumption[0]
82 | params.totalConsumptionDelegate = service.characteristicDelegate('totalConsumption')
83 | if (service.values.consumption === undefined) {
84 | // Power to be computed by history if not exposed by device
85 | params.computedConsumptionDelegate = service.addCharacteristicDelegate({
86 | key: 'consumption',
87 | Characteristic: this.Characteristics.eve.Consumption,
88 | unit: ' W'
89 | })
90 | }
91 | } else if (this.servicesByServiceName.Power?.length === 1) {
92 | const service = this.servicesByServiceName.Power[0]
93 | params.consumptionDelegate = service.characteristicDelegate('consumption')
94 | // Total Consumption to be computed by history
95 | params.computedTotalConsumptionDelegate = service.addCharacteristicDelegate({
96 | key: 'totalConsumption',
97 | Characteristic: this.Characteristics.eve.TotalConsumption,
98 | unit: ' kWh'
99 | })
100 | }
101 |
102 | if (Object.keys(params).length > 0) {
103 | this.historyService = new ServiceDelegate.History(this, params)
104 | }
105 |
106 | if (this.servicesByServiceName[this.values.serviceName].length === 1) {
107 | if (
108 | this.values.serviceName === 'Outlet' &&
109 | this.servicesByServiceName.Consumption == null &&
110 | this.servicesByServiceName.Power == null
111 | ) {
112 | // Dumb Outlet
113 | const service = new ServiceDelegate(this, {
114 | name: this.name + ' Consumption',
115 | Service: this.Services.eve.Consumption,
116 | hidden: true
117 | })
118 | service.addCharacteristicDelegate({
119 | key: 'dummyTotalConsumption',
120 | Characteristic: this.Characteristics.eve.TotalConsumption,
121 | props: {
122 | perms: [
123 | this.Characteristic.Perms.PAIRED_READ,
124 | this.Characteristic.Perms.NOTIFY,
125 | this.Characteristic.Perms.HIDDEN
126 | ]
127 | },
128 | silent: true,
129 | value: 0
130 | })
131 | }
132 | } else {
133 | this.debug('servicesByServiceName: %j', Object.keys(this.servicesByServiceName))
134 | this.debug('servicesByServiceName[%s]: %j', this.values.serviceName, Object.keys(this.servicesByServiceName[this.values.serviceName]))
135 | for (const i in this.servicesByServiceName[this.values.serviceName]) {
136 | try {
137 | const service = this.servicesByServiceName[this.values.serviceName][i]
138 | this.debug('service %d: %j', i, service.rpath)
139 | service.addCharacteristicDelegate({
140 | key: 'index',
141 | Characteristic: this.Characteristics.hap.ServiceLabelIndex,
142 | silent: true,
143 | value: Number(i) + 1
144 | })
145 | service.values.index = Number(i) + 1
146 | if (i === '0') {
147 | continue
148 | }
149 | this.historyService?.addLastOnDelegate(
150 | service.characteristicDelegate('on'),
151 | service.addCharacteristicDelegate({
152 | key: 'lastActivation',
153 | Characteristic: this.Characteristics.eve.LastActivation,
154 | silent: true
155 | })
156 | )
157 | } catch (error) {
158 | this.warn(error)
159 | }
160 | }
161 | }
162 |
163 | setImmediate(() => {
164 | this.debug('initialised')
165 | this.emit('initialised')
166 | })
167 | }
168 | }
169 |
170 | DeconzAccessory.Light = Light
171 | DeconzAccessory.Outlet = Light
172 | DeconzAccessory.Switch = Light
173 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/Sensor.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/Sensor.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | // Keep separate for Eve History
7 | // Switch/Outlet/Lightbulb
8 | // Stateless Programmable Switch (Eve button)
9 | // Sensors
10 |
11 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
12 | import 'homebridge-lib/ServiceDelegate/History'
13 |
14 | import { DeconzAccessory } from '../DeconzAccessory/index.js'
15 |
16 | class Sensor extends DeconzAccessory {
17 | constructor (gateway, device) {
18 | super(gateway, device, gateway.Accessory.Categories.SENSOR)
19 |
20 | this.identify()
21 |
22 | this.service = this.createService(device.resource, { primaryService: true })
23 |
24 | for (const subtype in device.resourceBySubtype) {
25 | const resource = device.resourceBySubtype[subtype]
26 | if (subtype === device.primary) {
27 | continue
28 | }
29 | this.createService(resource)
30 | }
31 |
32 | switch (device.resource.serviceName) {
33 | case 'Daylight':
34 | case 'LightLevel':
35 | // Create dummy motion sensor service.
36 | this.motionService = new ServiceDelegate(this, {
37 | name: this.name + ' Motion',
38 | Service: this.Services.hap.MotionSensor,
39 | hidden: true
40 | })
41 | this.motionService.addCharacteristicDelegate({
42 | key: 'motion',
43 | Characteristic: this.Characteristics.hap.MotionDetected,
44 | props: {
45 | perms: [
46 | this.Characteristic.Perms.PAIRED_READ,
47 | this.Characteristic.Perms.NOTIFY,
48 | this.Characteristic.Perms.HIDDEN
49 | ]
50 | },
51 | value: 0
52 | })
53 | break
54 | default:
55 | break
56 | }
57 |
58 | const params = {}
59 | if (this.servicesByServiceName.Contact?.length === 1) {
60 | const service = this.servicesByServiceName.Contact[0]
61 | params.contactDelegate = service.characteristicDelegate('contact')
62 | params.lastContactDelegate = service.addCharacteristicDelegate({
63 | key: 'lastActivation',
64 | Characteristic: this.Characteristics.eve.LastActivation,
65 | silent: true
66 | })
67 | params.timesOpenedDelegate = service.addCharacteristicDelegate({
68 | key: 'timesOpened',
69 | Characteristic: this.Characteristics.eve.TimesOpened,
70 | value: 0,
71 | silent: true
72 | })
73 | }
74 | if (this.servicesByServiceName.Motion?.length === 1) {
75 | const service = this.servicesByServiceName.Motion[0]
76 | params.motionDelegate = service.characteristicDelegate('motion')
77 | params.lastMotionDelegate = service.addCharacteristicDelegate({
78 | key: 'lastActivation',
79 | Characteristic: this.Characteristics.eve.LastActivation,
80 | silent: true
81 | })
82 | }
83 | if (this.servicesByServiceName.LightLevel?.length === 1) {
84 | const service = this.servicesByServiceName.LightLevel[0]
85 | params.lightLevelDelegate = service.characteristicDelegate('lightLevel')
86 | }
87 | if (this.servicesByServiceName.Daylight?.length === 1) {
88 | const service = this.servicesByServiceName.Daylight[0]
89 | params.lightLevelDelegate = service.characteristicDelegate('lightLevel')
90 | }
91 | if (this.servicesByServiceName.Temperature?.length === 1) {
92 | const service = this.servicesByServiceName.Temperature[0]
93 | params.temperatureDelegate = service.characteristicDelegate('temperature')
94 | }
95 | if (this.servicesByServiceName.Humidity?.length === 1) {
96 | const service = this.servicesByServiceName.Humidity[0]
97 | params.humidityDelegate = service.characteristicDelegate('humidity')
98 | }
99 | if (this.servicesByServiceName.AirPressure?.length === 1) {
100 | const service = this.servicesByServiceName.AirPressure[0]
101 | params.airPressureDelegate = service.characteristicDelegate('airPressure')
102 | }
103 | if (this.servicesByServiceName.AirQuality?.length >= 1) {
104 | const service = this.servicesByServiceName.AirQuality[0]
105 | if (service.characteristicDelegate('vocDensity') != null) {
106 | params.vocDensityDelegate = service.characteristicDelegate('vocDensity')
107 | }
108 | }
109 | if (this.servicesByServiceName.Flag?.length === 1) {
110 | const service = this.servicesByServiceName.Flag[0]
111 | params.onDelegate = service.characteristicDelegate('on')
112 | params.lastOnDelegate = service.addCharacteristicDelegate({
113 | key: 'lastActivation',
114 | Characteristic: this.Characteristics.eve.LastActivation,
115 | silent: true
116 | })
117 | }
118 | if (
119 | params.temperatureDelegate != null && params.humidityDelegate != null &&
120 | params.airPressureDelegate == null && params.vocDensityDelegate == null &&
121 | this.servicesByServiceName.Battery?.length === 1
122 | ) {
123 | // Eve would see this as an Eve Thermo Control.
124 | this.airPressureService = new ServiceDelegate(this, {
125 | name: this.name + ' Pressure',
126 | Service: this.Services.eve.AirPressureSensor,
127 | hidden: true
128 | })
129 | this.airPressureService.addCharacteristicDelegate({
130 | key: 'airPressure',
131 | Characteristic: this.Characteristics.eve.AirPressure,
132 | props: {
133 | perms: [
134 | this.Characteristic.Perms.PAIRED_READ,
135 | this.Characteristic.Perms.NOTIFY,
136 | this.Characteristic.Perms.HIDDEN
137 | ]
138 | },
139 | value: 0
140 | })
141 | }
142 | if (Object.keys(params).length > 0) {
143 | this.historyService = new ServiceDelegate.History(this, params)
144 | }
145 |
146 | setImmediate(() => {
147 | this.debug('initialised')
148 | this.emit('initialised')
149 | })
150 | }
151 | }
152 |
153 | DeconzAccessory.Sensor = Sensor
154 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/Thermostat.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/Thermostat.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/History'
8 |
9 | import { DeconzAccessory } from '../DeconzAccessory/index.js'
10 |
11 | class Thermostat extends DeconzAccessory {
12 | /** Instantiate a delegate for an accessory corresponding to a device.
13 | * @param {DeconzAccessory.Gateway} gateway - The gateway.
14 | * @param {Deconz.Device} device - The device.
15 | */
16 | constructor (gateway, device, settings = {}) {
17 | super(gateway, device, gateway.Accessory.Categories.THERMOSTAT)
18 | this.identify()
19 |
20 | this.service = this.createService(device.resource, { primaryService: true })
21 |
22 | for (const subtype in device.resourceBySubtype) {
23 | const resource = device.resourceBySubtype[subtype]
24 | if (subtype === device.primary) {
25 | continue
26 | }
27 | this.createService(resource)
28 | }
29 |
30 | if (device.resource.body.state.valve !== undefined) {
31 | this.historyService = new ServiceDelegate.History(this, {
32 | temperatureDelegate: this.service.characteristicDelegate('currentTemperature'),
33 | targetTemperatureDelegate: this.service.characteristicDelegate('targetTemperature'),
34 | valvePositionDelegate: this.service.characteristicDelegate('valvePosition')
35 | })
36 | }
37 |
38 | setImmediate(() => {
39 | this.debug('initialised')
40 | this.emit('initialised')
41 | })
42 | }
43 | }
44 |
45 | DeconzAccessory.Thermostat = Thermostat
46 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/WarningDevice.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/WarningDevice.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/History'
8 |
9 | import { DeconzAccessory } from '../DeconzAccessory/index.js'
10 |
11 | /** Delegate class for a HomeKit accessory, corresponding to a light device
12 | * or groups resource.
13 | * @extends DeconzAccessory
14 | * @memberof DeconzAccessory
15 | */
16 | class WarningDevice extends DeconzAccessory {
17 | /** Instantiate a delegate for an accessory corresponding to a device.
18 | * @param {DeconzAccessory.Gateway} gateway - The gateway.
19 | * @param {Deconz.Device} device - The device.
20 | */
21 | constructor (gateway, device, settings = {}) {
22 | super(gateway, device, gateway.Accessory.Categories.SENSOR)
23 |
24 | this.identify()
25 |
26 | this.service = this.createService(device.resource, { primaryService: true })
27 |
28 | for (const subtype in device.resourceBySubtype) {
29 | const resource = device.resourceBySubtype[subtype]
30 | if (subtype === device.primary) {
31 | continue
32 | }
33 | this.createService(resource)
34 | }
35 |
36 | const params = {}
37 | if (this.servicesByServiceName.WarningDevice?.length === 1) {
38 | params.onDelegate = this.service.characteristicDelegate('on')
39 | params.lastOnDelegate = this.service.addCharacteristicDelegate({
40 | key: 'lastActivation',
41 | Characteristic: this.Characteristics.eve.LastActivation,
42 | silent: true
43 | })
44 | }
45 | if (this.servicesByServiceName.Temperature?.length === 1) {
46 | const service = this.servicesByServiceName.Temperature[0]
47 | params.temperatureDelegate = service.characteristicDelegate('temperature')
48 | }
49 | if (Object.keys(params).length > 0) {
50 | this.historyService = new ServiceDelegate.History(this, params)
51 | }
52 |
53 | setImmediate(() => {
54 | this.debug('initialised')
55 | this.emit('initialised')
56 | })
57 | }
58 | }
59 |
60 | DeconzAccessory.WarningDevice = WarningDevice
61 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/WindowCovering.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/WindowCovering.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzAccessory } from '../DeconzAccessory/index.js'
7 |
8 | /** Delegate class for a HomeKit accessory, corresponding to a light device
9 | * or groups resource.
10 | * @extends DeconzAccessory
11 | * @memberof DeconzAccessory
12 | */
13 | class WindowCovering extends DeconzAccessory {
14 | /** Instantiate a delegate for an accessory corresponding to a device.
15 | * @param {DeconzAccessory.Gateway} gateway - The gateway.
16 | * @param {Deconz.Device} device - The device.
17 | */
18 | constructor (gateway, device, settings = {}) {
19 | super(gateway, device, gateway.Accessory.Categories.WINDOW_COVERING)
20 |
21 | this.identify()
22 |
23 | this.service = this.createService(device.resource, {
24 | primaryService: true,
25 | serviceName: this.values.serviceName
26 | })
27 |
28 | for (const subtype in device.resourceBySubtype) {
29 | const resource = device.resourceBySubtype[subtype]
30 | if (subtype === device.primary) {
31 | continue
32 | }
33 | this.createService(resource)
34 | }
35 |
36 | setImmediate(() => {
37 | this.debug('initialised')
38 | this.emit('initialised')
39 | })
40 | }
41 | }
42 |
43 | DeconzAccessory.WindowCovering = WindowCovering
44 |
--------------------------------------------------------------------------------
/lib/DeconzAccessory/index.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzAccessory/index.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate'
7 | import { OptionParser } from 'homebridge-lib/OptionParser'
8 |
9 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
10 |
11 | import { DeconzService } from '../DeconzService/index.js'
12 | import '../DeconzService/Button.js'
13 |
14 | const { HttpError } = ApiClient
15 | const { SINGLE, DOUBLE, LONG } = DeconzService.Button
16 |
17 | /** Abstract superclass for a delegate of a HomeKit accessory,
18 | * corresponding to a Zigbee or virtual device on a deCONZ gateway.
19 | * @extends AccessoryDelegate
20 | */
21 | class DeconzAccessory extends AccessoryDelegate {
22 | /** Instantiate a delegate for an accessory corresponding to a device.
23 | * @param {DeconzAccessory.Gateway} gateway - The gateway.
24 | * @param {Deconz.Device} device - The device.
25 | * @param {Accessory.Category} category - The HomeKit accessory category.
26 | */
27 | constructor (gateway, device, category) {
28 | super(gateway.platform, {
29 | id: device.id,
30 | name: device.resource.body.name,
31 | manufacturer: device.resource.manufacturer,
32 | model: device.resource.model,
33 | firmware: device.resource.firmware,
34 | category,
35 | logLevel: gateway.logLevel
36 | })
37 |
38 | this.context.gid = gateway.id
39 |
40 | this.serviceByRpath = {}
41 | this.serviceBySubtype = {}
42 | this.servicesByServiceName = {}
43 |
44 | /** The gateway.
45 | * @type {DeconzAccessory.Gateway}
46 | */
47 | this.gateway = gateway
48 |
49 | /** The accessory ID.
50 | *
51 | * This is the {@link Deconz.Device#id id} of the corresponding device.
52 | * @type {string}
53 | */
54 | this.id = device.id
55 |
56 | /** The corresponding device.
57 | * @type {Deconz.Device}
58 | */
59 | this.device = device
60 |
61 | /** The API client instance for the gateway.
62 | * @type {ApiClient}
63 | */
64 | this.client = gateway.client
65 |
66 | this
67 | .on('polled', (device) => {
68 | let reExpose = false
69 | this.values.firmware = device.resource.firmware
70 | for (const subtype in device.resourceBySubtype) {
71 | const resource = device.resourceBySubtype[subtype]
72 | this.debug('%s: polled: %j', resource.rpath, resource.body)
73 | const service = this.serviceBySubtype[subtype]
74 | if (service == null) {
75 | this.log('%s: new resource: %j', resource.rpath, resource.body)
76 | reExpose = true
77 | } else {
78 | service.update(resource.body, resource.rpath)
79 | }
80 | }
81 | for (const subtype in this.serviceBySubtype) {
82 | const service = this.serviceBySubtype[subtype]
83 | const resource = device.resourceBySubtype[subtype]
84 | if (resource == null) {
85 | this.log('%s: resource deleted', service.rpath)
86 | reExpose = true
87 | }
88 | }
89 | if (reExpose) {
90 | this.gateway.reExposeAccessory(this.id)
91 | }
92 | })
93 | .on('changed', (rpath, body) => {
94 | this.debug('%s: changed: %j', rpath, body)
95 | const service = this.serviceByRpath[rpath]
96 | if (service != null) {
97 | service.update(body, rpath)
98 | }
99 | })
100 | .on('identify', async () => {
101 | try {
102 | await this.identify()
103 | } catch (error) {
104 | if (!(error instanceof HttpError)) {
105 | this.warn(error)
106 | }
107 | }
108 | })
109 | }
110 |
111 | /** The primary resource of the device.
112 | * @type {Deconz.Resource}
113 | */
114 | get resource () { return this.device.resource }
115 |
116 | /** List of resource paths of associated resources in order of prio.
117 | * @type {string[]}
118 | */
119 | get rpaths () { return this.device.rpaths }
120 |
121 | async identify () {
122 | this.log(
123 | '%s %s v%s (%d resources)', this.values.manufacturer, this.values.model,
124 | this.values.firmware, this.rpaths.length
125 | )
126 | this.debug('%d resources: %s', this.rpaths.length, this.rpaths.join(', '))
127 | this.vdebug('device: %j', this.device)
128 | if (this.service != null) {
129 | await this.service.identify()
130 | }
131 | }
132 |
133 | createService (resource, params = {}) {
134 | if (resource == null) {
135 | return
136 | }
137 | if (params.serviceName == null) {
138 | params.serviceName = resource.serviceName
139 | }
140 | if (DeconzService[params.serviceName] == null) {
141 | this.warn(
142 | '%s: %s: service type not available',
143 | resource.rpath, params.serviceName
144 | )
145 | return
146 | }
147 | this.debug(
148 | '%s: capabilities: %j', resource.rpath, resource.capabilities
149 | )
150 | this.debug('%s: params: %j', resource.rpath, params)
151 |
152 | let service
153 | if (params.serviceName === 'AirQuality') {
154 | service = this.servicesByServiceName.AirQuality?.[0]
155 | if (service != null) {
156 | service.addResource(resource)
157 | }
158 | } else if (params.serviceName === 'Battery') {
159 | service = this.servicesByServiceName.Battery?.[0]
160 | } else if (params.serviceName === 'Consumption') {
161 | service = this.servicesByServiceName.Power?.[0]
162 | if (service != null) {
163 | service.addResource(resource)
164 | }
165 | } else if (params.serviceName === 'Power') {
166 | service = this.servicesByServiceName.Consumption?.[0]
167 | if (service != null) {
168 | service.addResource(resource)
169 | }
170 | } else if (params.serviceName === 'Label') {
171 | service = this.servicesByServiceName.Label?.[0]
172 | // Default button
173 | if (resource.capabilities.buttons == null) {
174 | if (service == null) {
175 | this.warn(
176 | '%s: unknown %s: %j', resource.rpath, resource.body.type,
177 | resource.body
178 | )
179 | resource.capabilities.buttons = {
180 | 1: {
181 | label: 'Unknown Button',
182 | events: SINGLE | DOUBLE | LONG
183 | }
184 | }
185 | resource.capabilities.namespace =
186 | this.Characteristics.hap.ServiceLabelNamespace.ARABIC_NUMERALS
187 | } else {
188 | resource.capabilities.buttons = {}
189 | }
190 | }
191 | }
192 | if (service == null) {
193 | service = new DeconzService[params.serviceName](this, resource, {
194 | primaryService: params.primaryService
195 | })
196 | }
197 | if (this.servicesByServiceName[params.serviceName] == null) {
198 | this.servicesByServiceName[params.serviceName] = [service]
199 | } else {
200 | this.servicesByServiceName[params.serviceName].push(service)
201 | }
202 | if (params.serviceName === 'Label') {
203 | service.createButtonServices(resource, params)
204 | }
205 | this.serviceBySubtype[resource.subtype] = service
206 | this.serviceByRpath[resource.rpath] = service
207 | if (resource.body.config?.battery !== undefined) {
208 | if (this.servicesByServiceName.Battery?.[0] == null) {
209 | this.servicesByServiceName.Battery = [new DeconzService.Battery(this, resource)]
210 | }
211 | service.batteryService = this.servicesByServiceName.Battery[0]
212 | }
213 | return service
214 | }
215 |
216 | onUiGet (details = false) {
217 | const resource = this.device.resourceBySubtype[this.device.primary]
218 | const body = {
219 | id: details ? this.id : undefined,
220 | manufacturer: this.values.manufacturer,
221 | model: this.values.model,
222 | name: this.name,
223 | resources: this.device.rpaths,
224 | settings: details
225 | ? {
226 | anyOn: this.device.resource.rtype === 'groups'
227 | ? this.values.anyOn
228 | : undefined,
229 | buttonRepeat: undefined, // map per button
230 | expose: true,
231 | exposeEffects: this.service.values.exposeEffects,
232 | exposeScenes: this.service.values.exposeScenes,
233 | multiClip: undefined,
234 | multiLight: undefined,
235 | logLevel: this.values.logLevel,
236 | lowBatteryThreshold: this.servicesByServiceName?.Battery?.[0].values.lowBatteryThreshold,
237 | // offset: this.servicesByServiceName?.Temperature?.[0].values.offset,
238 | pin: this.service.values.pin,
239 | serviceName: this.values.serviceName,
240 | venetianBlind: this.service.values.venetianBlind,
241 | useExternalTemperature: this.service.values.useExternalTemperature,
242 | wallSwitch: this.service.values.wallSwitch
243 | }
244 | : undefined,
245 | type: resource.rtype,
246 | zigbee: this.device.zigbee
247 | }
248 | return { status: 200, body }
249 | }
250 |
251 | onUiPut (body) {
252 | let reExpose = false
253 | const responseBody = {}
254 | for (const key in body) {
255 | try {
256 | let value
257 | switch (key) {
258 | case 'expose':
259 | value = OptionParser.toBool(key, body[key])
260 | if (value) {
261 | reExpose = true
262 | } else {
263 | this.gateway.exposeDevice(this.id, value)
264 | }
265 | responseBody[key] = value
266 | continue
267 | // Settings for the primary service.
268 | case 'anyOn':
269 | case 'exposeEffects':
270 | case 'exposeScenes':
271 | case 'venetianBlind':
272 | if (this.service.values[key] != null) {
273 | value = OptionParser.toBool(key, body[key])
274 | this.service.values[key] = value
275 | reExpose = true
276 | responseBody[key] = value
277 | continue
278 | }
279 | break
280 | case 'logLevel':
281 | value = OptionParser.toInt(key, body[key], 0, 3)
282 | this.values[key] = value
283 | responseBody[key] = value
284 | continue
285 | case 'lowBatteryThreshold':
286 | if (this.servicesByServiceName.Battery?.[0] != null) {
287 | value = OptionParser.toInt(key, body[key], 10, 100)
288 | this.servicesByServiceName.Battery[0].values[key] = value
289 | responseBody[key] = value
290 | continue
291 | }
292 | break
293 | case 'pin':
294 | if (this.service.values[key] != null) {
295 | value = OptionParser.toString(key, body[key])
296 | this.service.values[key] = value
297 | responseBody[key] = value
298 | continue
299 | }
300 | break
301 | case 'serviceName':
302 | if (this.values.serviceName != null) {
303 | value = OptionParser.toString(key, body[key])
304 | if (['Light', 'Outlet', 'Switch', 'Valve'].includes(value) == null) {
305 | throw new Error(`${value}: illegal serviceName`)
306 | }
307 | this.values.serviceName = value
308 | reExpose = true
309 | responseBody[key] = value
310 | continue
311 | }
312 | break
313 | case 'useExternalTemperature':
314 | case 'wallSwitch':
315 | if (this.service.values[key] != null) {
316 | value = OptionParser.toBool(key, body[key])
317 | this.service.values[key] = value
318 | responseBody[key] = value
319 | continue
320 | }
321 | break
322 | default:
323 | break
324 | }
325 | this.warn('ui error: %s: invalid key', key)
326 | } catch (error) { this.warn('ui error: %s', error) }
327 | }
328 | if (reExpose) {
329 | this.gateway.reExposeAccessory(this.id)
330 | }
331 | return { status: 200, body: responseBody }
332 | }
333 | }
334 |
335 | export { DeconzAccessory }
336 |
--------------------------------------------------------------------------------
/lib/DeconzPlatform.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzPlatform.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { once } from 'node:events'
7 |
8 | import { timeout } from 'homebridge-lib'
9 | import { HttpClient } from 'homebridge-lib/HttpClient'
10 | import { OptionParser } from 'homebridge-lib/OptionParser'
11 | import { Platform } from 'homebridge-lib/Platform'
12 |
13 | import { Discovery } from 'hb-deconz-tools/Discovery'
14 |
15 | import { DeconzAccessory } from './DeconzAccessory/index.js'
16 | import './DeconzAccessory/Gateway.js'
17 |
18 | class DeconzPlatform extends Platform {
19 | constructor (log, configJson, homebridge, bridge) {
20 | super(log, configJson, homebridge)
21 | this.parseConfigJson(configJson)
22 | this.debug('config: %j', this.config)
23 |
24 | this
25 | .on('accessoryRestored', this.accessoryRestored)
26 | .once('heartbeat', this.init)
27 | .on('heartbeat', this.heartbeat)
28 | }
29 |
30 | parseConfigJson (configJson) {
31 | this.config = {
32 | forceHttp: false,
33 | hosts: [],
34 | noResponse: false,
35 | parallelRequests: 10,
36 | stealth: false,
37 | timeout: 5,
38 | waitTimePut: 50,
39 | waitTimePutGroup: 1000,
40 | waitTimeResend: 300,
41 | waitTimeReset: 500,
42 | waitTimeUpdate: 100
43 | }
44 | const optionParser = new OptionParser(this.config, true)
45 | optionParser
46 | .on('userInputError', (message) => {
47 | this.warn('config.json: %s', message)
48 | })
49 | .stringKey('name')
50 | .stringKey('platform')
51 | .boolKey('forceHttp')
52 | .stringKey('host')
53 | .arrayKey('hosts')
54 | .boolKey('noResponse')
55 | .intKey('parallelRequests', 1, 30)
56 | .boolKey('stealth')
57 | .intKey('timeout', 5, 30)
58 | .intKey('waitTimePut', 0, 50)
59 | .intKey('waitTimePutGroup', 0, 1000)
60 | .intKey('waitTimeResend', 100, 1000)
61 | .intKey('waitTimeReset', 10, 2000)
62 | .intKey('waitTimeUpdate', 0, 500)
63 |
64 | this.gatewayMap = {}
65 |
66 | try {
67 | optionParser.parse(configJson)
68 | if (this.config.host != null) {
69 | this.config.hosts.push(this.config.host)
70 | }
71 | this.discovery = new Discovery({
72 | forceHttp: this.config.forceHttp,
73 | timeout: this.config.timeout
74 | })
75 | this.discovery
76 | .on('error', (error) => {
77 | if (error instanceof HttpClient.HttpError) {
78 | this.log(
79 | '%s: request %d: %s %s', error.request.name,
80 | error.request.id, error.request.method, error.request.resource
81 | )
82 | this.warn(
83 | '%s: request %d: %s', error.request.name, error.request.id, error
84 | )
85 | return
86 | }
87 | this.warn(error)
88 | })
89 | .on('request', (request) => {
90 | this.debug(
91 | '%s: request %d: %s %s', request.name,
92 | request.id, request.method, request.resource
93 | )
94 | })
95 | .on('response', (response) => {
96 | this.debug(
97 | '%s: request %d: %d %s', response.request.name,
98 | response.request.id, response.statusCode, response.statusMessage
99 | )
100 | })
101 | .on('found', (name, id, address) => {
102 | this.debug('%s: found %s at %s', name, id, address)
103 | })
104 | .on('searching', (host) => {
105 | this.debug('upnp: listening on %s', host)
106 | })
107 | .on('searchDone', () => { this.debug('upnp: search done') })
108 | } catch (error) {
109 | this.error(error)
110 | }
111 | }
112 |
113 | async foundGateway (host, config) {
114 | const id = config.bridgeid
115 | if (this.gatewayMap[id] == null) {
116 | this.gatewayMap[id] = new DeconzAccessory.Gateway(this, { config, host })
117 | }
118 | await this.gatewayMap[id].found(host, config)
119 | await once(this.gatewayMap[id], 'initialised')
120 | this.emit('found')
121 | }
122 |
123 | async findHost (host) {
124 | try {
125 | const config = await this.discovery.config(host)
126 | await this.foundGateway(host, config)
127 | } catch (error) {
128 | this.warn('%s: %s - retrying in 60s', host, error)
129 | await timeout(60000)
130 | return this.findHost(host)
131 | }
132 | }
133 |
134 | async init () {
135 | try {
136 | const jobs = []
137 | if (this.config.hosts.length > 0) {
138 | for (const host of this.config.hosts) {
139 | this.debug('job %d: find gateway at %s', jobs.length, host)
140 | jobs.push(this.findHost(host))
141 | }
142 | } else {
143 | this.debug('job %d: find at least one gateway', jobs.length)
144 | jobs.push(once(this, 'found'))
145 | for (const id in this.gatewayMap) {
146 | const gateway = this.gatewayMap[id]
147 | const host = gateway.values.host
148 | this.debug('job %d: find gateway %s', jobs.length, id)
149 | jobs.push(once(gateway, 'initialised'))
150 | try {
151 | const config = await this.discovery.config(host)
152 | await this.foundGateway(host, config)
153 | } catch (error) {
154 | this.warn('%s: %s', id, error)
155 | }
156 | }
157 | }
158 |
159 | this.debug('waiting for %d jobs', jobs.length)
160 | for (const id in jobs) {
161 | try {
162 | await jobs[id]
163 | this.debug('job %d/%d: done', Number(id) + 1, jobs.length)
164 | } catch (error) {
165 | this.warn(error)
166 | }
167 | }
168 |
169 | this.log('%d gateways', Object.keys(this.gatewayMap).length)
170 | this.emit('initialised')
171 | const dumpInfo = {
172 | config: this.config,
173 | gatewayMap: {}
174 | }
175 | for (const id in this.gatewayMap) {
176 | const gateway = this.gatewayMap[id]
177 | dumpInfo.gatewayMap[id] = Object.assign({}, gateway.context)
178 | dumpInfo.gatewayMap[id].deviceById = gateway.deviceById
179 | }
180 | await this.createDumpFile(dumpInfo)
181 | } catch (error) { this.error(error) }
182 | }
183 |
184 | async onUiRequest (method, url, body) {
185 | const path = url.split('/').slice(1)
186 | if (path.length < 1) {
187 | return { status: 403 } // Forbidden
188 | }
189 | if (path[0] === 'gateways') {
190 | if (path.length === 1) {
191 | if (method === 'GET') {
192 | // const gatewayByHost = await this.discovery.discover()
193 | const body = {}
194 | for (const id of Object.keys(this.gatewayMap).sort()) {
195 | const gateway = this.gatewayMap[id]
196 | body[gateway.values.host] = {
197 | config: gateway.context.config,
198 | host: gateway.values.host,
199 | id
200 | }
201 | }
202 | return { status: 200, body }
203 | }
204 | return { status: 405 } // Method Not Allowed
205 | }
206 | const gateway = this.gatewayMap[path[1]]
207 | if (gateway == null) {
208 | return { status: 404 } // Not Found
209 | }
210 | if (method === 'GET') {
211 | return gateway.onUiGet(path.slice(2))
212 | }
213 | if (method === 'PUT') {
214 | return gateway.onUiPut(path.slice(2), body)
215 | }
216 | return { status: 405 } // Method Not Allowed
217 | }
218 | return { status: 403 } // Forbidden
219 | }
220 |
221 | async heartbeat (beat) {
222 | try {
223 | if (beat % 300 === 5 && this.config.hosts.length === 0) {
224 | const configs = await this.discovery.discover()
225 | const jobs = []
226 | for (const host in configs) {
227 | jobs.push(this.foundGateway(host, configs[host]))
228 | }
229 | for (const job of jobs) {
230 | try {
231 | await job
232 | } catch (error) {
233 | this.error(error)
234 | }
235 | }
236 | }
237 | } catch (error) { this.error(error) }
238 | }
239 |
240 | /** Called when an accessory has been restored.
241 | *
242 | * Re-create {@link DeconzAccessory.Gateway Gateway} delegates for restored
243 | * gateway accessories.
244 | * Accessories for devices exposed by the gateway will be restored from
245 | * the gateway context, once Homebridge has started it's HAP server.
246 | */
247 | accessoryRestored (className, version, id, name, context) {
248 | try {
249 | if (className === 'Gateway') {
250 | if (
251 | this.config.hosts.length === 0 ||
252 | this.config.hosts.includes(context.host)
253 | ) {
254 | this.gatewayMap[id] = new DeconzAccessory.Gateway(this, context)
255 | }
256 | } else {
257 | const gateway = this.gatewayMap[context.gid]
258 | if (gateway != null) {
259 | gateway.addAccessory(id)
260 | }
261 | }
262 | } catch (error) { this.error(error) }
263 | }
264 | }
265 |
266 | export { DeconzPlatform }
267 |
--------------------------------------------------------------------------------
/lib/DeconzService/AirPressure.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/AirPressure.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class AirPressure extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.eve.AirPressureSensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'airPressure',
19 | Characteristic: this.Characteristics.eve.AirPressure,
20 | unit: ' hPa'
21 | })
22 |
23 | this.addCharacteristicDelegate({
24 | key: 'elevation',
25 | Characteristic: this.Characteristics.eve.Elevation,
26 | value: 0
27 | })
28 |
29 | // this.addCharacteristicDelegate({
30 | // key: 'trend',
31 | // Characteristic: this.Characteristics.eve.WeatherTrend,
32 | // value: 0
33 | // })
34 |
35 | this.addCharacteristicDelegates()
36 |
37 | this.update(resource.body, resource.rpath)
38 | }
39 |
40 | updateState (state) {
41 | if (state.pressure != null) {
42 | this.values.airPressure = Math.round(state.pressure * 10) / 10
43 | }
44 | // this.values.trend = Math.round(new Date().valueOf() / 60000) % 16
45 | super.updateState(state)
46 | }
47 | }
48 |
49 | DeconzService.AirPressure = AirPressure
50 |
--------------------------------------------------------------------------------
/lib/DeconzService/AirPurifier.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/AirPurifier.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | class FilterMaintenance extends DeconzService.SensorsResource {
10 | constructor (accessory, resource, params = {}) {
11 | params.Service = accessory.Services.hap.FilterMaintenance
12 | super(accessory, resource, params)
13 |
14 | this.addCharacteristicDelegate({
15 | key: 'filterChange',
16 | Characteristic: this.Characteristics.hap.FilterChangeIndication
17 | })
18 |
19 | if (
20 | resource.body.config.filterlifetime !== undefined &&
21 | resource.body.state.filterruntime !== undefined
22 | ) {
23 | this.addCharacteristicDelegate({
24 | key: 'filterLifeLevel',
25 | Characteristic: this.Characteristics.hap.FilterLifeLevel,
26 | unit: '%'
27 | })
28 | this.addCharacteristicDelegate({
29 | key: 'resetFilter',
30 | Characteristic: this.Characteristics.hap.ResetFilterIndication,
31 | props: { adminOnlyAccess: [this.Characteristic.Access.WRITE] },
32 | value: 0
33 | }).on('didSet', async (value, fromHomeKit) => {
34 | await this.put('/config', { filterlifetime: 6 * 30 * 24 * 60 })
35 | })
36 | this.values.filterLifeTime = resource.body.config.filterlifetime
37 | }
38 |
39 | this.update(resource.body, resource.rpath)
40 | }
41 |
42 | updateState (state) {
43 | if (this.values.filterLifeTime != null && state.filterruntime != null) {
44 | this.values.filterLifeLevel = 100 - Math.round(
45 | 100 * state.filterruntime / this.values.filterLifeTime
46 | )
47 | }
48 | if (state.replacefilter != null) {
49 | this.values.filterChange = state.replacefilter
50 | ? this.Characteristics.hap.FilterChangeIndication.CHANGE_FILTER
51 | : this.Characteristics.hap.FilterChangeIndication.FILTER_OK
52 | }
53 | }
54 |
55 | updateConfig (config) {
56 | if (config.filterlifetime != null) {
57 | this.values.filterLifeTime = config.filterlifetime
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * @memberof DeconzService
64 | */
65 | class AirPurifier extends DeconzService.SensorsResource {
66 | constructor (accessory, resource, params = {}) {
67 | params.Service = accessory.Services.hap.AirPurifier
68 | super(accessory, resource, params)
69 |
70 | this.addCharacteristicDelegate({
71 | key: 'active',
72 | Characteristic: this.Characteristics.hap.Active
73 | }).on('didSet', async (value, fromHomeKit) => {
74 | if (fromHomeKit) {
75 | await this.put('/config', { mode: this.modeValue(value) })
76 | }
77 | })
78 |
79 | this.addCharacteristicDelegate({
80 | key: 'currentState',
81 | Characteristic: this.Characteristics.hap.CurrentAirPurifierState
82 | })
83 |
84 | this.addCharacteristicDelegate({
85 | key: 'targetState',
86 | Characteristic: this.Characteristics.hap.TargetAirPurifierState
87 | }).on('didSet', async (value, fromHomeKit) => {
88 | if (fromHomeKit) {
89 | await this.put('/config', { mode: this.modeValue(null, value) })
90 | }
91 | })
92 |
93 | this.addCharacteristicDelegate({
94 | key: 'rotationSpeed',
95 | Characteristic: this.Characteristics.hap.RotationSpeed,
96 | unit: '%'
97 | }).on('didSet', async (value, fromHomeKit) => {
98 | if (fromHomeKit) {
99 | await this.put('/config', { mode: this.modeValue(null, null, value) })
100 | }
101 | })
102 |
103 | if (resource.body.state.airquality !== undefined) {
104 | this.airQualityService = new DeconzService.AirQuality(accessory, resource, {
105 | linkedServiceDelegate: this
106 | })
107 | }
108 |
109 | if (resource.body.state.replacefilter !== undefined) {
110 | this.filterService = new FilterMaintenance(accessory, resource, {
111 | linkedServiceDelegate: this
112 | })
113 | }
114 |
115 | if (resource.body.state.deviceruntime !== undefined) {
116 | // TODO
117 | }
118 |
119 | if (resource.body.config.ledindication !== undefined) {
120 | // TODO
121 | }
122 |
123 | if (resource.body.config.locked !== undefined) {
124 | this.addCharacteristicDelegate({
125 | key: 'lockPhysicalControls',
126 | Characteristic: this.Characteristics.hap.LockPhysicalControls
127 | }).on('didSet', async (value, fromHomeKit) => {
128 | if (fromHomeKit) {
129 | await this.put('/config', {
130 | locked: value === this.Characteristics.hap.LockPhysicalControls
131 | .CONTROL_LOCK_ENABLED
132 | })
133 | }
134 | })
135 | }
136 |
137 | super.addCharacteristicDelegates()
138 |
139 | this.update(resource.body, resource.rpath)
140 | }
141 |
142 | modeValue (
143 | active = this.values.active,
144 | targetState = this.values.targetState,
145 | rotationSpeed = this.values.rotationSpeed
146 | ) {
147 | if (active === this.Characteristics.hap.Active.INACTIVE) {
148 | return 'off'
149 | }
150 | if (
151 | targetState === this.Characteristics.hap.TargetAirPurifierState.AUTO ||
152 | rotationSpeed === 0
153 | ) {
154 | return 'auto'
155 | }
156 | return 'speed_' + Math.round(rotationSpeed / 20)
157 | }
158 |
159 | updateState (state) {
160 | if (state.speed != null) {
161 | this.values.active = state.speed > 0
162 | ? this.Characteristics.hap.Active.ACTIVE
163 | : this.Characteristics.hap.Active.INACTIVE
164 | this.values.currentState = state.speed === 0
165 | ? this.Characteristics.hap.CurrentAirPurifierState.INACTIVE
166 | : this.Characteristics.hap.CurrentAirPurifierState.PURIFYING_AIR
167 | this.values.rotationSpeed = state.speed
168 | }
169 | super.updateState(state)
170 | if (this.airQualityService != null) {
171 | this.airQualityService.updateState(state)
172 | }
173 | if (this.filterService != null) {
174 | this.filterService.updateState(state)
175 | }
176 | }
177 |
178 | updateConfig (config) {
179 | if (config.filterlifetime != null) {
180 | this.values.filterLifeTime = config.filterlifetime
181 | }
182 | if (config.ledindication != null) {
183 | // TODO
184 | }
185 | if (config.locked != null) {
186 | this.values.lockPhysicalControls = config.locked
187 | ? this.Characteristics.hap.LockPhysicalControls.CONTROL_LOCK_ENABLED
188 | : this.Characteristics.hap.LockPhysicalControls.CONTROL_LOCK_DISABLED
189 | }
190 | if (config.mode != null) {
191 | this.values.targetState = config.mode === 'auto'
192 | ? this.Characteristics.hap.TargetAirPurifierState.AUTO
193 | : this.Characteristics.hap.TargetAirPurifierState.MANUAL
194 | }
195 | super.updateConfig(config)
196 | if (this.airQualityService != null) {
197 | this.airQualityService.updateConfig(config)
198 | }
199 | if (this.filterService != null) {
200 | this.filterService.updateConfig(config)
201 | }
202 | }
203 | }
204 |
205 | DeconzService.AirPurifier = AirPurifier
206 |
--------------------------------------------------------------------------------
/lib/DeconzService/AirQuality.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/AirQuality.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/SensorsResource.js'
10 |
11 | const { dateToString } = ApiClient
12 |
13 | /**
14 | * @memberof DeconzService
15 | */
16 | class AirQuality extends DeconzService.SensorsResource {
17 | static addResource (service, resource) {
18 | if (service.values.airQuality === undefined) {
19 | service.addCharacteristicDelegate({
20 | key: 'airQuality',
21 | Characteristic: service.Characteristics.hap.AirQuality
22 | })
23 | }
24 |
25 | if (
26 | resource.body.state.measured_value !== undefined &&
27 | resource.body.capabilities?.measured_value != null
28 | ) {
29 | const cmv = resource.body.capabilities.measured_value
30 | switch (cmv.substance) {
31 | case 'PM2.5':
32 | if (cmv.quantity !== 'density') {
33 | service.warn('%s: unsupported substance', cmv.quantity)
34 | break
35 | }
36 | if (cmv.unit !== 'ug/m^3') {
37 | service.warn('%s: unsupported unit', cmv.unit)
38 | break
39 | }
40 | service.addCharacteristicDelegate({
41 | key: 'pm25Density',
42 | Characteristic: service.Characteristics.hap.PM2_5Density,
43 | unit: ' µg/m³',
44 | props: { minValue: cmv.min, maxValue: cmv.max }
45 | })
46 | service.resources[resource.rpath] = {
47 | key: 'pm25Density',
48 | f: (v) => { return v }
49 | }
50 | break
51 | case 'tVOC':
52 | if (cmv.quantity !== 'level') {
53 | service.warn('%s: unsupported substance', cmv.quantity)
54 | break
55 | }
56 | if (cmv.unit !== 'ppb') {
57 | service.warn('%s: unsupported unit', cmv.unit)
58 | break
59 | }
60 | service.addCharacteristicDelegate({
61 | key: 'vocDensity',
62 | Characteristic: service.Characteristics.hap.VOCDensity,
63 | unit: ' µg/m³',
64 | props: {
65 | minValue: Math.floor(cmv.min * 4.57),
66 | maxValue: Math.ceil(cmv.max * 4.57)
67 | }
68 | })
69 | service.resources[resource.rpath] = {
70 | key: 'vocDensity',
71 | f: (v) => { return Math.round(v * 4.57) }
72 | }
73 | break
74 | default:
75 | service.warn('%s: unsupported substance', cmv.substance)
76 | break
77 | }
78 | } else if (resource.body.state.airqualityppb !== undefined) {
79 | service.addCharacteristicDelegate({
80 | key: 'vocDensity',
81 | Characteristic: service.Characteristics.hap.VOCDensity,
82 | unit: ' µg/m³',
83 | props: { minValue: 0, maxValue: 65535, minStep: 1 }
84 | })
85 | } else if (resource.body.state.pm2_5 !== undefined) {
86 | service.addCharacteristicDelegate({
87 | key: 'pm25Density',
88 | Characteristic: service.Characteristics.hap.PM2_5Density,
89 | unit: ' µg/m³',
90 | props: { minValue: 0, maxValue: 65535, minStep: 1 }
91 | })
92 | }
93 |
94 | if (service.values.lastUpdated === undefined) {
95 | service.addCharacteristicDelegate({
96 | key: 'lastUpdated',
97 | Characteristic: service.Characteristics.my.LastUpdated,
98 | silent: true
99 | })
100 | }
101 |
102 | AirQuality.updateResourceState(service, resource.body.state)
103 | }
104 |
105 | static airQualityValue (service, value) {
106 | switch (value) {
107 | case 'excellent':
108 | return service.Characteristics.hap.AirQuality.EXCELLENT
109 | case 'good':
110 | return service.Characteristics.hap.AirQuality.GOOD
111 | case 'moderate':
112 | return service.Characteristics.hap.AirQuality.FAIR
113 | case 'poor':
114 | return service.Characteristics.hap.AirQuality.INFERIOR
115 | case 'unhealthy':
116 | return service.Characteristics.hap.AirQuality.POOR
117 | default:
118 | return service.Characteristics.hap.AirQuality.UNKNOWN
119 | }
120 | }
121 |
122 | static updateResourceState (service, state, rpath) {
123 | if (state.measured_value != null && service.resources[rpath] != null) {
124 | const { key, f } = service.resources[rpath]
125 | service.values[key] = f(state.measured_value)
126 | if (
127 | state.airquality != null && (
128 | key === 'vocDensity' || service.values.vocDensity === undefined
129 | )
130 | ) {
131 | service.values.airQuality = AirQuality.airQualityValue(
132 | service, state.airquality
133 | )
134 | }
135 | } else if (state.airqualityppb != null) {
136 | service.values.vocDensity = Math.round(state.airqualityppb * 4.57)
137 | if (state.airquality != null) {
138 | service.values.airQuality = AirQuality.airQualityValue(
139 | service, state.airquality
140 | )
141 | }
142 | } else if (state.pm2_5 != null) {
143 | service.values.pm25Density = state.pm2_5
144 | if (state.airquality != null && service.values.vocDensity === undefined) {
145 | service.values.airQuality = AirQuality.airQualityValue(
146 | service, state.airquality
147 | )
148 | }
149 | }
150 | if (state.lastupdated != null) {
151 | service.values.lastUpdated = dateToString(state.lastupdated)
152 | }
153 | }
154 |
155 | constructor (accessory, resource, params = {}) {
156 | params.Service = accessory.Services.hap.AirQualitySensor
157 | super(accessory, resource, params)
158 | this.resources = {}
159 |
160 | AirQuality.addResource(this, resource)
161 |
162 | super.addCharacteristicDelegates({ noLastUpdated: true })
163 |
164 | this.update(resource.body, resource.rpath)
165 | }
166 |
167 | updateState (state, rpath) {
168 | AirQuality.updateResourceState(this, state, rpath)
169 | super.updateState(state)
170 | }
171 | }
172 |
173 | DeconzService.AirQuality = AirQuality
174 |
--------------------------------------------------------------------------------
/lib/DeconzService/Alarm.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Alarm.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Alarm extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.my.Resource
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'alarm',
19 | Characteristic: this.Characteristics.my.Alarm
20 | })
21 |
22 | super.addCharacteristicDelegates(params)
23 |
24 | this.update(resource.body, resource.rpath)
25 | }
26 |
27 | updateState (state) {
28 | if (state.test) {
29 | if (this.testTimout) {
30 | clearTimeout(this.testTimeout)
31 | }
32 | this.test = true
33 | this.testTimout = setTimeout(() => {
34 | delete this.testTimeout
35 | this.test = false
36 | }, 5000)
37 | }
38 | if (state.alarm) {
39 | this.values.alarm = true
40 | } else if (this.test) {
41 | this.values.alarm = true
42 | } else {
43 | this.values.alarm = false
44 | }
45 | super.updateState(state)
46 | }
47 | }
48 |
49 | DeconzService.Alarm = Alarm
50 |
--------------------------------------------------------------------------------
/lib/DeconzService/AlarmSystem.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/AlarmSystem.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 |
8 | let mapsInitialised = false
9 | const armModeMap = {}
10 | const armStateMap = {}
11 | const targetStateMap = {}
12 |
13 | function initMaps (currentState, targetState) {
14 | if (mapsInitialised) {
15 | return
16 | }
17 | armStateMap.disarmed = currentState.DISARMED
18 | armStateMap.armed_away = currentState.AWAY_ARM
19 | armStateMap.armed_stay = currentState.STAY_ARM
20 | armStateMap.armed_night = currentState.NIGHT_ARM
21 | armStateMap.in_alarm = currentState.ALARM_TRIGGERED
22 | armModeMap.disarmed = targetState.DISARM
23 | armModeMap.armed_away = targetState.AWAY_ARM
24 | armModeMap.armed_stay = targetState.STAY_ARM
25 | armModeMap.armed_night = targetState.NIGHT_ARM
26 | targetStateMap[targetState.DISARM] = 'disarm'
27 | targetStateMap[targetState.AWAY_ARM] = 'arm_away'
28 | targetStateMap[targetState.STAY_ARM] = 'arm_stay'
29 | targetStateMap[targetState.NIGHT_ARM] = 'arm_night'
30 | mapsInitialised = true
31 | }
32 |
33 | /**
34 | * @memberof DeconzService
35 | */
36 | class AlarmSystem extends DeconzService {
37 | constructor (accessory, resource, params = {}) {
38 | params.Service = accessory.Services.hap.SecuritySystem
39 | super(accessory, resource, params)
40 |
41 | initMaps(
42 | this.Characteristics.hap.SecuritySystemCurrentState,
43 | this.Characteristics.hap.SecuritySystemTargetState
44 | )
45 | this.addCharacteristicDelegate({
46 | key: 'currentState',
47 | Characteristic: this.Characteristics.hap.SecuritySystemCurrentState
48 | })
49 | this.addCharacteristicDelegate({
50 | key: 'targetState',
51 | Characteristic: this.Characteristics.hap.SecuritySystemTargetState
52 | }).on('didSet', async (value, fromHomeKit) => {
53 | if (fromHomeKit) {
54 | await this.put(`/${targetStateMap[value]}`, {
55 | code0: this.values.pin
56 | })
57 | }
58 | })
59 | this.addCharacteristicDelegate({
60 | key: 'alarmType',
61 | Characteristic: this.Characteristics.hap.SecuritySystemAlarmType
62 | })
63 | this.addCharacteristicDelegate({
64 | key: 'pin',
65 | value: '0000'
66 | })
67 |
68 | this.update(resource.body, resource.rpath)
69 | }
70 |
71 | updateState (state) {
72 | if (armStateMap[state.armstate] != null) {
73 | this.values.currentState = armStateMap[state.armstate]
74 | }
75 | this.values.alarmType = state.armstate === 'in_alarm'
76 | ? 1 // this.Characteristics.hap.SecuritySystemAlarmType.UNKNOWN
77 | : 0 // this.Characteristics.hap.SecuritySystemAlarmType.NO_ALARM
78 | }
79 |
80 | updateConfig (config) {
81 | if (armModeMap[config.armmode] != null) {
82 | this.values.targetState = armModeMap[config.armmode]
83 | }
84 | }
85 | }
86 |
87 | DeconzService.AlarmSystem = AlarmSystem
88 |
--------------------------------------------------------------------------------
/lib/DeconzService/Battery.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Battery.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 | import 'homebridge-lib/ServiceDelegate/Battery'
8 |
9 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
10 |
11 | import { DeconzService } from '../DeconzService/index.js'
12 |
13 | const { dateToString } = ApiClient
14 |
15 | /**
16 | * @memberof DeconzService
17 | */
18 | class Battery extends ServiceDelegate.Battery {
19 | constructor (accessory, resource, params = {}) {
20 | const batteryParams = {
21 | name: accessory.name + ' Battery',
22 | exposeConfiguredName: true
23 | }
24 | const state = resource.body.state
25 | const config = resource.body.config
26 | if (state.battery != null) {
27 | batteryParams.batteryLevel = state.battery
28 | batteryParams.lowBatteryThreshold = 20
29 | if (state.charging != null) {
30 | batteryParams.chargingState = state.charging
31 | }
32 | } else if (config.battery != null) {
33 | batteryParams.batteryLevel = config.battery
34 | batteryParams.lowBatteryThreshold = 20
35 | }
36 | super(accessory, batteryParams)
37 |
38 | if (state.battery != null) {
39 | this.addCharacteristicDelegate({
40 | key: 'lastUpdated',
41 | Characteristic: this.Characteristics.my.LastUpdated,
42 | silent: true
43 | })
44 | }
45 |
46 | this.update(resource.body, resource.rpath)
47 | }
48 |
49 | update (body, rpath) {
50 | if (this.updating) {
51 | return
52 | }
53 | if (body.config != null) {
54 | this.updateConfig(body.config)
55 | }
56 | if (body.state != null) {
57 | this.updateState(body.state)
58 | }
59 | }
60 |
61 | updateState (state) {
62 | if (state.battery != null) {
63 | this.values.batteryLevel = state.battery
64 | if (state.charging != null) {
65 | this.values.chargingState = state.charging
66 | ? this.Characteristics.hap.ChargingState.CHARGING
67 | : this.Characteristics.hap.ChargingState.NOT_CHARGING
68 | }
69 | this.values.lastUpdated = dateToString(state.lastupdated)
70 | }
71 | }
72 |
73 | updateConfig (config) {
74 | if (config.battery != null) {
75 | this.values.batteryLevel = config.battery
76 | }
77 | }
78 | }
79 |
80 | DeconzService.Battery = Battery
81 |
--------------------------------------------------------------------------------
/lib/DeconzService/Button.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Button.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { DeconzService } from './index.js'
9 |
10 | const deconzEvent = {
11 | PRESS: 0,
12 | HOLD: 1,
13 | SHORT_RELEASE: 2,
14 | LONG_RELEASE: 3,
15 | DOUBLE_PRESS: 4,
16 | TRIPLE_PRESS: 5,
17 | QUADRUPLE_PRESS: 6,
18 | SHAKE: 7,
19 | DROP: 8,
20 | TILT: 9
21 | }
22 |
23 | let homeKitEvent
24 |
25 | /**
26 | * @memberof DeconzService
27 | */
28 | class Button extends ServiceDelegate {
29 | static get SINGLE () { return 0x01 }
30 | static get DOUBLE () { return 0x02 }
31 | static get LONG () { return 0x04 }
32 |
33 | props (bitmap) {
34 | if (homeKitEvent == null) {
35 | homeKitEvent = {
36 | SINGLE_PRESS: this.Characteristics.hap.ProgrammableSwitchEvent.SINGLE_PRESS,
37 | DOUBLE_PRESS: this.Characteristics.hap.ProgrammableSwitchEvent.DOUBLE_PRESS,
38 | LONG_PRESS: this.Characteristics.hap.ProgrammableSwitchEvent.LONG_PRESS
39 | }
40 | }
41 |
42 | switch (bitmap) {
43 | case Button.SINGLE:
44 | return {
45 | minValue: homeKitEvent.SINGLE_PRESS,
46 | maxValue: homeKitEvent.SINGLE_PRESS,
47 | validValues: [
48 | homeKitEvent.SINGLE_PRESS
49 | ]
50 | }
51 | case Button.SINGLE | Button.DOUBLE:
52 | return {
53 | minValue: homeKitEvent.SINGLE_PRESS,
54 | maxValue: homeKitEvent.DOUBLE_PRESS,
55 | validValues: [
56 | homeKitEvent.SINGLE_PRESS,
57 | homeKitEvent.DOUBLE_PRESS
58 | ]
59 | }
60 | case Button.SINGLE | Button.LONG:
61 | return {
62 | minValue: homeKitEvent.SINGLE_PRESS,
63 | maxValue: homeKitEvent.LONG_PRESS,
64 | validValues: [
65 | homeKitEvent.SINGLE_PRESS,
66 | homeKitEvent.LONG_PRESS
67 | ]
68 | }
69 | case Button.SINGLE | Button.DOUBLE | Button.LONG:
70 | return {
71 | minValue: homeKitEvent.SINGLE_PRESS,
72 | maxValue: homeKitEvent.LONG_PRESS,
73 | validValues: [
74 | homeKitEvent.SINGLE_PRESS,
75 | homeKitEvent.DOUBLE_PRESS,
76 | homeKitEvent.LONG_PRESS
77 | ]
78 | }
79 | case Button.DOUBLE:
80 | return {
81 | minValue: homeKitEvent.DOUBLE_PRESS,
82 | maxValue: homeKitEvent.DOUBLE_PRESS,
83 | validValues: [
84 | homeKitEvent.DOUBLE_PRESS
85 | ]
86 | }
87 | case Button.DOUBLE | Button.LONG:
88 | return {
89 | minValue: homeKitEvent.DOUBLE_PRESS,
90 | maxValue: homeKitEvent.LONG_PRESS,
91 | validValues: [
92 | homeKitEvent.DOUBLE_PRESS,
93 | homeKitEvent.LONG_PRESS
94 | ]
95 | }
96 | case Button.LONG:
97 | return {
98 | minValue: homeKitEvent.LONG_PRESS,
99 | maxValue: homeKitEvent.LONG_PRESS,
100 | validValues: [
101 | homeKitEvent.LONG_PRESS
102 | ]
103 | }
104 | }
105 | }
106 |
107 | constructor (deconzAccessory, params = {}) {
108 | params.Service = deconzAccessory.Services.hap.StatelessProgrammableSwitch
109 | params.subtype = params.button
110 | params.exposeConfiguredName = true
111 | super(deconzAccessory, params)
112 | this.button = params.button
113 |
114 | this.addCharacteristicDelegate({
115 | key: 'event',
116 | Characteristic: this.Characteristics.hap.ProgrammableSwitchEvent,
117 | props: this.props(params.events)
118 | })
119 |
120 | this.addCharacteristicDelegate({
121 | key: 'index',
122 | Characteristic: this.Characteristics.hap.ServiceLabelIndex,
123 | value: params.button
124 | })
125 | }
126 |
127 | homeKitValue (value, oldValue = 0, repeat = false) {
128 | const button = Math.floor(value / 1000)
129 | if (button !== this.button) {
130 | return null
131 | }
132 | const oldButton = Math.floor(oldValue / 1000)
133 | const event = value % 1000
134 | const oldEvent = oldValue % 1000
135 | switch (event) {
136 | case deconzEvent.PRESS:
137 | // Wait for Hold or Release after press.
138 | return null
139 | case deconzEvent.SHORT_RELEASE:
140 | return homeKitEvent.SINGLE_PRESS
141 | case deconzEvent.HOLD:
142 | if (repeat) {
143 | return homeKitEvent.SINGLE_PRESS
144 | }
145 | // falls through
146 | case deconzEvent.LONG_RELEASE:
147 | if (repeat || (button === oldButton && oldEvent === deconzEvent.HOLD)) {
148 | // Already issued action on previous Hold.
149 | return null
150 | }
151 | // falls through
152 | case deconzEvent.TRIPLE_PRESS:
153 | case deconzEvent.QUADRUPLE_PRESS:
154 | case deconzEvent.SHAKE:
155 | return homeKitEvent.LONG_PRESS
156 | case deconzEvent.DOUBLE_PRESS:
157 | case deconzEvent.DROP:
158 | return homeKitEvent.DOUBLE_PRESS
159 | case deconzEvent.TILT:
160 | default:
161 | return null
162 | }
163 | }
164 |
165 | update (value, oldValue, repeat) {
166 | const event = this.homeKitValue(value, oldValue, repeat)
167 | if (event !== null) {
168 | this.values.event = event
169 | }
170 | }
171 |
172 | updateRotation () {
173 | this.values.event = homeKitEvent.SINGLE_PRESS
174 | }
175 | }
176 |
177 | DeconzService.Button = Button
178 |
--------------------------------------------------------------------------------
/lib/DeconzService/CarbonMonoxide.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/CarbonMonoxide.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class CarbonMonoxide extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.CarbonMonoxideSensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'carbonmonoxide',
19 | Characteristic: this.Characteristics.hap.CarbonMonoxideDetected
20 | })
21 |
22 | super.addCharacteristicDelegates(params)
23 |
24 | this.update(resource.body, resource.rpath)
25 | }
26 |
27 | updateState (state) {
28 | if (state.test) {
29 | if (this.testTimout) {
30 | clearTimeout(this.testTimeout)
31 | }
32 | this.test = true
33 | this.testTimout = setTimeout(() => {
34 | delete this.testTimeout
35 | this.test = false
36 | }, 5000)
37 | }
38 | if (state.carbonmonoxide) {
39 | this.values.carbonmonoxide = this.Characteristics.hap.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL
40 | } else if (this.test) {
41 | this.values.carbonmonoxide = this.Characteristics.hap.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL
42 | } else {
43 | this.values.carbonmonoxide = this.Characteristics.hap.CarbonMonoxideDetected.CO_LEVELS_NORMAL
44 | }
45 | super.updateState(state)
46 | }
47 | }
48 |
49 | DeconzService.CarbonMonoxide = CarbonMonoxide
50 |
--------------------------------------------------------------------------------
/lib/DeconzService/Consumption.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Consumption.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/SensorsResource.js'
10 |
11 | const { dateToString } = ApiClient
12 |
13 | /**
14 | * @memberof DeconzService
15 | */
16 | class Consumption extends DeconzService.SensorsResource {
17 | static addResource (service, resource) {
18 | service.addCharacteristicDelegate({
19 | key: 'totalConsumption',
20 | Characteristic: service.Characteristics.eve.TotalConsumption,
21 | unit: ' kWh'
22 | })
23 |
24 | if (
25 | resource.body.state.power !== undefined &&
26 | service.values.consumption === undefined
27 | ) {
28 | service.addCharacteristicDelegate({
29 | key: 'consumption',
30 | Characteristic: service.Characteristics.eve.Consumption,
31 | unit: ' W'
32 | })
33 | }
34 |
35 | if (service.values.lastUpdated === undefined) {
36 | service.addCharacteristicDelegate({
37 | key: 'lastUpdated',
38 | Characteristic: service.Characteristics.my.LastUpdated,
39 | silent: true
40 | })
41 | }
42 |
43 | Consumption.updateResourceState(service, resource.body.state)
44 | }
45 |
46 | static updateResourceState (service, state) {
47 | if (state.consumption != null) {
48 | service.values.totalConsumption = state.consumption / 1000
49 | }
50 | if (state.power != null) {
51 | service.values.consumption = state.power
52 | }
53 | if (state.lastupdated != null) {
54 | service.values.lastUpdated = dateToString(state.lastupdated)
55 | }
56 | }
57 |
58 | constructor (accessory, resource, params = {}) {
59 | params.name = accessory.name + ' Consumption'
60 | params.Service = accessory.Services.eve.Consumption
61 | params.exposeConfiguredName = true
62 | super(accessory, resource, params)
63 |
64 | Consumption.addResource(this, resource)
65 |
66 | super.addCharacteristicDelegates({ noLastUpdated: true })
67 |
68 | this.update(resource.body, resource.rpath)
69 | }
70 |
71 | updateState (state) {
72 | Consumption.updateResourceState(this, state)
73 | super.updateState(state)
74 | }
75 | }
76 |
77 | DeconzService.Consumption = Consumption
78 |
--------------------------------------------------------------------------------
/lib/DeconzService/Contact.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Contact.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Contact extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.ContactSensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'contact',
19 | Characteristic: this.Characteristics.hap.ContactSensorState
20 | })
21 |
22 | // With _Status Tapered_ Eve thinks the _Door Sensor_ is an Eve Windows Guard
23 | // (instead of an Eve Door & Window) and won't display it.
24 | this.addCharacteristicDelegates({ noTampered: true })
25 |
26 | this.update(resource.body, resource.rpath)
27 | }
28 |
29 | updateState (state) {
30 | if (state.open != null) {
31 | this.values.contact = state.open
32 | ? this.Characteristics.hap.ContactSensorState.CONTACT_NOT_DETECTED
33 | : this.Characteristics.hap.ContactSensorState.CONTACT_DETECTED
34 | }
35 | super.updateState(state)
36 | }
37 | }
38 |
39 | DeconzService.Contact = Contact
40 |
--------------------------------------------------------------------------------
/lib/DeconzService/Daylight.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Daylight.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/SensorsResource.js'
10 |
11 | const { dateToString } = ApiClient
12 |
13 | const daylightEvents = {
14 | 100: { name: 'Solar Midnight', period: 'Night' },
15 | 110: { name: 'Astronomical Dawn', period: 'Astronomical Twilight' },
16 | 120: { name: 'Nautical Dawn', period: 'Nautical Twilight' },
17 | 130: { name: 'Dawn', period: 'Twilight' },
18 | 140: { name: 'Sunrise', period: 'Sunrise' },
19 | 150: { name: 'End Sunrise', period: 'Golden Hour' },
20 | 160: { name: 'End Golden Hour', period: 'Day' },
21 | 170: { name: 'Solar Noon', period: 'Day' },
22 | 180: { name: 'Start Golden Hour', period: 'Golden Hour' },
23 | 190: { name: 'Start Sunset', period: 'Sunset' },
24 | 200: { name: 'Sunset', period: 'Twilight' },
25 | 210: { name: 'Dusk', period: 'Nautical Twilight' },
26 | 220: { name: 'Nautical Dusk', period: 'Astronomical Twilight' },
27 | 230: { name: 'Astronomical Dusk', period: 'Night' }
28 | }
29 |
30 | // Eve uses the following thresholds:
31 | const VERY_BRIGHT = 1000
32 | const BRIGHT = 300
33 | const NORMAL = 100
34 | const DIM = 10
35 | const DARK = 0
36 |
37 | const daylightPeriods = {
38 | Night: { lightLevel: DARK, dark: true, daylight: false },
39 | 'Astronomical Twilight': { lightLevel: DIM, dark: true, daylight: false },
40 | 'Nautical Twilight': { lightLevel: DIM, dark: true, daylight: false },
41 | Twilight: { lightLevel: NORMAL, dark: false, daylight: false },
42 | Sunrise: { lightLevel: BRIGHT, dark: false, daylight: true },
43 | Sunset: { lightLevel: BRIGHT, dark: false, daylight: true },
44 | 'Golden Hour': { lightLevel: BRIGHT, dark: false, daylight: true },
45 | Day: { lightLevel: VERY_BRIGHT, dark: false, daylight: true }
46 | }
47 |
48 | /**
49 | * @memberof DeconzService
50 | */
51 | class Daylight extends DeconzService.SensorsResource {
52 | constructor (accessory, resource, params = {}) {
53 | params.Service = accessory.Services.hap.LightSensor
54 | super(accessory, resource, params)
55 |
56 | this.addCharacteristicDelegate({
57 | key: 'lightLevel',
58 | Characteristic: this.Characteristics.hap.CurrentAmbientLightLevel,
59 | unit: ' lux'
60 | })
61 |
62 | this.addCharacteristicDelegate({
63 | key: 'dark',
64 | Characteristic: this.Characteristics.my.Dark
65 | })
66 |
67 | this.addCharacteristicDelegate({
68 | key: 'daylight',
69 | Characteristic: this.Characteristics.my.Daylight
70 | })
71 |
72 | this.addCharacteristicDelegate({
73 | key: 'status',
74 | Characteristic: this.Characteristics.my.Status,
75 | props: {
76 | minValue: 100,
77 | maxValue: 230,
78 | perms: [
79 | this.Characteristic.Perms.PAIRED_READ,
80 | this.Characteristic.Perms.NOTIFY]
81 | },
82 | value: resource.body.state.status
83 | })
84 |
85 | this.addCharacteristicDelegate({
86 | key: 'lastEvent',
87 | Characteristic: this.Characteristics.my.LastEvent
88 | })
89 |
90 | this.addCharacteristicDelegate({
91 | key: 'period',
92 | Characteristic: this.Characteristics.my.Period
93 | })
94 |
95 | this.addCharacteristicDelegates()
96 |
97 | this.addCharacteristicDelegate({
98 | key: 'sunrise',
99 | Characteristic: this.Characteristics.my.Sunrise
100 | })
101 |
102 | this.addCharacteristicDelegate({
103 | key: 'sunset',
104 | Characteristic: this.Characteristics.my.Sunset
105 | })
106 |
107 | if (!resource.body.config.configured) {
108 | this.warn('%s: %s not configured', resource.rpath, resource.body.type)
109 | }
110 |
111 | this.update(resource.body, resource.rpath)
112 | }
113 |
114 | updateState (state) {
115 | if (state.status != null) {
116 | this.values.status = state.status
117 | const { name, period } = daylightEvents[state.status]
118 | this.values.lastEvent = name
119 | this.values.period = period
120 | const { lightLevel, dark, daylight } = daylightPeriods[period]
121 | this.values.lightLevel = lightLevel
122 | this.values.dark = dark
123 | this.values.daylight = daylight
124 | }
125 | if (state.sunrise != null) {
126 | this.values.sunrise = dateToString(state.sunrise)
127 | }
128 | if (state.sunset != null) {
129 | this.values.sunset = dateToString(state.sunset)
130 | }
131 | super.updateState(state)
132 | }
133 | }
134 |
135 | DeconzService.Daylight = Daylight
136 |
--------------------------------------------------------------------------------
/lib/DeconzService/Flag.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Flag.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Flag extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.Switch
15 | super(accessory, resource, params)
16 |
17 | if (resource.capabilities.readonly) {
18 | this.addCharacteristicDelegate({
19 | key: 'on',
20 | Characteristic: this.Characteristics.hap.On,
21 | props: {
22 | perms: [
23 | this.Characteristic.Perms.PAIRED_READ, this.Characteristic.Perms.NOTIFY
24 | ]
25 | }
26 | })
27 | } else {
28 | this.addCharacteristicDelegate({
29 | key: 'on',
30 | Characteristic: this.Characteristics.hap.On
31 | }).on('didSet', async (value, fromHomeKit) => {
32 | if (fromHomeKit) {
33 | await this.put('/state', { flag: value })
34 | }
35 | })
36 | }
37 |
38 | this.addCharacteristicDelegates()
39 |
40 | this.update(resource.body, resource.rpath)
41 | }
42 |
43 | updateState (state) {
44 | if (state.flag != null) {
45 | this.values.on = state.flag
46 | }
47 | super.updateState(state)
48 | }
49 | }
50 |
51 | DeconzService.Flag = Flag
52 |
--------------------------------------------------------------------------------
/lib/DeconzService/Gateway.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Gateway.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { DeconzService } from './index.js'
9 |
10 | /** Delegate class for a DeconzGateway service.
11 | * @extends ServiceDelegate
12 | * @memberof DeconzService
13 | */
14 | class Gateway extends ServiceDelegate {
15 | constructor (gateway, params = {}) {
16 | params.Service = gateway.Services.my.DeconzGateway
17 | params.exposeConfiguredName = true
18 | super(gateway, params)
19 | this.gateway = gateway
20 |
21 | this.addCharacteristicDelegate({
22 | key: 'lastUpdated',
23 | Characteristic: this.Characteristics.my.LastUpdated,
24 | silent: true
25 | })
26 |
27 | this.addCharacteristicDelegate({
28 | key: 'statusActive',
29 | Characteristic: this.Characteristics.hap.StatusActive,
30 | value: true,
31 | silent: true
32 | })
33 |
34 | this.addCharacteristicDelegate({
35 | key: 'search',
36 | Characteristic: this.Characteristics.my.Search,
37 | value: false
38 | }).on('didSet', (value, fromHomeKit) => {
39 | if (fromHomeKit) {
40 | this.gateway.values.search = value
41 | }
42 | })
43 |
44 | this.addCharacteristicDelegate({
45 | key: 'transitionTime',
46 | Characteristic: this.Characteristics.my.TransitionTime,
47 | value: this.gateway.defaultTransitionTime
48 | })
49 | this.values.transitionTime = this.gateway.defaultTransitionTime
50 | }
51 |
52 | update (config) {
53 | this.values.expose = true
54 | this.values.lastUpdated = new Date().toString().slice(0, 24)
55 | }
56 | }
57 |
58 | DeconzService.Gateway = Gateway
59 |
--------------------------------------------------------------------------------
/lib/DeconzService/Humidity.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Humidity.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Humidity extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.HumiditySensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'humidity',
19 | Characteristic: this.Characteristics.hap.CurrentRelativeHumidity,
20 | unit: '%'
21 | })
22 |
23 | this.addCharacteristicDelegates()
24 |
25 | this.update(resource.body, resource.rpath)
26 | }
27 |
28 | updateState (state) {
29 | if (state.measured_value != null) {
30 | this.values.humidity = Math.round(state.measured_value * 10) / 10
31 | } else if (state.humidity != null) {
32 | this.values.humidity = Math.round(state.humidity / 10) / 10
33 | } else if (state.moisture != null) {
34 | this.values.humidity = Math.round(state.moisture / 10) / 10
35 | }
36 | super.updateState(state)
37 | }
38 | }
39 |
40 | DeconzService.Humidity = Humidity
41 |
--------------------------------------------------------------------------------
/lib/DeconzService/Label.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Label.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Label extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.ServiceLabel
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'namespace',
19 | Characteristic: this.Characteristics.hap.ServiceLabelNamespace,
20 | value: resource.capabilities.namespace
21 | })
22 |
23 | this.addCharacteristicDelegates({ noLastUpdated: true })
24 |
25 | this.buttonResources = {}
26 | this.buttonServices = {}
27 | this.hasRepeat = false
28 | }
29 |
30 | createButtonServices (resource) {
31 | if (resource.body.type.endsWith('Switch')) {
32 | this.buttonResources[resource.rpath] = {
33 | buttonEvent: resource.body.state.buttonevent,
34 | lastUpdated: new Date(),
35 | toButtonEvent: resource.capabilities.toButtonEvent
36 | }
37 | } else if (resource.body.type.endsWith('RelativeRotary')) {
38 | const keys = Object.keys(resource.capabilities.buttons)
39 | this.buttonResources[resource.rpath] = {
40 | expectedRotation: resource.body.state.expectedrotation,
41 | lastUpdated: new Date(),
42 | right: keys[0],
43 | left: keys[1]
44 | }
45 | } else if (resource.body.type.endsWith('AncillaryControl')) {
46 | this.buttonResources[resource.rpath] = {
47 | buttonEvent: resource.body.state.action,
48 | lastUpdated: new Date(),
49 | toButtonEvent: resource.capabilities.toButtonEvent
50 | }
51 | }
52 | for (const i in resource.capabilities.buttons) {
53 | const { label, events, hasRepeat } = resource.capabilities.buttons[i]
54 | if (hasRepeat) {
55 | this.hasRepeat = hasRepeat
56 | }
57 | this.buttonServices[i] = new DeconzService.Button(this.accessoryDelegate, {
58 | name: this.name + ' ' + label,
59 | button: Number(i),
60 | events,
61 | hasRepeat
62 | })
63 | }
64 | }
65 |
66 | updateState (state, rpath) {
67 | const buttonResource = this.buttonResources[rpath]
68 | if (buttonResource == null) {
69 | this.log('%s: resource deleted', rpath)
70 | this.gateway.reExposeAccessory(this.accessory.id)
71 | return
72 | }
73 | const lastUpdated = new Date(state.lastupdated + 'Z')
74 | if (lastUpdated > buttonResource.lastUpdated) {
75 | buttonResource.lastUpdated = lastUpdated
76 | if (buttonResource.buttonEvent !== undefined) {
77 | const oldValue = buttonResource.buttonEvent
78 | if (state.buttonevent != null) {
79 | buttonResource.buttonEvent = buttonResource.toButtonEvent == null
80 | ? state.buttonevent
81 | : buttonResource.toButtonEvent(state.buttonevent)
82 | } else if (state.action != null) {
83 | buttonResource.buttonEvent = buttonResource.toButtonEvent(state.action)
84 | }
85 | // TODO handle repeat
86 | const i = Math.floor(buttonResource.buttonEvent / 1000)
87 | this.buttonServices[i]?.update(
88 | buttonResource.buttonEvent, oldValue,
89 | false // this.accessoryDelegate.settingsService.values.repeat
90 | )
91 | } else {
92 | if (state.expectedrotation != null) {
93 | buttonResource.expectedRotation = state.expectedrotation
94 | }
95 | const i = buttonResource.expectedRotation >= 0
96 | ? buttonResource.right
97 | : buttonResource.left
98 | this.buttonServices[i]?.updateRotation()
99 | }
100 | }
101 | super.updateState(state)
102 | }
103 |
104 | updateConfig (config) {
105 | // TODO handle change in devicemode
106 | super.updateConfig(config)
107 | }
108 |
109 | async identify () {
110 | this.debug('hasRepeat: %j', this.hasRepeat)
111 | return super.identify()
112 | }
113 | }
114 |
115 | DeconzService.Label = Label
116 |
--------------------------------------------------------------------------------
/lib/DeconzService/Leak.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Leak.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Leak extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.LeakSensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'leak',
19 | Characteristic: this.Characteristics.hap.LeakDetected
20 | })
21 |
22 | super.addCharacteristicDelegates()
23 |
24 | this.update(resource.body, resource.rpath)
25 | }
26 |
27 | updateState (state) {
28 | if (state.test) {
29 | if (this.testTimout) {
30 | clearTimeout(this.testTimeout)
31 | }
32 | this.test = true
33 | this.testTimout = setTimeout(() => {
34 | delete this.testTimeout
35 | this.test = false
36 | }, 5000)
37 | }
38 | if (state.water) {
39 | this.values.leak = this.Characteristics.hap.LeakDetected.LEAK_DETECTED
40 | } else if (this.test) {
41 | this.values.leak = this.Characteristics.hap.LeakDetected.LEAK_DETECTED
42 | } else {
43 | this.values.leak = this.Characteristics.hap.LeakDetected.LEAK_NOT_DETECTED
44 | }
45 | super.updateState(state)
46 | }
47 | }
48 |
49 | DeconzService.Leak = Leak
50 |
--------------------------------------------------------------------------------
/lib/DeconzService/LightLevel.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/LightLevel.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/SensorsResource.js'
10 |
11 | const { lightLevelToLux } = ApiClient
12 |
13 | /**
14 | * @memberof DeconzService
15 | */
16 | class LightLevel extends DeconzService.SensorsResource {
17 | constructor (accessory, resource, params = {}) {
18 | params.Service = accessory.Services.hap.LightSensor
19 | super(accessory, resource, params)
20 |
21 | this.addCharacteristicDelegate({
22 | key: 'lightLevel',
23 | Characteristic: this.Characteristics.hap.CurrentAmbientLightLevel,
24 | unit: ' lux'
25 | })
26 |
27 | this.addCharacteristicDelegate({
28 | key: 'dark',
29 | Characteristic: this.Characteristics.my.Dark
30 | })
31 |
32 | this.addCharacteristicDelegate({
33 | key: 'daylight',
34 | Characteristic: this.Characteristics.my.Daylight
35 | })
36 |
37 | this.addCharacteristicDelegates()
38 |
39 | this.update(resource.body, resource.rpath)
40 | }
41 |
42 | updateState (state) {
43 | if (state.lightlevel != null) {
44 | this.values.lightLevel = lightLevelToLux(state.lightlevel)
45 | }
46 | if (state.dark != null) {
47 | this.values.dark = state.dark
48 | }
49 | if (state.daylight != null) {
50 | this.values.daylight = state.daylight
51 | }
52 | super.updateState(state)
53 | }
54 | }
55 |
56 | DeconzService.LightLevel = LightLevel
57 |
--------------------------------------------------------------------------------
/lib/DeconzService/LightsResource.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/LightsResource.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { timeout } from 'homebridge-lib'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 |
10 | class LightsResource extends DeconzService {
11 | constructor (accessory, resource, params) {
12 | super(accessory, resource, params)
13 | this.stateKey = resource.rtype === 'groups' ? 'action' : 'state'
14 | this.statePath = '/' + this.stateKey
15 |
16 | this.updating = 0
17 | this.targetState = {}
18 | }
19 |
20 | addCharacteristicDelegates (params = {}) {
21 | if (this.resource.rtype !== 'groups') {
22 | this.addCharacteristicDelegate({
23 | key: 'lastSeen',
24 | Characteristic: this.Characteristics.my.LastSeen,
25 | silent: true
26 | })
27 |
28 | this.addCharacteristicDelegate({
29 | key: 'statusFault',
30 | Characteristic: this.Characteristics.hap.StatusFault
31 | })
32 | }
33 | }
34 |
35 | updateState (state) {
36 | if (state.reachable != null) {
37 | this.values.statusFault = state.reachable
38 | ? this.Characteristics.hap.StatusFault.NO_FAULT
39 | : this.Characteristics.hap.StatusFault.GENERAL_FAULT
40 | }
41 | }
42 |
43 | updateConfig (config) {
44 | }
45 |
46 | async identify () {
47 | if (this.resource.body?.capabilities?.alerts?.includes('breathe')) {
48 | await this.put(this.statePath, { alert: 'breathe' })
49 | await timeout(1000)
50 | return this.put(this.statePath, { alert: 'finish' })
51 | }
52 | return this.put(this.statePath, { alert: 'select' })
53 | }
54 |
55 | // Collect state changes into a combined request.
56 | async putState (state) {
57 | for (const key in state) {
58 | this.resource.body[this.stateKey][key] = state[key]
59 | this.targetState[key] = state[key]
60 | }
61 | return this._putState()
62 | }
63 |
64 | // Send the request (for the combined state changes) to the gateway.
65 | async _putState () {
66 | try {
67 | if (this.platform.config.waitTimeUpdate > 0) {
68 | this.updating++
69 | await timeout(this.platform.config.waitTimeUpdate)
70 | if (--this.updating > 0) {
71 | return
72 | }
73 | }
74 | const targetState = this.targetState
75 | this.targetState = {}
76 | if (
77 | this.gateway.transitionTime !== this.gateway.defaultTransitionTime &&
78 | targetState.transitiontime === undefined
79 | ) {
80 | targetState.transitiontime = this.gateway.transitionTime * 10
81 | this.gateway.resetTransitionTime()
82 | }
83 | if (this.resource.body?.capabilities?.transition_block) {
84 | if (
85 | (
86 | targetState.on != null || targetState.bri != null ||
87 | targetState.bri_inc != null
88 | ) && (
89 | targetState.xy != null || targetState.ct != null ||
90 | targetState.hue != null || targetState.sat != null ||
91 | targetState.effect != null
92 | )
93 | ) {
94 | targetState.transitiontime = 0
95 | }
96 | }
97 | await this.put(this.statePath, targetState)
98 | this.recentlyUpdated = true
99 | await timeout(500)
100 | this.recentlyUpdated = false
101 | } catch (error) { this.warn(error) }
102 | }
103 | }
104 |
105 | DeconzService.LightsResource = LightsResource
106 |
--------------------------------------------------------------------------------
/lib/DeconzService/Motion.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Motion.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Motion extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.MotionSensor
15 | super(accessory, resource, params)
16 |
17 | this.durationKey = resource.body.config.delay != null ? 'delay' : 'duration'
18 | this.sensitivitymax = resource.body.config.sensitivitymax
19 |
20 | this.addCharacteristicDelegate({
21 | key: 'motion',
22 | Characteristic: this.Characteristics.hap.MotionDetected,
23 | value: false
24 | })
25 |
26 | if (resource.body.state.distance !== undefined) {
27 | this.addCharacteristicDelegate({
28 | key: 'distance',
29 | Characteristic: this.Characteristics.my.Distance,
30 | unit: ' cm',
31 | value: 0
32 | })
33 | }
34 |
35 | this.addCharacteristicDelegate({
36 | key: 'sensitivity',
37 | Characteristic: this.Characteristics.eve.Sensitivity
38 | }).on('didSet', async (value, fromHomeKit) => {
39 | if (fromHomeKit) {
40 | const sensitivity = value === this.Characteristics.eve.Sensitivity.HIGH
41 | ? this.sensitivitymax
42 | : value === this.Characteristics.eve.Sensitivity.LOW
43 | ? 0
44 | : Math.round(this.sensitivitymax / 2)
45 | await this.put('/config', { sensitivity })
46 | }
47 | })
48 |
49 | this.addCharacteristicDelegate({
50 | key: 'duration',
51 | Characteristic: this.Characteristics.eve.Duration,
52 | unit: 's'
53 | }).on('didSet', async (value, fromHomeKit) => {
54 | if (fromHomeKit) {
55 | const config = {}
56 | config[this.durationKey] =
57 | value === this.Characteristics.eve.Duration.VALID_VALUES[0] ? 0 : value
58 | await this.put('/config', config)
59 | }
60 | })
61 |
62 | if (resource.body.config.detectionrange !== undefined) {
63 | this.addCharacteristicDelegate({
64 | key: 'detectionRange',
65 | Characteristic: this.Characteristics.my.DetectionRange,
66 | unit: ' cm',
67 | value: 600
68 | }).on('didSet', async (value, fromHomeKit) => {
69 | if (fromHomeKit) {
70 | const detectionrange = Math.max(0, Math.min(value, 600))
71 | await this.putConfig({ detectionrange })
72 | }
73 | })
74 | }
75 |
76 | if (resource.body.config.resetpresence !== undefined) {
77 | this.addCharacteristicDelegate({
78 | key: 'reset',
79 | Characteristic: this.Characteristics.my.Reset,
80 | value: false
81 | }).on('didSet', async (value, fromHomeKit) => {
82 | if (fromHomeKit) {
83 | await this.put('/config', { resetpresence: value })
84 | }
85 | })
86 | }
87 |
88 | this.addCharacteristicDelegates()
89 |
90 | this.update(resource.body, resource.rpath)
91 | }
92 |
93 | updateState (state) {
94 | if (state.presence != null) {
95 | this.values.motion = state.presence
96 | }
97 | if (state.distance != null) {
98 | this.values.distance = state.distance
99 | }
100 | if (state.vibration != null) {
101 | this.values.motion = state.vibration
102 | }
103 | super.updateState(state)
104 | }
105 |
106 | updateConfig (config) {
107 | if (config[this.durationKey] != null) {
108 | let duration
109 | for (const value of this.Characteristics.eve.Duration.VALID_VALUES) {
110 | duration = value
111 | if (config[this.durationKey] <= value) {
112 | break
113 | }
114 | }
115 | this.values.duration = duration
116 | }
117 | if (config.sensitivity != null) {
118 | this.values.sensitivity = config.sensitivity === this.sensitivitymax
119 | ? this.Characteristics.eve.Sensitivity.HIGH
120 | : config.sensitivity === 0
121 | ? this.Characteristics.eve.Sensitivity.LOW
122 | : this.Characteristics.eve.Sensitivity.MEDIUM
123 | }
124 | if (config.detectionrange != null) {
125 | this.values.detectionRange = config.detectionrange
126 | }
127 | if (config.resetpresence != null) {
128 | this.values.reset = config.resetpresence
129 | }
130 | super.updateConfig(config)
131 | }
132 | }
133 |
134 | DeconzService.Motion = Motion
135 |
--------------------------------------------------------------------------------
/lib/DeconzService/Outlet.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Outlet.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/LightsResource.js'
8 |
9 | class Outlet extends DeconzService.LightsResource {
10 | constructor (accessory, resource, params = {}) {
11 | params.Service = accessory.Services.hap.Outlet
12 | super(accessory, resource, params)
13 |
14 | this.addCharacteristicDelegate({
15 | key: 'on',
16 | Characteristic: this.Characteristics.hap.On,
17 | value: this.capabilities.on
18 | ? this.resource.body.state.on
19 | : this.resource.body.state.all_on
20 | }).on('didSet', (value, fromHomeKit) => {
21 | if (fromHomeKit) {
22 | this.putState({ on: value })
23 | }
24 | })
25 |
26 | if (this.resource.body.state.on === undefined) {
27 | this.addCharacteristicDelegate({
28 | key: 'anyOn',
29 | Characteristic: this.Characteristics.my.AnyOn,
30 | value: this.resource.body.state.any_on
31 | }).on('didSet', (value, fromHomeKit) => {
32 | if (fromHomeKit) {
33 | this.putState({ on: value })
34 | }
35 | })
36 | }
37 |
38 | this.addCharacteristicDelegate({
39 | key: 'outletInUse',
40 | Characteristic: this.Characteristics.hap.OutletInUse,
41 | value: 1 // Eve interpretes OutletInUse as: device is physically plugged in.
42 | })
43 |
44 | if (this.resource.rtype === 'lights') {
45 | this.addCharacteristicDelegate({
46 | key: 'wallSwitch',
47 | value: false
48 | })
49 | }
50 |
51 | this.addCharacteristicDelegates()
52 | }
53 |
54 | updateState (state) {
55 | for (const key in state) {
56 | const value = state[key]
57 | this.resource.body.state[key] = value
58 | switch (key) {
59 | case 'all_on':
60 | this.values.on = value
61 | break
62 | case 'any_on':
63 | this.values.anyOn = value
64 | break
65 | case 'on':
66 | if (this.values.wallSwitch && !state.reachable) {
67 | if (this.values.on) {
68 | this.log('not reachable: force On to false')
69 | }
70 | this.values.on = false
71 | break
72 | }
73 | this.values.on = value
74 | break
75 | default:
76 | break
77 | }
78 | }
79 | super.updateState(state)
80 | }
81 | }
82 |
83 | DeconzService.Outlet = Outlet
84 |
--------------------------------------------------------------------------------
/lib/DeconzService/Power.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Power.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/SensorsResource.js'
10 |
11 | const { dateToString } = ApiClient
12 |
13 | /**
14 | * @memberof DeconzService
15 | */
16 | class Power extends DeconzService.SensorsResource {
17 | static addResource (service, resource) {
18 | if (service.values.consumption === undefined) {
19 | service.addCharacteristicDelegate({
20 | key: 'consumption',
21 | Characteristic: service.Characteristics.eve.Consumption,
22 | unit: ' W'
23 | })
24 | }
25 |
26 | if (resource.body.state.current !== undefined) {
27 | service.addCharacteristicDelegate({
28 | key: 'electricCurrent',
29 | Characteristic: service.Characteristics.eve.ElectricCurrent,
30 | unit: ' A'
31 | })
32 | }
33 |
34 | if (resource.body.state.voltage !== undefined) {
35 | service.addCharacteristicDelegate({
36 | key: 'voltage',
37 | Characteristic: service.Characteristics.eve.Voltage,
38 | unit: ' V'
39 | })
40 | }
41 |
42 | if (service.values.lastUpdated === undefined) {
43 | service.addCharacteristicDelegate({
44 | key: 'lastUpdated',
45 | Characteristic: service.Characteristics.my.LastUpdated,
46 | silent: true
47 | })
48 | }
49 |
50 | Power.updateResourceState(service, resource.body.state)
51 | }
52 |
53 | static updateResourceState (service, state) {
54 | if (state.power != null) {
55 | service.values.consumption = state.power
56 | }
57 | if (state.current != null) {
58 | service.values.electricCurrent = state.current / 1000
59 | }
60 | if (state.voltage != null) {
61 | service.values.voltage = state.voltage
62 | }
63 | if (state.lastupdated != null) {
64 | service.values.lastUpdated = dateToString(state.lastupdated)
65 | }
66 | }
67 |
68 | constructor (accessory, resource, params = {}) {
69 | params.name = accessory.name + ' Consumption'
70 | params.Service = accessory.Services.eve.Consumption
71 | params.exposeConfiguredName = true
72 | super(accessory, resource, params)
73 |
74 | Power.addResource(this, resource)
75 |
76 | super.addCharacteristicDelegates({ noLastUpdated: true })
77 |
78 | this.update(resource.body, resource.rpath)
79 | }
80 |
81 | updateState (state) {
82 | Power.updateResourceState(this, state)
83 | super.updateState(state)
84 | }
85 | }
86 |
87 | DeconzService.Power = Power
88 |
--------------------------------------------------------------------------------
/lib/DeconzService/Schedule.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Schedule.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
9 |
10 | import { DeconzService } from '../DeconzService/index.js'
11 |
12 | const { HttpError } = ApiClient
13 |
14 | /**
15 | * @memberof DeconzService
16 | */
17 | class Schedule extends ServiceDelegate {
18 | constructor (accessory, rid, body) {
19 | super(accessory, {
20 | id: accessory.gateway.id + '-T' + rid,
21 | name: body.name,
22 | Service: accessory.Services.my.Resource,
23 | subtype: 'T' + rid,
24 | exposeConfiguredName: true
25 | })
26 | this.id = accessory.gateway.id + '-T' + rid
27 | this.gateway = accessory.gateway
28 | this.accessory = accessory
29 | this.client = accessory.client
30 | this.rtype = 'schedules'
31 | this.rid = rid
32 | this.rpath = '/' + this.rtype + '/' + this.rid
33 |
34 | this.addCharacteristicDelegate({
35 | key: 'enabled',
36 | Characteristic: this.Characteristics.my.Enabled
37 | }).on('didSet', async (value, fromHomeKit) => {
38 | await this.put({ status: value ? 'enabled' : 'disabled' })
39 | this.values.statusActive = value
40 | })
41 |
42 | this.addCharacteristicDelegate({
43 | key: 'statusActive',
44 | Characteristic: this.Characteristics.hap.StatusActive
45 | })
46 |
47 | // this.addCharacteristicDelegate({
48 | // key: 'index',
49 | // Characteristic: this.Characteristics.hap.ServiceLabelIndex,
50 | // value: rid
51 | // })
52 | }
53 |
54 | update (body) {
55 | this.values.enabled = body.status === 'enabled'
56 | this.values.statusActive = this.values.enabled
57 | }
58 |
59 | async put (body) {
60 | try {
61 | await this.client.put(this.rpath, body)
62 | } catch (error) {
63 | if (!(error instanceof HttpError)) {
64 | this.warn(error)
65 | }
66 | }
67 | }
68 | }
69 |
70 | DeconzService.Schedule = Schedule
71 |
--------------------------------------------------------------------------------
/lib/DeconzService/SensorsResource.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/SensorsResource.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { timeout } from 'homebridge-lib'
7 |
8 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
9 |
10 | import { DeconzService } from '../DeconzService/index.js'
11 |
12 | const { dateToString } = ApiClient
13 |
14 | /**
15 | * @memberof DeconzService
16 | */
17 | class SensorsResource extends DeconzService {
18 | constructor (accessory, resource, params) {
19 | super(accessory, resource, params)
20 |
21 | this.updating = 0
22 | this.targetConfig = {}
23 | }
24 |
25 | addCharacteristicDelegates (params = {}) {
26 | if (!params.noLastUpdated) {
27 | this.addCharacteristicDelegate({
28 | key: 'lastUpdated',
29 | Characteristic: this.Characteristics.my.LastUpdated,
30 | silent: true
31 | })
32 | }
33 |
34 | this.addCharacteristicDelegate({
35 | key: 'enabled',
36 | Characteristic: this.Characteristics.my.Enabled
37 | }).on('didSet', async (value, fromHomeKit) => {
38 | this.values.statusActive = value
39 | if (fromHomeKit) {
40 | await this.put('/config', { on: value })
41 | }
42 | })
43 |
44 | this.addCharacteristicDelegate({
45 | key: 'statusActive',
46 | Characteristic: this.Characteristics.hap.StatusActive
47 | })
48 |
49 | if (this.resource.body.config.reachable !== undefined) {
50 | this.addCharacteristicDelegate({
51 | key: 'statusFault',
52 | Characteristic: this.Characteristics.hap.StatusFault
53 | })
54 | }
55 |
56 | if (this.resource.body.state.tampered !== undefined && !params.noTampered) {
57 | this.addCharacteristicDelegate({
58 | key: 'tampered',
59 | Characteristic: this.Characteristics.hap.StatusTampered
60 | })
61 | }
62 | }
63 |
64 | updateState (state) {
65 | if (state.lastupdated != null) {
66 | this.values.lastUpdated = dateToString(state.lastupdated)
67 | }
68 | if (state.tampered != null) {
69 | this.values.tampered = state.tampered
70 | ? this.Characteristics.hap.StatusTampered.TAMPERED
71 | : this.Characteristics.hap.StatusTampered.NOT_TAMPERED
72 | }
73 | }
74 |
75 | updateConfig (config) {
76 | if (config.on != null) {
77 | this.values.enabled = !!config.on
78 | }
79 | if (config.reachable != null) {
80 | this.values.statusFault = config.reachable
81 | ? this.Characteristics.hap.StatusFault.NO_FAULT
82 | : this.Characteristics.hap.StatusFault.GENERAL_FAULT
83 | }
84 | }
85 |
86 | async identify () {
87 | if (this.resource.body.config.alert) {
88 | return this.put('/config', { alert: 'select' })
89 | }
90 | }
91 |
92 | // Collect config changes into a combined request.
93 | async putConfig (config) {
94 | for (const key in config) {
95 | this.resource.body.config[key] = config[key]
96 | this.targetConfig[key] = config[key]
97 | }
98 | return this._putConfig()
99 | }
100 |
101 | // Send the request (for the combined config changes) to the gateway.
102 | async _putConfig () {
103 | try {
104 | if (this.platform.config.waitTimeUpdate > 0) {
105 | this.updating++
106 | await timeout(this.platform.config.waitTimeUpdate)
107 | if (--this.updating > 0) {
108 | return
109 | }
110 | }
111 | const targetConfig = this.targetConfig
112 | this.targetConfig = {}
113 | await this.put('/config', targetConfig)
114 | // this.recentlyUpdated = true
115 | // await timeout(500)
116 | // this.recentlyUpdated = false
117 | } catch (error) { this.warn(error) }
118 | }
119 | }
120 |
121 | DeconzService.SensorsResource = SensorsResource
122 |
--------------------------------------------------------------------------------
/lib/DeconzService/Smoke.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Smoke.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Smoke extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.SmokeSensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'smoke',
19 | Characteristic: this.Characteristics.hap.SmokeDetected
20 | })
21 |
22 | this.addCharacteristicDelegate({
23 | key: 'deviceStatus',
24 | Characteristic: this.Characteristics.eve.ElgatoDeviceStatus
25 | })
26 |
27 | super.addCharacteristicDelegates(params)
28 |
29 | this.update(resource.body, resource.rpath)
30 | }
31 |
32 | updateState (state) {
33 | if (state.test) {
34 | if (this.testTimout) {
35 | clearTimeout(this.testTimeout)
36 | }
37 | this.test = true
38 | this.testTimout = setTimeout(() => {
39 | delete this.testTimeout
40 | this.test = false
41 | }, 5000)
42 | }
43 | let status = 0
44 | if (state.fire) {
45 | this.values.smoke = this.Characteristics.hap.SmokeDetected.SMOKE_DETECTED
46 | status |= this.Characteristics.eve.ElgatoDeviceStatus.SMOKE_DETECTED
47 | } else if (this.test) {
48 | this.values.smoke = this.Characteristics.hap.SmokeDetected.SMOKE_DETECTED
49 | status |= this.Characteristics.eve.ElgatoDeviceStatus.ALARM_TEST_ACTIVE
50 | } else {
51 | this.values.smoke = this.Characteristics.hap.SmokeDetected.SMOKE_NOT_DETECTED
52 | }
53 | this.values.deviceStatus = status
54 | super.updateState(state)
55 | }
56 | }
57 |
58 | DeconzService.Smoke = Smoke
59 |
--------------------------------------------------------------------------------
/lib/DeconzService/Status.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Status.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Status extends DeconzService.SensorsResource {
13 | props (caps) {
14 | if (caps.min == null || caps.max == null) {
15 | return undefined
16 | }
17 | // Eve 3.1 displays the following controls, depending on the properties:
18 | // 1. {minValue: 0, maxValue: 1, minStep: 1} switch
19 | // 2. {minValue: a, maxValue: b, minStep: 1}, 1 < b - a <= 20 down|up
20 | // 3. {minValue: a, maxValue: b}, (a, b) != (0, 1) slider
21 | // 4. {minValue: a, maxValue: b, minStep: 1}, b - a > 20 slider
22 | // Avoid the following bugs:
23 | // 5. {minValue: 0, maxValue: 1} nothing
24 | // 6. {minValue: a, maxValue: b, minStep: 1}, b - a = 1 switch*
25 | // *) switch sends values 0 and 1 instead of a and b;
26 | if (caps.min === 0 && caps.max === 1) {
27 | // Workaround Eve bug (case 5 above).
28 | return { minValue: caps.min, maxValue: caps.max, minStep: 1 }
29 | }
30 | if (caps.max - caps.min === 1) {
31 | // Workaround Eve bug (case 6 above).
32 | return { minValue: caps.min, maxValue: caps.max }
33 | }
34 | return { minValue: caps.min, maxValue: caps.max, minStep: 1 }
35 | }
36 |
37 | constructor (accessory, resource, params = {}) {
38 | params.Service = accessory.Services.my.Status
39 | super(accessory, resource, params)
40 |
41 | if (resource.capabilities.readonly) {
42 | this.addCharacteristicDelegate({
43 | key: 'status',
44 | Characteristic: this.Characteristics.my.Status,
45 | props: {
46 | perms: [
47 | this.Characteristic.Perms.PAIRED_READ, this.Characteristic.Perms.NOTIFY
48 | ]
49 | }
50 | })
51 | } else {
52 | this.addCharacteristicDelegate({
53 | key: 'status',
54 | Characteristic: this.Characteristics.my.Status,
55 | props: this.props(resource.capabilities)
56 | }).on('didSet', async (value, fromHomeKit) => {
57 | if (fromHomeKit) {
58 | await this.put('/state', { status: value })
59 | }
60 | })
61 | }
62 |
63 | this.addCharacteristicDelegates()
64 |
65 | this.update(resource.body, resource.rpath)
66 | }
67 |
68 | updateState (state) {
69 | if (state.status != null) {
70 | this.values.status = state.status
71 | }
72 | super.updateState(state)
73 | }
74 | }
75 |
76 | DeconzService.Status = Status
77 |
--------------------------------------------------------------------------------
/lib/DeconzService/Switch.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Switch.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/LightsResource.js'
8 |
9 | class Switch extends DeconzService.LightsResource {
10 | constructor (accessory, resource, params = {}) {
11 | params.Service = accessory.Services.hap.Switch
12 | super(accessory, resource, params)
13 |
14 | this.addCharacteristicDelegate({
15 | key: 'on',
16 | Characteristic: this.Characteristics.hap.On,
17 | value: this.capabilities.on
18 | ? this.resource.body.state.on
19 | : this.resource.body.state.all_on
20 | }).on('didSet', (value, fromHomeKit) => {
21 | if (fromHomeKit) {
22 | this.putState({ on: value })
23 | }
24 | })
25 |
26 | if (this.resource.body.state.on === undefined) {
27 | this.addCharacteristicDelegate({
28 | key: 'anyOn',
29 | Characteristic: this.Characteristics.my.AnyOn,
30 | value: this.resource.body.state.any_on
31 | }).on('didSet', (value, fromHomeKit) => {
32 | if (fromHomeKit) {
33 | this.putState({ on: value })
34 | }
35 | })
36 | }
37 |
38 | if (this.resource.rtype === 'lights') {
39 | this.addCharacteristicDelegate({
40 | key: 'wallSwitch',
41 | value: false
42 | })
43 | }
44 |
45 | this.addCharacteristicDelegates()
46 | }
47 |
48 | updateState (state) {
49 | for (const key in state) {
50 | const value = state[key]
51 | this.resource.body.state[key] = value
52 | switch (key) {
53 | case 'all_on':
54 | this.values.on = value
55 | break
56 | case 'any_on':
57 | this.values.anyOn = value
58 | break
59 | case 'on':
60 | if (this.values.wallSwitch && !state.reachable) {
61 | if (this.values.on) {
62 | this.log('not reachable: force On to false')
63 | }
64 | this.values.on = false
65 | break
66 | }
67 | this.values.on = value
68 | break
69 | default:
70 | break
71 | }
72 | }
73 | super.updateState(state)
74 | }
75 | }
76 |
77 | DeconzService.Switch = Switch
78 |
--------------------------------------------------------------------------------
/lib/DeconzService/Temperature.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Temperature.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Temperature extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.TemperatureSensor
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'temperature',
19 | Characteristic: this.Characteristics.hap.CurrentTemperature,
20 | unit: '°C',
21 | props: {
22 | minValue: Math.round(resource.body.capabilities?.measured_value?.min ?? -40),
23 | maxValue: Math.round(resource.body.capabilities?.measured_value?.max ?? 100),
24 | minStep: 0.1
25 | },
26 | value: 0
27 | })
28 |
29 | this.addCharacteristicDelegate({
30 | key: 'offset',
31 | Characteristic: this.Characteristics.my.Offset,
32 | unit: '°C',
33 | props: { minValue: -5, maxValue: 5, minStep: 0.1 },
34 | value: 0
35 | }).on('didSet', async (value, fromHomeKit) => {
36 | if (fromHomeKit) {
37 | await this.put('/config', { offset: Math.round(value * 100) })
38 | }
39 | })
40 |
41 | this.addCharacteristicDelegate({
42 | key: 'displayUnits',
43 | Characteristic: this.Characteristics.hap.TemperatureDisplayUnits,
44 | value: this.Characteristics.hap.TemperatureDisplayUnits.CELSIUS
45 | })
46 |
47 | this.addCharacteristicDelegates()
48 |
49 | this.update(resource.body, resource.rpath)
50 | }
51 |
52 | updateState (state) {
53 | if (state.measured_value != null) {
54 | this.values.temperature = Math.round(state.measured_value * 10) / 10
55 | } else if (state.temperature != null) {
56 | this.values.temperature = Math.round(state.temperature / 10) / 10
57 | }
58 | super.updateState(state)
59 | }
60 |
61 | updateConfig (config) {
62 | if (config.offset != null) {
63 | this.values.offset = Math.round(config.offset / 10) / 10
64 | }
65 | super.updateConfig(config)
66 | }
67 | }
68 |
69 | DeconzService.Temperature = Temperature
70 |
--------------------------------------------------------------------------------
/lib/DeconzService/Thermostat.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Thermostat.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/SensorsResource.js'
8 |
9 | /**
10 | * @memberof DeconzService
11 | */
12 | class Thermostat extends DeconzService.SensorsResource {
13 | constructor (accessory, resource, params = {}) {
14 | params.Service = accessory.Services.hap.Thermostat
15 | super(accessory, resource, params)
16 |
17 | this.addCharacteristicDelegate({
18 | key: 'currentTemperature',
19 | Characteristic: this.Characteristics.hap.CurrentTemperature,
20 | unit: '°C',
21 | props: { minValue: -40, maxValue: 100, minStep: 0.1 },
22 | value: 0
23 | })
24 |
25 | this.addCharacteristicDelegate({
26 | key: 'targetTemperature',
27 | Characteristic: this.Characteristics.hap.TargetTemperature,
28 | unit: '°C',
29 | props: { minValue: 5, maxValue: 30, minStep: 0.5 },
30 | value: 0
31 | }).on('didSet', async (value, fromHomeKit) => {
32 | if (fromHomeKit) {
33 | await this.put('/config', { heatsetpoint: Math.round(value * 100) })
34 | }
35 | })
36 |
37 | if (resource.body.state.valve !== undefined) {
38 | this.addCharacteristicDelegate({
39 | key: 'valvePosition',
40 | Characteristic: this.Characteristics.eve.ValvePosition,
41 | unit: '%'
42 | })
43 | }
44 |
45 | this.addCharacteristicDelegate({
46 | key: 'currentState',
47 | Characteristic: this.Characteristics.hap.CurrentHeatingCoolingState,
48 | props: {
49 | validValues: [
50 | this.Characteristics.hap.CurrentHeatingCoolingState.OFF,
51 | this.Characteristics.hap.CurrentHeatingCoolingState.HEAT
52 | ]
53 | }
54 | })
55 |
56 | this.addCharacteristicDelegate({
57 | key: 'targetState',
58 | Characteristic: this.Characteristics.hap.TargetHeatingCoolingState,
59 | props: {
60 | validValues: [
61 | this.Characteristics.hap.TargetHeatingCoolingState.OFF,
62 | this.Characteristics.hap.TargetHeatingCoolingState.HEAT
63 | ]
64 | }
65 | }).on('didSet', async (value, fromHomeKit) => {
66 | if (fromHomeKit) {
67 | await this.put('/config', {
68 | mode: value === this.Characteristics.hap.TargetHeatingCoolingState.OFF
69 | ? 'off'
70 | : this.capabilities.heatValue
71 | })
72 | }
73 | })
74 |
75 | this.addCharacteristicDelegate({
76 | key: 'offset',
77 | Characteristic: this.Characteristics.my.Offset,
78 | unit: '°C',
79 | props: { minValue: -5, maxValue: 5, minStep: 0.1 },
80 | value: 0
81 | }).on('didSet', async (value, fromHomeKit) => {
82 | if (fromHomeKit) {
83 | await this.put('/config', { offset: Math.round(value * 100) })
84 | }
85 | })
86 |
87 | this.addCharacteristicDelegate({
88 | key: 'displayUnits',
89 | Characteristic: this.Characteristics.hap.TemperatureDisplayUnits,
90 | value: this.Characteristics.hap.TemperatureDisplayUnits.CELSIUS
91 | })
92 |
93 | this.addCharacteristicDelegate({
94 | key: 'programData',
95 | Characteristic: this.Characteristics.eve.ProgramData,
96 | silent: true,
97 | value: Buffer.from('ff04f6', 'hex').toString('base64')
98 | })
99 |
100 | this.addCharacteristicDelegate({
101 | key: 'programCommand',
102 | Characteristic: this.Characteristics.eve.ProgramCommand,
103 | silent: true
104 | })
105 |
106 | if (resource.body.config.displayflipped !== undefined) {
107 | this.addCharacteristicDelegate({
108 | key: 'imageMirroring',
109 | Characteristic: this.Characteristics.hap.ImageMirroring
110 | }).on('didSet', async (value, fromHomeKit) => {
111 | if (fromHomeKit) {
112 | await this.put('/config', { displayflipped: value })
113 | }
114 | })
115 | }
116 | if (resource.body.config.externalsensortemp !== undefined) {
117 | this.addCharacteristicDelegate({
118 | key: 'useExternalTemperature',
119 | value: false,
120 | silent: true
121 | })
122 | }
123 |
124 | if (resource.body.config.locked !== undefined) {
125 | this.addCharacteristicDelegate({
126 | key: 'lockPhysicalControls',
127 | Characteristic: this.Characteristics.hap.LockPhysicalControls
128 | }).on('didSet', async (value, fromHomeKit) => {
129 | if (fromHomeKit) {
130 | await this.put('/config', {
131 | locked: value === this.Characteristics.hap.LockPhysicalControls
132 | .CONTROL_LOCK_ENABLED
133 | })
134 | }
135 | })
136 | }
137 |
138 | super.addCharacteristicDelegates()
139 |
140 | this.update(resource.body, resource.rpath)
141 | }
142 |
143 | updateState (state) {
144 | if (state.on != null) {
145 | this.values.currentState = state.on
146 | ? this.Characteristics.hap.CurrentHeatingCoolingState.HEAT
147 | : this.Characteristics.hap.CurrentHeatingCoolingState.OFF
148 | }
149 | if (!this.values.useExternalTemperature && state.temperature != null) {
150 | this.values.currentTemperature = Math.round(state.temperature / 10) / 10
151 | }
152 | if (state.valve != null) {
153 | this.values.valvePosition = state.valve
154 | }
155 | super.updateState(state)
156 | }
157 |
158 | updateConfig (config) {
159 | if (config.displayflipped != null) {
160 | this.values.imageMirroring = config.displayflipped
161 | }
162 | if (this.values.useExternalTemperature && config.externalsensortemp != null) {
163 | this.values.currentTemperature = Math.round(config.externalsensortemp / 10) / 10
164 | }
165 | if (config.heatsetpoint != null) {
166 | this.values.targetTemperature = Math.round(config.heatsetpoint / 50) / 2
167 | }
168 | if (config.locked != null) {
169 | this.values.lockPhysicalControls = config.locked
170 | ? this.Characteristics.hap.LockPhysicalControls.CONTROL_LOCK_ENABLED
171 | : this.Characteristics.hap.LockPhysicalControls.CONTROL_LOCK_DISABLED
172 | }
173 | if (config.mode != null) {
174 | this.values.targetState = config.mode === 'off'
175 | ? this.Characteristics.hap.TargetHeatingCoolingState.OFF
176 | : this.Characteristics.hap.TargetHeatingCoolingState.HEAT
177 | }
178 | if (config.offset != null) {
179 | this.values.offset = Math.round(config.offset / 10) / 10
180 | }
181 | super.updateConfig(config)
182 | }
183 | }
184 |
185 | DeconzService.Thermostat = Thermostat
186 |
--------------------------------------------------------------------------------
/lib/DeconzService/Valve.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/Valve.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { DeconzService } from '../DeconzService/index.js'
7 | import '../DeconzService/LightsResource.js'
8 |
9 | class Valve extends DeconzService.LightsResource {
10 | constructor (accessory, resource, params = {}) {
11 | params.Service = accessory.Services.hap.Valve
12 | super(accessory, resource, params)
13 |
14 | this.addCharacteristicDelegate({
15 | key: 'active',
16 | Characteristic: this.Characteristics.hap.Active,
17 | value: this.capabilities.on
18 | ? this.resource.body.state.on
19 | : this.resource.body.state.all_on
20 | }).on('didSet', async (value, fromHomeKit) => {
21 | try {
22 | if (fromHomeKit) {
23 | await this.put(this.statePath, { on: value === this.Characteristics.hap.Active.ACTIVE })
24 | }
25 | if (this.values.active) {
26 | this.values.inUse = this.Characteristics.hap.InUse.IN_USE
27 | if (this.values.setDuration > 0) {
28 | this.values.remainingDuration = this.values.setDuration
29 | this.autoInActive = new Date().valueOf() + this.values.setDuration * 1000
30 | this.autoInActiveTimeout = setTimeout(async () => {
31 | try {
32 | await this.put(this.statePath, { on: false })
33 | } catch (error) { this.warn(error) }
34 | }, this.values.setDuration * 1000)
35 | }
36 | } else {
37 | this.values.inUse = this.Characteristics.hap.InUse.NOT_IN_USE
38 | if (this.autoInActiveTimeout != null) {
39 | clearTimeout(this.autoInActiveTimeout)
40 | delete this.autoInActiveTimeout
41 | delete this.autoInActive
42 | this.values.remainingDuration = 0
43 | }
44 | }
45 | } catch (error) { this.warn(error) }
46 | })
47 |
48 | this.addCharacteristicDelegate({
49 | key: 'inUse',
50 | Characteristic: this.Characteristics.hap.InUse,
51 | value: this.Characteristics.hap.InUse.NOT_IN_USE
52 | })
53 |
54 | this.addCharacteristicDelegate({
55 | key: 'remainingDuration',
56 | Characteristic: this.Characteristics.hap.RemainingDuration,
57 | value: 0,
58 | props: {
59 | maxValue: 4 * 3600
60 | },
61 | getter: async () => {
62 | const remaining = this.autoInActive - new Date().valueOf()
63 | return remaining > 0 ? Math.round(remaining / 1000) : 0
64 | }
65 | })
66 |
67 | this.addCharacteristicDelegate({
68 | key: 'setDuration',
69 | Characteristic: this.Characteristics.hap.SetDuration,
70 | value: 300,
71 | props: {
72 | maxValue: 4 * 3600
73 | }
74 | })
75 |
76 | this.addCharacteristicDelegate({
77 | key: 'valveType',
78 | Characteristic: this.Characteristics.hap.ValveType,
79 | value: this.Characteristics.hap.ValveType.GENERIC_VALVE
80 | })
81 |
82 | if (this.resource.rtype === 'lights') {
83 | this.addCharacteristicDelegate({
84 | key: 'wallSwitch',
85 | value: false
86 | })
87 | }
88 |
89 | this.addCharacteristicDelegates()
90 |
91 | this.values.active = this.Characteristics.hap.Active.INACTIVE
92 | }
93 |
94 | updateState (state) {
95 | for (const key in state) {
96 | const value = state[key]
97 | this.resource.body.state[key] = value
98 | switch (key) {
99 | case 'on':
100 | if (this.values.wallSwitch && !state.reachable) {
101 | this.log('not reachable: force Active to false')
102 | this.values.active = this.Characteristics.hap.Active.INACTIVE
103 | this.values.inUse = this.Characteristics.hap.InUse.NOT_IN_USE
104 | break
105 | }
106 | // falls through
107 | case 'all_on':
108 | this.values.active = value
109 | ? this.Characteristics.hap.Active.ACTIVE
110 | : this.Characteristics.hap.Active.INACTIVE
111 | this.values.inUse = value
112 | ? this.Characteristics.hap.InUse.IN_USE
113 | : this.Characteristics.hap.InUse.NOT_IN_USE
114 | break
115 | default:
116 | break
117 | }
118 | }
119 | super.updateState(state)
120 | }
121 | }
122 |
123 | DeconzService.Valve = Valve
124 |
--------------------------------------------------------------------------------
/lib/DeconzService/WarningDevice.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/WarningDevice.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { timeout } from 'homebridge-lib'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/LightsResource.js'
10 |
11 | class WarningDevice extends DeconzService.LightsResource {
12 | constructor (accessory, resource, params = {}) {
13 | params.Service = accessory.Services.hap.Switch
14 | super(accessory, resource, params)
15 |
16 | this.addCharacteristicDelegate({
17 | key: 'on',
18 | Characteristic: this.Characteristics.hap.On,
19 | value: false
20 | }).on('didSet', async (value, fromHomeKit) => {
21 | if (fromHomeKit) {
22 | const onTime = this.values.duration > 0 ? this.values.duration : 1
23 | let body = { alert: 'none' }
24 | if (value) {
25 | if (this.values.mute) {
26 | body = { alert: 'blink', ontime: onTime }
27 | } else if (this.values.duration === 0) {
28 | body = { alert: 'select' }
29 | } else {
30 | body = { alert: 'lselect', ontime: onTime }
31 | }
32 | }
33 | await this.put(this.statePath, body)
34 | if (value) {
35 | await timeout(onTime * 1000)
36 | this.values.on = false
37 | }
38 | }
39 | })
40 |
41 | this.addCharacteristicDelegate({
42 | key: 'duration',
43 | Characteristic: this.Characteristics.hap.SetDuration
44 | })
45 |
46 | this.addCharacteristicDelegate({
47 | key: 'mute',
48 | Characteristic: this.Characteristics.hap.Mute
49 | })
50 |
51 | this.addCharacteristicDelegates()
52 |
53 | this.update(resource.body, resource.rpath)
54 | }
55 |
56 | updateState (state) {
57 | if (state.on != null) {
58 | this.values.on = state.on
59 | }
60 | super.updateState(state)
61 | }
62 | }
63 |
64 | DeconzService.WarningDevice = WarningDevice
65 |
--------------------------------------------------------------------------------
/lib/DeconzService/WindowCovering.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/WindowCovering.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { timeout } from 'homebridge-lib'
7 |
8 | import { DeconzService } from '../DeconzService/index.js'
9 | import '../DeconzService/LightsResource.js'
10 |
11 | class WindowCovering extends DeconzService.LightsResource {
12 | constructor (accessory, resource, params = {}) {
13 | params.Service = accessory.Services.hap.WindowCovering
14 | super(accessory, resource, params)
15 |
16 | this.addCharacteristicDelegate({
17 | key: 'venetianBlind',
18 | value: false,
19 | silent: true
20 | })
21 |
22 | this.addCharacteristicDelegate({
23 | key: 'currentPosition',
24 | Characteristic: this.Characteristics.hap.CurrentPosition,
25 | unit: '%'
26 | })
27 |
28 | this.addCharacteristicDelegate({
29 | key: 'targetPosition',
30 | Characteristic: this.Characteristics.hap.TargetPosition,
31 | unit: '%'
32 | }).on('didSet', async (value, fromHomeKit) => {
33 | if (!fromHomeKit) {
34 | return
35 | }
36 | this.values.targetPosition = Math.round(this.values.targetPosition / 5) * 5
37 | return this.setPosition()
38 | })
39 |
40 | this.addCharacteristicDelegate({
41 | key: 'positionState',
42 | Characteristic: this.Characteristics.hap.PositionState,
43 | value: this.Characteristics.hap.PositionState.STOPPED
44 | })
45 |
46 | this.addCharacteristicDelegate({
47 | key: 'holdPosition',
48 | Characteristic: this.Characteristics.hap.HoldPosition
49 | }).on('didSet', async () => {
50 | await this.put(this.statePath, { stop: true })
51 | this.values.positionState = this.Characteristics.hap.PositionState.STOPPED
52 | })
53 |
54 | if (this.values.venetianBlind) {
55 | this.addCharacteristicDelegate({
56 | key: 'closeUpwards',
57 | Characteristic: this.Characteristics.my.CloseUpwards
58 | }).on('didSet', async (value, fromHomeKit) => {
59 | if (!fromHomeKit) {
60 | return
61 | }
62 | if (this.values.currentPosition !== 100) {
63 | return this.setPosition()
64 | }
65 | })
66 | }
67 |
68 | if (resource.capabilities.maxSpeed != null) {
69 | this.addCharacteristicDelegate({
70 | key: 'motorSpeed',
71 | Characteristic: this.Characteristics.my.MotorSpeed,
72 | unit: '',
73 | props: {
74 | unit: '',
75 | minValue: 0,
76 | maxValue: resource.capabilities.maxSpeed,
77 | minStep: 1
78 | }
79 | }).on('didSet', async (value, fromHomeKit) => {
80 | if (!fromHomeKit) {
81 | return
82 | }
83 | await this.put('/config', { speed: value })
84 | })
85 | }
86 |
87 | if (resource.capabilities.positionChange) {
88 | this.addCharacteristicDelegate({
89 | key: 'positionChange',
90 | Characteristic: this.Characteristics.my.PositionChange
91 | }).on('didSet', async (value) => {
92 | if (value !== 0) {
93 | await this.put(this.statePath, { lift_inc: -value })
94 | await timeout(this.platform.config.waitTimeReset)
95 | this.values.positionChange = 0
96 | }
97 | })
98 | this.values.positionChange = 0
99 | }
100 |
101 | this.addCharacteristicDelegates()
102 |
103 | this.update(resource.body, resource.rpath)
104 | this.values.targetPosition = this.values.currentPosition
105 | }
106 |
107 | async setPosition () {
108 | let lift = 100 - this.values.targetPosition // % closed --> % open
109 | if (this.values.venetianBlind) {
110 | if (this.values.closeUpwards) {
111 | lift *= -1
112 | }
113 | lift += 100
114 | lift /= 2
115 | lift = Math.round(lift)
116 | this.targetCloseUpwards = this.values.closeUpwards
117 | }
118 | this.values.positionState =
119 | this.values.targetPosition > this.values.currentPosition
120 | ? this.Characteristics.hap.PositionState.INCREASING
121 | : this.Characteristics.hap.PositionState.DECREASING
122 | this.moving = new Date()
123 | if (
124 | this.resource.capabilities.useOpen &&
125 | (this.lift === 0 || lift === 100)
126 | ) {
127 | return this.put(this.statePath, { open: lift === 0 })
128 | }
129 | return this.put(this.statePath, { lift })
130 | }
131 |
132 | updateState (state) {
133 | if (state.lift != null) {
134 | let position = Math.round(state.lift / 5) * 5
135 | let closeUpwards
136 | if (this.values.venetianBlind) {
137 | position *= 2
138 | position -= 100
139 | if (position < 0) {
140 | position *= -1
141 | closeUpwards = true
142 | } else if (position > 0) {
143 | closeUpwards = false
144 | }
145 | }
146 | position = 100 - position // % open -> % closed
147 | this.values.currentPosition = position
148 | if (closeUpwards != null) {
149 | this.values.closeUpwards = closeUpwards
150 | }
151 | if (
152 | this.moving == null || new Date() - this.moving >= 30000 || (
153 | position === this.values.targetPosition &&
154 | (closeUpwards == null || closeUpwards === this.targetCloseUpwards)
155 | )
156 | ) {
157 | this.moving = null
158 | this.values.targetPosition = position
159 | this.values.positionState = this.Characteristics.hap.PositionState.STOPPED
160 | }
161 | }
162 | if (state.speed != null) {
163 | this.values.motorSpeed = state.speed
164 | }
165 | super.updateState(state)
166 | }
167 | }
168 |
169 | DeconzService.WindowCovering = WindowCovering
170 |
--------------------------------------------------------------------------------
/lib/DeconzService/index.js:
--------------------------------------------------------------------------------
1 | // homebridge-deconz/lib/DeconzService/index.js
2 | // Copyright © 2022-2025 Erik Baauw. All rights reserved.
3 | //
4 | // Homebridge plugin for deCONZ.
5 |
6 | import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
7 |
8 | import { ApiClient } from 'hb-deconz-tools/ApiClient'
9 |
10 | const { HttpError, dateToString } = ApiClient
11 |
12 | /** Service delegates.
13 | * @extends ServiceDelegate
14 | */
15 | class DeconzService extends ServiceDelegate {
16 | constructor (accessory, resource, params) {
17 | super(accessory, {
18 | id: resource.id,
19 | name: params.name ?? resource.body.name,
20 | Service: params.Service,
21 | subtype: resource.subtype,
22 | primaryService: params.primaryService,
23 | exposeConfiguredName: true
24 | })
25 | this.id = resource.id
26 | this.gateway = accessory.gateway
27 | this.accessory = accessory
28 | this.client = accessory.client
29 | this.resource = resource
30 | this.rtype = resource.rtype
31 | this.rid = resource.rid
32 | this.rpath = resource.rpath
33 | this.capabilities = resource.capabilities
34 |
35 | this.serviceNameByRpath = {}
36 |
37 | // this.characteristicDelegate('configuredName')
38 | // .on('didSet', async (value, fromHomeKit) => {
39 | // if (fromHomeKit && value != null && value !== '') {
40 | // this.debug('PUT %s %j', this.rpath, { name: value })
41 | // await this.client.put(this.rpath, { name: value })
42 | // }
43 | // })
44 | }
45 |
46 | addResource (resource) {
47 | this.serviceNameByRpath[resource.rpath] = resource.serviceName
48 | DeconzService[resource.serviceName].addResource(this, resource)
49 | }
50 |
51 | update (body, rpath) {
52 | if (this.updating) {
53 | return
54 | }
55 | const serviceName = this.serviceNameByRpath[rpath]
56 | if (serviceName != null) {
57 | if (body.state != null) {
58 | DeconzService[serviceName].updateResourceState(this, body.state)
59 | }
60 | return
61 | }
62 | // if (body.name != null) {
63 | // this.values.configuredName = body.name.slice(0, 31).trim()
64 | // }
65 | if (body.lastseen != null && this.rtype === 'lights') {
66 | this.values.lastSeen = dateToString(body.lastseen)
67 | }
68 | if (body.config != null) {
69 | this.updateConfig(body.config)
70 | if (this.batteryService != null) {
71 | this.batteryService.updateConfig(body.config)
72 | }
73 | }
74 | if (body.state != null) {
75 | this.updateState(body.state, rpath)
76 | }
77 | if (this.rtype === 'groups') {
78 | if (body.action != null) {
79 | this.updateState(body.action, rpath, 'action')
80 | }
81 | if (body.scenes != null) {
82 | this.updateScenes(body.scenes)
83 | }
84 | }
85 | }
86 |
87 | async put (path, body) {
88 | this.debug('PUT %s %j', path, body)
89 | try {
90 | await this.client.put(this.rpath + path, body)
91 | } catch (error) {
92 | if (!(error instanceof HttpError)) {
93 | this.warn(error)
94 | }
95 | }
96 | }
97 | }
98 |
99 | export { DeconzService }
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "homebridge-deconz",
3 | "description": "Homebridge plugin for deCONZ",
4 | "displayName": "Homebridge deCONZ",
5 | "author": "Erik Baauw",
6 | "maintainers": [
7 | "ebaauw"
8 | ],
9 | "license": "Apache-2.0",
10 | "version": "1.2.0-0",
11 | "keywords": [
12 | "homebridge-plugin",
13 | "homekit",
14 | "deconz",
15 | "phoscon",
16 | "raspbee",
17 | "conbee",
18 | "dresden-elektronik",
19 | "dresdenelektronik"
20 | ],
21 | "type": "module",
22 | "main": "index.js",
23 | "bin": {
24 | "deconz": "cli/deconz.js",
25 | "otau": "cli/otau.js",
26 | "ui": "cli/ui.js"
27 | },
28 | "engines": {
29 | "deCONZ": "2.30.2",
30 | "homebridge": "^1.9.0||^2.0.0-beta",
31 | "node": "^22||^20||^18"
32 | },
33 | "dependencies": {
34 | "hb-deconz-tools": "~2.0.11",
35 | "homebridge-lib": "~7.1.5"
36 | },
37 | "scripts": {
38 | "prepare": "standard && rm -rf out && jsdoc -c jsdoc.json",
39 | "test": "standard && echo \"Error: no test specified\" && exit 1"
40 | },
41 | "repository": {
42 | "type": "git",
43 | "url": "git+https://github.com/ebaauw/homebridge-deconz.git"
44 | },
45 | "bugs": {
46 | "url": "https://github.com/ebaauw/homebridge-deconz/issues"
47 | },
48 | "homepage": "https://github.com/ebaauw/homebridge-deconz#readme",
49 | "funding": [
50 | {
51 | "type": "github",
52 | "url": "https://github.com/sponsors/ebaauw"
53 | },
54 | {
55 | "type": "paypal",
56 | "url": "https://www.paypal.me/ebaauw/EUR"
57 | }
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------