@slycke/homebridge-xcomfort-shc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## 1.0.0
2
+
3
+ ### Changed
4
+ - Renamed the maintained fork to `@slycke/homebridge-xcomfort-shc`.
5
+ - Migrated to Homebridge 2, Node.js 22/24, TypeScript, and ESM.
6
+ - Replaced legacy polling with an internal async poller.
7
+ - Preserved the `Xcomfort` platform alias and SHC device-id UUID seed for migration.
8
+
9
+ ### Added
10
+ - Homebridge UI config schema.
11
+ - Local smoke-test documentation.
12
+ - Plugin logo.
13
+ - Debounced dimmer brightness updates.
14
+
15
+ ### Fixed
16
+ - Trim SHC device names before creating/updating accessories.
17
+ - Log unsupported SHC device types once per plugin runtime.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Per Slycke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @slycke/homebridge-xcomfort-shc
2
+
3
+ [![npm version](https://badgen.net/npm/v/@slycke/homebridge-xcomfort-shc)](https://www.npmjs.com/package/@slycke/homebridge-xcomfort-shc)
4
+
5
+ <p align="center">
6
+ <img src="assets/logo.png" alt="Xcomfort SHC Homebridge plugin logo" width="160">
7
+ </p>
8
+
9
+ Homebridge 2 platform plugin for Eaton Xcomfort Smart Home Controller devices.
10
+
11
+ This is a maintained fork and successor of [`moritzw1/homebridge-xcomfort`](https://github.com/moritzw1/homebridge-xcomfort). It keeps the `Xcomfort` Homebridge platform alias for easier migration.
12
+
13
+ Repository: [`slycke/homebridge-xcomfort-shc`](https://github.com/slycke/homebridge-xcomfort-shc)
14
+
15
+ This project is not affiliated with Eaton.
16
+
17
+ ## Requirements
18
+
19
+ - Homebridge 2
20
+ - Node.js 22.12 or newer, or Node.js 24
21
+ - Eaton Xcomfort Smart Home Controller (SHC) on the same local network
22
+ - An SHC zone with the devices you want to expose to Homebridge
23
+
24
+ The round Xcomfort bridge is not supported, only SHC.
25
+
26
+ ## Supported Devices
27
+
28
+ - CRCA-00 Room Controller Touch: humidity, temperature, ambient light
29
+ - CDAx-01/0x dimming actuators: on/off and brightness
30
+ - CSAU-01/01-1x switch actuators: on/off
31
+ - Light actuators: on/off as HomeKit lights
32
+
33
+ ## Installation
34
+
35
+ Install through the Homebridge UI by searching for `@slycke/homebridge-xcomfort-shc`, or install manually:
36
+
37
+ ```sh
38
+ npm install -g @slycke/homebridge-xcomfort-shc
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Example Homebridge platform config:
44
+
45
+ ```json
46
+ {
47
+ "platform": "Xcomfort",
48
+ "ip": "192.168.2.100",
49
+ "user": "admin",
50
+ "password": "password",
51
+ "zone": "14",
52
+ "intervalActive": 15,
53
+ "intervalSleep": 60,
54
+ "step": 1
55
+ }
56
+ ```
57
+
58
+ Fields:
59
+
60
+ - `platform`: must remain `Xcomfort`.
61
+ - `ip`: IPv4 address of the SHC.
62
+ - `user`: SHC user with access to the configured zone.
63
+ - `password`: SHC user password.
64
+ - `zone`: numeric part of the SHC zone, for example `14` for `hz_14`. Existing configs using `hz_14` are also accepted.
65
+ - `intervalActive`: polling interval in seconds while Home has recently interacted with an accessory. Default: `15`.
66
+ - `intervalSleep`: polling interval in seconds after five minutes without Home app activity. Default: `60`.
67
+ - `step`: brightness step size for dim actuators. Default: `1`.
68
+
69
+ ## Migrating From `homebridge-xcomfort`
70
+
71
+ Do not run the original `homebridge-xcomfort` package and this package at the same time. Both use the `Xcomfort` platform alias during the migration period.
72
+
73
+ Recommended migration:
74
+
75
+ 1. Back up your Homebridge config.
76
+ 2. Install `@slycke/homebridge-xcomfort-shc`.
77
+ 3. Remove `homebridge-xcomfort`.
78
+ 4. Keep your existing `platform: "Xcomfort"` config.
79
+ 5. Restart Homebridge once.
80
+ 6. Verify that the cached accessories restored and that each device responds.
81
+
82
+ The plugin preserves accessory UUID generation from the SHC device id, so existing HomeKit rooms, scenes, and automations have the best chance of surviving the package transition.
83
+
84
+ If you restarted with both packages installed and see duplicate or no-response accessories, remove the original `homebridge-xcomfort` package, restart Homebridge, and then check the cached accessories again.
85
+
86
+ ## Development
87
+
88
+ ```sh
89
+ npm install
90
+ npm run build
91
+ npm run lint
92
+ npm test
93
+ ```
94
+
95
+ For final local testing, use an ignored Homebridge profile with Homebridge, Homebridge UI, and this plugin installed into that profile. This keeps the test environment close to a real UI-managed install without adding UI packages to this plugin's published dependencies.
96
+
97
+ ```sh
98
+ cp -R test/hbConfig test/hbConfig.local
99
+ $EDITOR test/hbConfig.local/config.json
100
+ npm install --prefix ./test/hbConfig.local homebridge@^2.0.0
101
+ npm install --prefix ./test/hbConfig.local homebridge-config-ui-x@latest
102
+ npm install --prefix ./test/hbConfig.local .
103
+ ./test/hbConfig.local/node_modules/.bin/hb-service run -U ./test/hbConfig.local -P ./test/hbConfig.local/node_modules --strict-plugin-resolution --stdout
104
+ ```
105
+
106
+ Then open <http://localhost:8581>. This supervised UI test mode supports UI-managed restart paths, child bridge controls, logs, accessories, and the plugin config form.
107
+
108
+ Never put real SHC credentials in `test/hbConfig/config.json`; it is the committed template. `test/hbConfig.local/` is ignored by git and is the place for hardware credentials, pairing data, and cached accessories.
109
+ The committed smoke-test bridge is named `Xcomfort SHC Test`; update `test/hbConfig.local/config.json` manually if you already copied the template.
110
+
111
+ The `hb-service run` command looks for `homebridge` in the same `node_modules` directory as `homebridge-config-ui-x` before falling back to a global install. The local-profile install above is preferred for development, but a global Homebridge/Homebridge UI install can also be used for manual testing if that matches your production setup.
112
+
113
+ For a faster CLI-only smoke test without Homebridge UI:
114
+
115
+ ```sh
116
+ npx homebridge -D -U ./test/hbConfig.local -P . --strict-plugin-resolution
117
+ ```
118
+
119
+ Homebridge UI v5 does not start its web server just because the `config` platform is present in `config.json`. If you only need to inspect the UI while running Homebridge manually, start the standalone UI server in a second terminal:
120
+
121
+ ```sh
122
+ node ./test/hbConfig.local/node_modules/homebridge-config-ui-x/dist/bin/standalone.js -U ./test/hbConfig.local -P ./test/hbConfig.local/node_modules -I
123
+ ```
124
+
125
+ In this two-terminal standalone UI setup, the UI is not supervising the Homebridge process. UI features that require Homebridge service IPC, such as restart buttons, child bridge restart controls, and Matter monitoring, may report that the Homebridge service is unavailable.
126
+
127
+ Use a child bridge for production installs when possible. This keeps Homebridge itself available if a plugin-specific SHC connection issue occurs.
128
+
129
+ ## Known Limitations
130
+
131
+ - The plugin depends on [`xcomfort-shc-api`](https://github.com/oanylund/xcomfort-shc-api), which is a CommonJS library last published several years ago. The dependency is isolated behind an adapter so it can be replaced later without changing accessory code.
132
+ - Xcomfort dimmers can emit many brightness commands while dragging the Home app slider. The plugin clamps and coalesces rapid brightness updates before sending them to the SHC, then reports SHC failures back to HomeKit. Very fast slider movement may still be constrained by the SHC.
133
+ - Shutter actuators are discovered but not exposed yet. HomeKit supports window coverings, but this plugin still needs a verified SHC command/value mapping for `ShutterActuator` before enabling control.
134
+ - Matter-specific accessory registration is not implemented yet. The plugin is safe to run on Homebridge 2 with Matter enabled.
135
+
136
+ ## Credits
137
+
138
+ This project builds on the earlier open source work in [`moritzw1/homebridge-xcomfort`](https://github.com/moritzw1/homebridge-xcomfort).
139
+
140
+ The SHC communication layer currently uses [`oanylund/xcomfort-shc-api`](https://github.com/oanylund/xcomfort-shc-api).
Binary file
@@ -0,0 +1,62 @@
1
+ {
2
+ "pluginAlias": "Xcomfort",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "schema": {
6
+ "type": "object",
7
+ "required": [
8
+ "ip",
9
+ "user",
10
+ "password",
11
+ "zone"
12
+ ],
13
+ "properties": {
14
+ "ip": {
15
+ "title": "SHC IP address",
16
+ "type": "string",
17
+ "placeholder": "192.168.1.100",
18
+ "format": "ipv4",
19
+ "description": "IPv4 address of the Eaton Xcomfort Smart Home Controller on your local network."
20
+ },
21
+ "user": {
22
+ "title": "Username",
23
+ "type": "string",
24
+ "description": "Username for a local SHC user with access to the configured zone."
25
+ },
26
+ "password": {
27
+ "title": "Password",
28
+ "type": "string",
29
+ "password": true,
30
+ "description": "Password for the SHC user."
31
+ },
32
+ "zone": {
33
+ "title": "Zone ID",
34
+ "type": "string",
35
+ "placeholder": "14",
36
+ "description": "SHC zone containing the devices to expose. Enter the numeric part only, for example 14 for hz_14. Existing configs that include hz_ are also accepted."
37
+ },
38
+ "intervalActive": {
39
+ "title": "Active poll interval",
40
+ "type": "number",
41
+ "default": 15,
42
+ "minimum": 1,
43
+ "description": "Polling interval in seconds while the Home app has recently interacted with an accessory."
44
+ },
45
+ "intervalSleep": {
46
+ "title": "Sleep poll interval",
47
+ "type": "number",
48
+ "default": 60,
49
+ "minimum": 1,
50
+ "description": "Polling interval in seconds after five minutes without Home app activity."
51
+ },
52
+ "step": {
53
+ "title": "Dim actuator step",
54
+ "type": "number",
55
+ "default": 5,
56
+ "minimum": 1,
57
+ "maximum": 100,
58
+ "description": "Brightness step size for dim actuators. For example, 5 allows 5%, 10%, 15%, and so on."
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,20 @@
1
+ import type { XcomfortPlatform } from '../platform.js';
2
+ import type { XcomfortDevice, XcomfortPlatformAccessory } from '../types.js';
3
+ export declare class DimActuatorAccessory {
4
+ private readonly platform;
5
+ private readonly accessory;
6
+ private readonly service;
7
+ private isOn;
8
+ private brightness;
9
+ private pendingBrightness?;
10
+ private brightnessTimer?;
11
+ private pendingBrightnessResolvers;
12
+ constructor(platform: XcomfortPlatform, accessory: XcomfortPlatformAccessory, device: XcomfortDevice);
13
+ update(device: XcomfortDevice): void;
14
+ private handleOnGet;
15
+ private handleOnSet;
16
+ private handleBrightnessGet;
17
+ private handleBrightnessSet;
18
+ private setBrightnessDebounced;
19
+ private flushBrightness;
20
+ }
@@ -0,0 +1,99 @@
1
+ import { DIM_BRIGHTNESS_DEBOUNCE_MS } from '../settings.js';
2
+ import { clampInteger, parseDimState } from '../utils.js';
3
+ export class DimActuatorAccessory {
4
+ platform;
5
+ accessory;
6
+ service;
7
+ isOn = false;
8
+ brightness = 0;
9
+ pendingBrightness;
10
+ brightnessTimer;
11
+ pendingBrightnessResolvers = [];
12
+ constructor(platform, accessory, device) {
13
+ this.platform = platform;
14
+ this.accessory = accessory;
15
+ this.service = this.accessory.getService(this.platform.Service.Lightbulb)
16
+ ?? this.accessory.addService(this.platform.Service.Lightbulb);
17
+ this.accessory.getService(this.platform.Service.AccessoryInformation)
18
+ ?.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Eaton')
19
+ .setCharacteristic(this.platform.Characteristic.Model, 'Xcomfort DimActuator')
20
+ .setCharacteristic(this.platform.Characteristic.SerialNumber, device.id);
21
+ this.service.getCharacteristic(this.platform.Characteristic.Brightness).setProps({
22
+ minStep: this.platform.dimStep,
23
+ });
24
+ this.service.getCharacteristic(this.platform.Characteristic.On)
25
+ .onGet(() => this.handleOnGet())
26
+ .onSet((value) => this.handleOnSet(value));
27
+ this.service.getCharacteristic(this.platform.Characteristic.Brightness)
28
+ .onGet(() => this.handleBrightnessGet())
29
+ .onSet((value) => this.handleBrightnessSet(value));
30
+ this.update(device);
31
+ }
32
+ update(device) {
33
+ const state = parseDimState(device.value);
34
+ this.isOn = state.isOn;
35
+ this.brightness = state.brightness;
36
+ this.accessory.context.isOn = this.isOn;
37
+ this.accessory.context.brightness = this.brightness;
38
+ this.accessory.context.value = device.value;
39
+ this.service.updateCharacteristic(this.platform.Characteristic.On, this.isOn);
40
+ this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.brightness);
41
+ }
42
+ handleOnGet() {
43
+ this.platform.markDeviceWasActive();
44
+ return this.isOn;
45
+ }
46
+ async handleOnSet(value) {
47
+ const next = Boolean(value);
48
+ if (next === this.isOn) {
49
+ return;
50
+ }
51
+ await this.platform.setDeviceState(this.accessory.context.deviceId, next ? 'on' : 'off');
52
+ this.isOn = next;
53
+ this.accessory.context.isOn = next;
54
+ }
55
+ handleBrightnessGet() {
56
+ this.platform.markDeviceWasActive();
57
+ return this.brightness;
58
+ }
59
+ async handleBrightnessSet(value) {
60
+ const next = clampInteger(Number(value), 0, 100);
61
+ this.brightness = next;
62
+ this.isOn = next > 0;
63
+ this.accessory.context.brightness = next;
64
+ this.accessory.context.isOn = this.isOn;
65
+ await this.setBrightnessDebounced(next);
66
+ }
67
+ setBrightnessDebounced(next) {
68
+ this.pendingBrightness = next;
69
+ if (this.brightnessTimer) {
70
+ clearTimeout(this.brightnessTimer);
71
+ }
72
+ const promise = new Promise((resolve, reject) => {
73
+ this.pendingBrightnessResolvers.push({ resolve, reject });
74
+ });
75
+ this.brightnessTimer = setTimeout(() => {
76
+ void this.flushBrightness();
77
+ }, DIM_BRIGHTNESS_DEBOUNCE_MS);
78
+ return promise;
79
+ }
80
+ async flushBrightness() {
81
+ const next = this.pendingBrightness;
82
+ const resolvers = this.pendingBrightnessResolvers;
83
+ this.pendingBrightness = undefined;
84
+ this.pendingBrightnessResolvers = [];
85
+ this.brightnessTimer = undefined;
86
+ if (next === undefined) {
87
+ resolvers.forEach(({ resolve }) => resolve());
88
+ return;
89
+ }
90
+ try {
91
+ await this.platform.setDeviceState(this.accessory.context.deviceId, String(next));
92
+ resolvers.forEach(({ resolve }) => resolve());
93
+ }
94
+ catch (error) {
95
+ resolvers.forEach(({ reject }) => reject(error));
96
+ }
97
+ }
98
+ }
99
+ //# sourceMappingURL=dimActuator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dimActuator.js","sourceRoot":"","sources":["../../src/accessories/dimActuator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { XcomfortPlatform } from '../platform.js';
2
+ import type { XcomfortDevice, XcomfortPlatformAccessory } from '../types.js';
3
+ export declare class SensorAccessory {
4
+ private readonly platform;
5
+ private readonly accessory;
6
+ private readonly service;
7
+ private readonly characteristic;
8
+ private value;
9
+ constructor(platform: XcomfortPlatform, accessory: XcomfortPlatformAccessory, device: XcomfortDevice);
10
+ update(device: XcomfortDevice): void;
11
+ private handleGet;
12
+ private findOrCreateService;
13
+ private characteristicForType;
14
+ }
@@ -0,0 +1,58 @@
1
+ import { parseSensorValue } from '../utils.js';
2
+ export class SensorAccessory {
3
+ platform;
4
+ accessory;
5
+ service;
6
+ characteristic;
7
+ value = 0;
8
+ constructor(platform, accessory, device) {
9
+ this.platform = platform;
10
+ this.accessory = accessory;
11
+ this.service = this.findOrCreateService(device.type);
12
+ this.characteristic = this.characteristicForType(device.type);
13
+ this.accessory.getService(this.platform.Service.AccessoryInformation)
14
+ ?.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Eaton')
15
+ .setCharacteristic(this.platform.Characteristic.Model, `Xcomfort ${device.type}`)
16
+ .setCharacteristic(this.platform.Characteristic.SerialNumber, device.id);
17
+ this.service.getCharacteristic(this.characteristic)
18
+ .onGet(() => this.handleGet());
19
+ this.update(device);
20
+ }
21
+ update(device) {
22
+ this.value = parseSensorValue(device.type, device.value);
23
+ this.accessory.context.sensorValue = this.value;
24
+ this.accessory.context.value = device.value;
25
+ this.service.updateCharacteristic(this.characteristic, this.value);
26
+ }
27
+ handleGet() {
28
+ return this.value;
29
+ }
30
+ findOrCreateService(type) {
31
+ switch (type) {
32
+ case 'HumiditySensor':
33
+ return this.accessory.getService(this.platform.Service.HumiditySensor)
34
+ ?? this.accessory.addService(this.platform.Service.HumiditySensor);
35
+ case 'LuxSensor':
36
+ return this.accessory.getService(this.platform.Service.LightSensor)
37
+ ?? this.accessory.addService(this.platform.Service.LightSensor);
38
+ case 'TemperatureSensor':
39
+ return this.accessory.getService(this.platform.Service.TemperatureSensor)
40
+ ?? this.accessory.addService(this.platform.Service.TemperatureSensor);
41
+ default:
42
+ throw new Error(`Unsupported sensor type ${type}`);
43
+ }
44
+ }
45
+ characteristicForType(type) {
46
+ switch (type) {
47
+ case 'HumiditySensor':
48
+ return this.platform.Characteristic.CurrentRelativeHumidity;
49
+ case 'LuxSensor':
50
+ return this.platform.Characteristic.CurrentAmbientLightLevel;
51
+ case 'TemperatureSensor':
52
+ return this.platform.Characteristic.CurrentTemperature;
53
+ default:
54
+ throw new Error(`Unsupported sensor type ${type}`);
55
+ }
56
+ }
57
+ }
58
+ //# sourceMappingURL=sensor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensor.js","sourceRoot":"","sources":["../../src/accessories/sensor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { XcomfortPlatform } from '../platform.js';
2
+ import type { XcomfortDevice, XcomfortPlatformAccessory } from '../types.js';
3
+ export declare class StateActuatorAccessory {
4
+ private readonly platform;
5
+ private readonly accessory;
6
+ private readonly service;
7
+ private isOn;
8
+ constructor(platform: XcomfortPlatform, accessory: XcomfortPlatformAccessory, device: XcomfortDevice);
9
+ update(device: XcomfortDevice): void;
10
+ private handleOnGet;
11
+ private handleOnSet;
12
+ }
@@ -0,0 +1,43 @@
1
+ import { parseSwitchState } from '../utils.js';
2
+ export class StateActuatorAccessory {
3
+ platform;
4
+ accessory;
5
+ service;
6
+ isOn = false;
7
+ constructor(platform, accessory, device) {
8
+ this.platform = platform;
9
+ this.accessory = accessory;
10
+ const serviceType = device.type === 'LightActuator'
11
+ ? this.platform.Service.Lightbulb
12
+ : this.platform.Service.Switch;
13
+ this.service = this.accessory.getService(serviceType) ?? this.accessory.addService(serviceType);
14
+ this.accessory.getService(this.platform.Service.AccessoryInformation)
15
+ ?.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Eaton')
16
+ .setCharacteristic(this.platform.Characteristic.Model, `Xcomfort ${device.type}`)
17
+ .setCharacteristic(this.platform.Characteristic.SerialNumber, device.id);
18
+ this.service.getCharacteristic(this.platform.Characteristic.On)
19
+ .onGet(() => this.handleOnGet())
20
+ .onSet((value) => this.handleOnSet(value));
21
+ this.update(device);
22
+ }
23
+ update(device) {
24
+ this.isOn = parseSwitchState(device.value);
25
+ this.accessory.context.isOn = this.isOn;
26
+ this.accessory.context.value = device.value;
27
+ this.service.updateCharacteristic(this.platform.Characteristic.On, this.isOn);
28
+ }
29
+ handleOnGet() {
30
+ this.platform.markDeviceWasActive();
31
+ return this.isOn;
32
+ }
33
+ async handleOnSet(value) {
34
+ const next = Boolean(value);
35
+ if (next === this.isOn) {
36
+ return;
37
+ }
38
+ await this.platform.setDeviceState(this.accessory.context.deviceId, next ? 'on' : 'off');
39
+ this.isOn = next;
40
+ this.accessory.context.isOn = next;
41
+ }
42
+ }
43
+ //# sourceMappingURL=stateActuator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stateActuator.js","sourceRoot":"","sources":["../../src/accessories/stateActuator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,OAAO,sBAAsB;IAKd;IACA;IALF,OAAO,CAAU;IAC1B,IAAI,GAAG,KAAK,CAAC;IAErB,YACmB,QAA0B,EAC1B,SAAoC,EACrD,MAAsB;QAFL,aAAQ,GAAR,QAAQ,CAAkB;QAC1B,cAAS,GAAT,SAAS,CAA2B;QAGrD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,KAAK,eAAe;YACjD,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YACjC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;QAEjC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QAEhG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAC;YACnE,EAAE,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,OAAO,CAAC;aACtE,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,YAAY,MAAM,CAAC,IAAI,EAAE,CAAC;aAChF,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAE3E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;aAC5D,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;aAC/B,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAE7C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,CAAC,MAAsB;QAC3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACxC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC5C,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAChF,CAAC;IAEO,WAAW;QACjB,IAAI,CAAC,QAAQ,CAAC,mBAAmB,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,KAA0B;QAClD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACzF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IACrC,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
2
+ import { XcomfortPlatform } from './platform.js';
3
+ export default (api) => {
4
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, XcomfortPlatform);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,eAAe,CAAC,GAAQ,EAAQ,EAAE;IAChC,GAAG,CAAC,gBAAgB,CAAC,WAAW,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC;AACrE,CAAC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, Service } from 'homebridge';
2
+ import type { XcomfortPlatformConfig } from './types.js';
3
+ export declare class XcomfortPlatform implements DynamicPlatformPlugin {
4
+ readonly log: Logger;
5
+ readonly api: API;
6
+ readonly Service: typeof Service;
7
+ readonly Characteristic: typeof Characteristic;
8
+ private readonly accessories;
9
+ private readonly controllers;
10
+ private readonly config?;
11
+ private readonly client;
12
+ private poller?;
13
+ private sleepTimer?;
14
+ private sleepMode;
15
+ private readonly loggedUnsupportedDeviceTypes;
16
+ constructor(log: Logger, rawConfig: XcomfortPlatformConfig, api: API);
17
+ configureAccessory(accessory: PlatformAccessory): void;
18
+ markDeviceWasActive(): void;
19
+ setDeviceState(deviceId: string, value: string): Promise<void>;
20
+ communicationError(error: unknown, message: string): Error;
21
+ get dimStep(): number;
22
+ private start;
23
+ private startPolling;
24
+ private discoverDevices;
25
+ private upsertDevice;
26
+ private createController;
27
+ private categoryForDevice;
28
+ private unregisterStaleAccessories;
29
+ private resetSleepTimer;
30
+ private shutdown;
31
+ }
@@ -0,0 +1,215 @@
1
+ import { DimActuatorAccessory } from './accessories/dimActuator.js';
2
+ import { SensorAccessory } from './accessories/sensor.js';
3
+ import { StateActuatorAccessory } from './accessories/stateActuator.js';
4
+ import { AsyncPoller } from './poller.js';
5
+ import { DISPLAY_NAME, PLATFORM_NAME, PLUGIN_NAME, SLEEP_AFTER_MS } from './settings.js';
6
+ import { deviceUuidSeed, isSupportedDeviceType, normalizeConfig, formatError } from './utils.js';
7
+ import { XcomfortShcClient } from './xcomfortClient.js';
8
+ export class XcomfortPlatform {
9
+ log;
10
+ api;
11
+ Service;
12
+ Characteristic;
13
+ accessories = new Map();
14
+ controllers = new Map();
15
+ config;
16
+ client;
17
+ poller;
18
+ sleepTimer;
19
+ sleepMode = false;
20
+ loggedUnsupportedDeviceTypes = new Set();
21
+ constructor(log, rawConfig, api) {
22
+ this.log = log;
23
+ this.api = api;
24
+ this.Service = this.api.hap.Service;
25
+ this.Characteristic = this.api.hap.Characteristic;
26
+ this.config = normalizeConfig(rawConfig);
27
+ this.client = new XcomfortShcClient(this.log);
28
+ this.api.on('didFinishLaunching', () => {
29
+ void this.start();
30
+ });
31
+ this.api.on('shutdown', () => {
32
+ this.shutdown();
33
+ });
34
+ }
35
+ configureAccessory(accessory) {
36
+ const typedAccessory = accessory;
37
+ this.accessories.set(typedAccessory.UUID, typedAccessory);
38
+ this.log.debug(`Restored cached accessory ${typedAccessory.displayName} (${typedAccessory.UUID})`);
39
+ }
40
+ markDeviceWasActive() {
41
+ if (!this.config || !this.poller) {
42
+ return;
43
+ }
44
+ if (this.sleepMode) {
45
+ this.sleepMode = false;
46
+ this.log.debug('Leaving sleep polling mode after Home app activity');
47
+ this.poller.setInterval(this.config.activeIntervalMs);
48
+ }
49
+ this.resetSleepTimer();
50
+ }
51
+ async setDeviceState(deviceId, value) {
52
+ if (!this.config) {
53
+ throw this.communicationError(new Error('Plugin is not configured'), `Cannot control ${deviceId}`);
54
+ }
55
+ try {
56
+ const ok = await this.client.controlDevice(this.config.zone, deviceId, value);
57
+ if (!ok) {
58
+ throw new Error('SHC did not confirm the control command');
59
+ }
60
+ this.markDeviceWasActive();
61
+ }
62
+ catch (error) {
63
+ throw this.communicationError(error, `Failed to control Xcomfort device ${deviceId}`);
64
+ }
65
+ }
66
+ communicationError(error, message) {
67
+ this.log.warn(`${message}: ${formatError(error)}`);
68
+ const hap = this.api.hap;
69
+ if (hap.HapStatusError && hap.HAPStatus) {
70
+ return new hap.HapStatusError(hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
71
+ }
72
+ return error instanceof Error ? error : new Error(formatError(error));
73
+ }
74
+ get dimStep() {
75
+ return this.config?.dimStep ?? 1;
76
+ }
77
+ async start() {
78
+ if (!this.config) {
79
+ this.log.warn(`${DISPLAY_NAME} is not configured. Add ip, user, password, and zone to start the plugin.`);
80
+ return;
81
+ }
82
+ try {
83
+ await this.client.connect(this.config);
84
+ this.log.info('Xcomfort SHC connection successful');
85
+ this.startPolling();
86
+ }
87
+ catch (error) {
88
+ this.log.warn(`Error connecting to Xcomfort SHC. Please check config: ${formatError(error)}`);
89
+ }
90
+ }
91
+ startPolling() {
92
+ if (!this.config) {
93
+ return;
94
+ }
95
+ this.poller = new AsyncPoller(this.log, this.config.activeIntervalMs, () => this.discoverDevices());
96
+ this.poller.start(true);
97
+ this.resetSleepTimer();
98
+ }
99
+ async discoverDevices() {
100
+ if (!this.config) {
101
+ return;
102
+ }
103
+ const devices = await this.client.getDevices(this.config.zone);
104
+ const discovered = new Set();
105
+ for (const device of devices) {
106
+ if (!isSupportedDeviceType(device.type)) {
107
+ if (!this.loggedUnsupportedDeviceTypes.has(device.type)) {
108
+ this.loggedUnsupportedDeviceTypes.add(device.type);
109
+ this.log.debug(`Ignoring unsupported Xcomfort device type ${device.type} (${device.name})`);
110
+ }
111
+ continue;
112
+ }
113
+ const uuid = this.api.hap.uuid.generate(deviceUuidSeed(device.id));
114
+ discovered.add(uuid);
115
+ this.upsertDevice(uuid, device);
116
+ }
117
+ this.unregisterStaleAccessories(discovered);
118
+ this.poller?.reset();
119
+ }
120
+ upsertDevice(uuid, device) {
121
+ let accessory = this.accessories.get(uuid);
122
+ let isNew = false;
123
+ if (!accessory) {
124
+ accessory = new this.api.platformAccessory(device.name, uuid, this.categoryForDevice(device));
125
+ this.accessories.set(uuid, accessory);
126
+ isNew = true;
127
+ this.log.info(`Adding new Xcomfort device: ${device.name}`);
128
+ }
129
+ accessory.context.deviceId = device.id;
130
+ accessory.context.deviceType = device.type;
131
+ accessory.context.name = device.name;
132
+ accessory.context.value = device.value;
133
+ accessory.context.reachable = true;
134
+ let controller = this.controllers.get(uuid);
135
+ if (!controller) {
136
+ controller = this.createController(accessory, device);
137
+ this.controllers.set(uuid, controller);
138
+ }
139
+ controller.update(device);
140
+ if (isNew) {
141
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
142
+ }
143
+ else {
144
+ this.api.updatePlatformAccessories([accessory]);
145
+ }
146
+ }
147
+ createController(accessory, device) {
148
+ switch (device.type) {
149
+ case 'DimActuator':
150
+ return new DimActuatorAccessory(this, accessory, device);
151
+ case 'LightActuator':
152
+ case 'SwitchActuator':
153
+ return new StateActuatorAccessory(this, accessory, device);
154
+ case 'HumiditySensor':
155
+ case 'LuxSensor':
156
+ case 'TemperatureSensor':
157
+ return new SensorAccessory(this, accessory, device);
158
+ default:
159
+ throw new Error('Unsupported Xcomfort device type');
160
+ }
161
+ }
162
+ categoryForDevice(device) {
163
+ switch (device.type) {
164
+ case 'DimActuator':
165
+ case 'LightActuator':
166
+ return 5 /* this.api.hap.Categories.LIGHTBULB */;
167
+ case 'SwitchActuator':
168
+ return 8 /* this.api.hap.Categories.SWITCH */;
169
+ case 'HumiditySensor':
170
+ case 'LuxSensor':
171
+ case 'TemperatureSensor':
172
+ return 10 /* this.api.hap.Categories.SENSOR */;
173
+ default:
174
+ return undefined;
175
+ }
176
+ }
177
+ unregisterStaleAccessories(discovered) {
178
+ const stale = [];
179
+ for (const [uuid, accessory] of this.accessories) {
180
+ if (accessory.context.deviceId && !discovered.has(uuid)) {
181
+ stale.push(accessory);
182
+ this.accessories.delete(uuid);
183
+ this.controllers.delete(uuid);
184
+ }
185
+ }
186
+ if (stale.length > 0) {
187
+ this.log.info(`Removing ${stale.length} stale Xcomfort accessory/accessories`);
188
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, stale);
189
+ }
190
+ }
191
+ resetSleepTimer() {
192
+ if (!this.config || !this.poller) {
193
+ return;
194
+ }
195
+ if (this.sleepTimer) {
196
+ clearTimeout(this.sleepTimer);
197
+ }
198
+ this.sleepTimer = setTimeout(() => {
199
+ if (!this.config || !this.poller) {
200
+ return;
201
+ }
202
+ this.sleepMode = true;
203
+ this.log.debug('Entering sleep polling mode after Home app inactivity');
204
+ this.poller.setInterval(this.config.sleepIntervalMs);
205
+ }, SLEEP_AFTER_MS);
206
+ }
207
+ shutdown() {
208
+ this.poller?.stop();
209
+ if (this.sleepTimer) {
210
+ clearTimeout(this.sleepTimer);
211
+ this.sleepTimer = undefined;
212
+ }
213
+ }
214
+ }
215
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Logger } from 'homebridge';
2
+ export declare class AsyncPoller {
3
+ private readonly log;
4
+ private intervalMs;
5
+ private readonly task;
6
+ private timer?;
7
+ private running;
8
+ private stopped;
9
+ constructor(log: Logger, intervalMs: number, task: () => Promise<void>);
10
+ start(immediate?: boolean): void;
11
+ stop(): void;
12
+ setInterval(intervalMs: number): void;
13
+ reset(): void;
14
+ private schedule;
15
+ private tick;
16
+ }
package/dist/poller.js ADDED
@@ -0,0 +1,67 @@
1
+ import { formatError } from './utils.js';
2
+ export class AsyncPoller {
3
+ log;
4
+ intervalMs;
5
+ task;
6
+ timer;
7
+ running = false;
8
+ stopped = true;
9
+ constructor(log, intervalMs, task) {
10
+ this.log = log;
11
+ this.intervalMs = intervalMs;
12
+ this.task = task;
13
+ }
14
+ start(immediate = false) {
15
+ this.stopped = false;
16
+ this.schedule(immediate ? 0 : this.intervalMs);
17
+ }
18
+ stop() {
19
+ this.stopped = true;
20
+ if (this.timer) {
21
+ clearTimeout(this.timer);
22
+ this.timer = undefined;
23
+ }
24
+ }
25
+ setInterval(intervalMs) {
26
+ this.intervalMs = intervalMs;
27
+ if (!this.stopped) {
28
+ this.schedule(this.intervalMs);
29
+ }
30
+ }
31
+ reset() {
32
+ if (!this.stopped) {
33
+ this.schedule(this.intervalMs);
34
+ }
35
+ }
36
+ schedule(delayMs) {
37
+ if (this.timer) {
38
+ clearTimeout(this.timer);
39
+ }
40
+ this.timer = setTimeout(() => {
41
+ void this.tick();
42
+ }, delayMs);
43
+ }
44
+ async tick() {
45
+ if (this.stopped) {
46
+ return;
47
+ }
48
+ if (this.running) {
49
+ this.schedule(this.intervalMs);
50
+ return;
51
+ }
52
+ this.running = true;
53
+ try {
54
+ await this.task();
55
+ }
56
+ catch (error) {
57
+ this.log.warn(`Xcomfort poll failed: ${formatError(error)}`);
58
+ }
59
+ finally {
60
+ this.running = false;
61
+ if (!this.stopped) {
62
+ this.schedule(this.intervalMs);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ //# sourceMappingURL=poller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,OAAO,WAAW;IAMH;IACT;IACS;IAPX,KAAK,CAAkB;IACvB,OAAO,GAAG,KAAK,CAAC;IAChB,OAAO,GAAG,IAAI,CAAC;IAEvB,YACmB,GAAW,EACpB,UAAkB,EACT,IAAyB;QAFzB,QAAG,GAAH,GAAG,CAAQ;QACpB,eAAU,GAAV,UAAU,CAAQ;QACT,SAAI,GAAJ,IAAI,CAAqB;IACzC,CAAC;IAEJ,KAAK,CAAC,SAAS,GAAG,KAAK;QACrB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC;IAED,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACzB,CAAC;IACH,CAAC;IAED,WAAW,CAAC,UAAkB;QAC5B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,OAAe;QAC9B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC3B,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,CAAC,EAAE,OAAO,CAAC,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,OAAO;QACT,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC/D,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ export declare const PLUGIN_NAME = "@slycke/homebridge-xcomfort-shc";
2
+ export declare const PLATFORM_NAME = "Xcomfort";
3
+ export declare const DISPLAY_NAME = "Xcomfort SHC";
4
+ export declare const DEFAULT_ACTIVE_INTERVAL_SECONDS = 15;
5
+ export declare const DEFAULT_SLEEP_INTERVAL_SECONDS = 60;
6
+ export declare const DEFAULT_DIM_STEP = 5;
7
+ export declare const DIM_BRIGHTNESS_DEBOUNCE_MS = 250;
8
+ export declare const SLEEP_AFTER_MS: number;
@@ -0,0 +1,9 @@
1
+ export const PLUGIN_NAME = '@slycke/homebridge-xcomfort-shc';
2
+ export const PLATFORM_NAME = 'Xcomfort';
3
+ export const DISPLAY_NAME = 'Xcomfort SHC';
4
+ export const DEFAULT_ACTIVE_INTERVAL_SECONDS = 15;
5
+ export const DEFAULT_SLEEP_INTERVAL_SECONDS = 60;
6
+ export const DEFAULT_DIM_STEP = 5;
7
+ export const DIM_BRIGHTNESS_DEBOUNCE_MS = 250;
8
+ export const SLEEP_AFTER_MS = 5 * 60 * 1000;
9
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAC;AAC7D,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AACxC,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAClD,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAClC,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAC9C,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC"}
@@ -0,0 +1,48 @@
1
+ import type { PlatformAccessory, PlatformConfig } from 'homebridge';
2
+ export type XcomfortDeviceType = 'DimActuator' | 'LightActuator' | 'SwitchActuator' | 'HumiditySensor' | 'LuxSensor' | 'TemperatureSensor';
3
+ export interface XcomfortDevice {
4
+ id: string;
5
+ name: string;
6
+ type: string;
7
+ value: string;
8
+ }
9
+ export type SupportedXcomfortDevice = XcomfortDevice & {
10
+ type: XcomfortDeviceType;
11
+ };
12
+ export interface XcomfortPlatformConfig extends PlatformConfig {
13
+ ip?: string;
14
+ user?: string;
15
+ password?: string;
16
+ zone?: string | number;
17
+ intervalActive?: number;
18
+ intervalSleep?: number;
19
+ step?: number;
20
+ }
21
+ export interface NormalizedConfig {
22
+ ip: string;
23
+ user: string;
24
+ password: string;
25
+ zone: string;
26
+ activeIntervalMs: number;
27
+ sleepIntervalMs: number;
28
+ dimStep: number;
29
+ }
30
+ export interface XcomfortAccessoryContext {
31
+ deviceId: string;
32
+ deviceType: XcomfortDeviceType;
33
+ name: string;
34
+ value?: string;
35
+ isOn?: boolean;
36
+ brightness?: number;
37
+ sensorValue?: number;
38
+ reachable?: boolean;
39
+ }
40
+ export type XcomfortPlatformAccessory = PlatformAccessory<XcomfortAccessoryContext>;
41
+ export interface XcomfortAccessoryController {
42
+ update(device: XcomfortDevice): void;
43
+ }
44
+ export interface XcomfortClient {
45
+ connect(config: NormalizedConfig): Promise<void>;
46
+ getDevices(zone: string): Promise<XcomfortDevice[]>;
47
+ controlDevice(zone: string, deviceId: string, value: string): Promise<boolean>;
48
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ import type { NormalizedConfig, XcomfortDeviceType, XcomfortPlatformConfig } from './types.js';
2
+ export declare function deviceUuidSeed(deviceId: string): string;
3
+ export declare function normalizeConfig(config: XcomfortPlatformConfig): NormalizedConfig | undefined;
4
+ export declare function isSupportedDeviceType(type: string): type is XcomfortDeviceType;
5
+ export declare function normalizeDeviceName(name: string): string;
6
+ export declare function parseDimState(value: string): {
7
+ isOn: boolean;
8
+ brightness: number;
9
+ };
10
+ export declare function parseSwitchState(value: string): boolean;
11
+ export declare function parseSensorValue(type: XcomfortDeviceType, value: string): number;
12
+ export declare function clampInteger(value: number, min: number, max: number): number;
13
+ export declare function clampNumber(value: number, min: number, max: number): number;
14
+ export declare function formatError(error: unknown): string;
package/dist/utils.js ADDED
@@ -0,0 +1,98 @@
1
+ import { DEFAULT_ACTIVE_INTERVAL_SECONDS, DEFAULT_DIM_STEP, DEFAULT_SLEEP_INTERVAL_SECONDS, } from './settings.js';
2
+ export function deviceUuidSeed(deviceId) {
3
+ return deviceId;
4
+ }
5
+ export function normalizeConfig(config) {
6
+ const ip = nonEmptyString(config.ip);
7
+ const user = nonEmptyString(config.user);
8
+ const password = nonEmptyString(config.password);
9
+ const zone = normalizeZone(config.zone);
10
+ if (!ip || !user || !password || !zone) {
11
+ return undefined;
12
+ }
13
+ return {
14
+ ip,
15
+ user,
16
+ password,
17
+ zone,
18
+ activeIntervalMs: secondsToMs(config.intervalActive, DEFAULT_ACTIVE_INTERVAL_SECONDS, 1),
19
+ sleepIntervalMs: secondsToMs(config.intervalSleep, DEFAULT_SLEEP_INTERVAL_SECONDS, 1),
20
+ dimStep: clampInteger(config.step ?? DEFAULT_DIM_STEP, 1, 100),
21
+ };
22
+ }
23
+ export function isSupportedDeviceType(type) {
24
+ return [
25
+ 'DimActuator',
26
+ 'LightActuator',
27
+ 'SwitchActuator',
28
+ 'HumiditySensor',
29
+ 'LuxSensor',
30
+ 'TemperatureSensor',
31
+ ].includes(type);
32
+ }
33
+ export function normalizeDeviceName(name) {
34
+ return name.trim();
35
+ }
36
+ export function parseDimState(value) {
37
+ const brightness = clampInteger(Number.parseInt(value, 10), 0, 100);
38
+ return {
39
+ isOn: brightness > 0,
40
+ brightness,
41
+ };
42
+ }
43
+ export function parseSwitchState(value) {
44
+ return value.toUpperCase() === 'ON';
45
+ }
46
+ export function parseSensorValue(type, value) {
47
+ const parsed = Number.parseFloat(value);
48
+ const numberValue = Number.isFinite(parsed) ? parsed : 0;
49
+ switch (type) {
50
+ case 'HumiditySensor':
51
+ return clampNumber(numberValue, 0, 100);
52
+ case 'LuxSensor':
53
+ return clampNumber(numberValue || 0.0001, 0.0001, 100000);
54
+ case 'TemperatureSensor':
55
+ return clampNumber(numberValue, 0, 100);
56
+ default:
57
+ return numberValue;
58
+ }
59
+ }
60
+ export function clampInteger(value, min, max) {
61
+ if (!Number.isFinite(value)) {
62
+ return min;
63
+ }
64
+ return Math.min(max, Math.max(min, Math.round(value)));
65
+ }
66
+ export function clampNumber(value, min, max) {
67
+ if (!Number.isFinite(value)) {
68
+ return min;
69
+ }
70
+ return Math.min(max, Math.max(min, value));
71
+ }
72
+ export function formatError(error) {
73
+ if (error instanceof Error) {
74
+ return error.message;
75
+ }
76
+ return String(error);
77
+ }
78
+ function normalizeZone(zone) {
79
+ if (typeof zone === 'number' && Number.isFinite(zone)) {
80
+ return String(zone);
81
+ }
82
+ const value = nonEmptyString(zone);
83
+ if (!value) {
84
+ return undefined;
85
+ }
86
+ return value.startsWith('hz_') ? value.substring(3) : value;
87
+ }
88
+ function nonEmptyString(value) {
89
+ if (typeof value !== 'string') {
90
+ return undefined;
91
+ }
92
+ const trimmed = value.trim();
93
+ return trimmed.length > 0 ? trimmed : undefined;
94
+ }
95
+ function secondsToMs(value, fallbackSeconds, minSeconds) {
96
+ return clampInteger(value ?? fallbackSeconds, minSeconds, Number.MAX_SAFE_INTEGER) * 1000;
97
+ }
98
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Logger } from 'homebridge';
2
+ import type { NormalizedConfig, XcomfortClient, XcomfortDevice } from './types.js';
3
+ export declare class XcomfortShcClient implements XcomfortClient {
4
+ private readonly log;
5
+ private client?;
6
+ constructor(log: Logger);
7
+ connect(config: NormalizedConfig): Promise<void>;
8
+ getDevices(zone: string): Promise<XcomfortDevice[]>;
9
+ controlDevice(zone: string, deviceId: string, value: string): Promise<boolean>;
10
+ private requireClient;
11
+ }
@@ -0,0 +1,82 @@
1
+ import { createRequire } from 'node:module';
2
+ import { formatError, normalizeDeviceName } from './utils.js';
3
+ const require = createRequire(import.meta.url);
4
+ const RawXcomfort = require('xcomfort-shc-api');
5
+ export class XcomfortShcClient {
6
+ log;
7
+ client;
8
+ constructor(log) {
9
+ this.log = log;
10
+ }
11
+ async connect(config) {
12
+ await new Promise((resolve, reject) => {
13
+ const client = new RawXcomfort({
14
+ baseUrl: `http://${config.ip}`,
15
+ username: config.user,
16
+ password: config.password,
17
+ autoSetup: true,
18
+ });
19
+ this.client = client;
20
+ const timeout = setTimeout(() => {
21
+ cleanup();
22
+ reject(new Error('Timed out waiting for SHC connection'));
23
+ }, 30000);
24
+ const onReady = () => {
25
+ cleanup();
26
+ client.on('error', (error) => {
27
+ this.log.warn(`Xcomfort SHC error: ${formatError(error)}`);
28
+ });
29
+ resolve();
30
+ };
31
+ const onError = (error) => {
32
+ cleanup();
33
+ reject(error instanceof Error ? error : new Error(formatError(error)));
34
+ };
35
+ const cleanup = () => {
36
+ clearTimeout(timeout);
37
+ client.removeListener?.('ready', onReady);
38
+ client.removeListener?.('error', onError);
39
+ };
40
+ client.on('ready', onReady);
41
+ client.on('error', onError);
42
+ });
43
+ }
44
+ async getDevices(zone) {
45
+ const response = await this.requireClient().query('StatusControlFunction/getDevices', [`hz_${zone}`, '']);
46
+ if (!Array.isArray(response)) {
47
+ throw new Error('Unexpected SHC getDevices response');
48
+ }
49
+ return response.filter(isXcomfortDevice).map((device) => ({
50
+ ...device,
51
+ name: normalizeDeviceName(device.name),
52
+ }));
53
+ }
54
+ async controlDevice(zone, deviceId, value) {
55
+ const response = await this.requireClient().query('StatusControlFunction/controlDevice', [
56
+ `hz_${zone}`,
57
+ deviceId,
58
+ value,
59
+ ]);
60
+ return isStatusResponse(response) && response.status === 'ok';
61
+ }
62
+ requireClient() {
63
+ if (!this.client) {
64
+ throw new Error('Xcomfort SHC client has not connected');
65
+ }
66
+ return this.client;
67
+ }
68
+ }
69
+ function isXcomfortDevice(value) {
70
+ if (!value || typeof value !== 'object') {
71
+ return false;
72
+ }
73
+ const candidate = value;
74
+ return (typeof candidate.id === 'string' &&
75
+ typeof candidate.name === 'string' &&
76
+ typeof candidate.type === 'string' &&
77
+ typeof candidate.value === 'string');
78
+ }
79
+ function isStatusResponse(value) {
80
+ return !!value && typeof value === 'object' && typeof value.status === 'string';
81
+ }
82
+ //# sourceMappingURL=xcomfortClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xcomfortClient.js","sourceRoot":"","sources":["../src/xcomfortClient.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "displayName": "Xcomfort SHC",
3
+ "name": "@slycke/homebridge-xcomfort-shc",
4
+ "description": "Homebridge 2 platform plugin for Eaton Xcomfort Smart Home Controller devices.",
5
+ "version": "1.0.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "assets",
13
+ "config.schema.json",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "license": "MIT",
19
+ "homepage": "https://github.com/slycke/homebridge-xcomfort-shc#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/slycke/homebridge-xcomfort-shc.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/slycke/homebridge-xcomfort-shc/issues"
26
+ },
27
+ "keywords": [
28
+ "homebridge-plugin",
29
+ "homebridge",
30
+ "xcomfort",
31
+ "eaton",
32
+ "shc",
33
+ "homekit"
34
+ ],
35
+ "engines": {
36
+ "node": "^22.12.0 || ^24.0.0",
37
+ "homebridge": "^2.0.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "watch": "tsc --watch",
42
+ "lint": "eslint .",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "prepack": "npm run build"
46
+ },
47
+ "dependencies": {
48
+ "xcomfort-shc-api": "^2.3.0"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^9.39.1",
52
+ "@types/node": "^24.10.1",
53
+ "eslint": "^9.39.1",
54
+ "homebridge": "^2.0.0",
55
+ "typescript": "^5.9.3",
56
+ "typescript-eslint": "^8.46.3",
57
+ "vitest": "^4.0.8"
58
+ }
59
+ }