@ecopoesis/homebridge-dmx 0.2.0 → 0.3.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/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +6 -4
- package/dist/platform.js.map +1 -1
- package/dist/platformAccessory.d.ts +5 -5
- package/dist/platformAccessory.d.ts.map +1 -1
- package/dist/platformAccessory.js +58 -29
- package/dist/platformAccessory.js.map +1 -1
- package/dist/stateRegistry.d.ts +21 -0
- package/dist/stateRegistry.d.ts.map +1 -0
- package/dist/stateRegistry.js +65 -0
- package/dist/stateRegistry.js.map +1 -0
- package/dist/zoneAccessory.d.ts +20 -4
- package/dist/zoneAccessory.d.ts.map +1 -1
- package/dist/zoneAccessory.js +103 -40
- package/dist/zoneAccessory.js.map +1 -1
- package/package.json +1 -1
- package/src/platform.ts +6 -4
- package/src/platformAccessory.ts +53 -31
- package/src/stateRegistry.ts +71 -0
- package/src/zoneAccessory.ts +106 -40
package/dist/platform.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class DmxPlatform implements DynamicPlatformPlugin {
|
|
|
8
8
|
readonly accessories: PlatformAccessory[];
|
|
9
9
|
private cfg;
|
|
10
10
|
private controllers;
|
|
11
|
+
private registry;
|
|
11
12
|
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
12
13
|
private makeController;
|
|
13
14
|
/** Called by Homebridge for each cached accessory at startup. */
|
package/dist/platform.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,GAAG,EACH,cAAc,EACd,qBAAqB,EACrB,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,GAAG,EACH,cAAc,EACd,qBAAqB,EACrB,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;AASpB,qBAAa,WAAY,YAAW,qBAAqB;aAUrC,GAAG,EAAE,MAAM;aACX,MAAM,EAAE,cAAc;aACtB,GAAG,EAAE,GAAG;IAX1B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IACtD,SAAgB,WAAW,EAAE,iBAAiB,EAAE,CAAM;IAEtD,OAAO,CAAC,GAAG,CAA6B;IACxC,OAAO,CAAC,WAAW,CAAsC;IACzD,OAAO,CAAC,QAAQ,CAAuB;gBAGrB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,GAAG;IAgC1B,OAAO,CAAC,cAAc;IAStB,iEAAiE;IACjE,kBAAkB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAKtD,OAAO,CAAC,eAAe;CA0DxB"}
|
package/dist/platform.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
7
|
import { StickController } from './controller.js';
|
|
8
8
|
import { StickFixture } from './platformAccessory.js';
|
|
9
|
+
import { StateRegistry } from './stateRegistry.js';
|
|
9
10
|
import { ZoneFixture } from './zoneAccessory.js';
|
|
10
11
|
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
|
|
11
12
|
export class DmxPlatform {
|
|
@@ -17,6 +18,7 @@ export class DmxPlatform {
|
|
|
17
18
|
accessories = [];
|
|
18
19
|
cfg = null;
|
|
19
20
|
controllers = new Map();
|
|
21
|
+
registry = new StateRegistry();
|
|
20
22
|
constructor(log, config, api) {
|
|
21
23
|
this.log = log;
|
|
22
24
|
this.config = config;
|
|
@@ -72,13 +74,13 @@ export class DmxPlatform {
|
|
|
72
74
|
if (existing) {
|
|
73
75
|
this.log.debug(`wiring cached accessory ${fx.id}`);
|
|
74
76
|
existing.context.fixture = { id: fx.id, controllerId: fx.controller.id };
|
|
75
|
-
new StickFixture(this, existing, fx, controller);
|
|
77
|
+
new StickFixture(this, existing, fx, controller, this.registry);
|
|
76
78
|
}
|
|
77
79
|
else {
|
|
78
80
|
this.log.info(`registering new accessory: ${fx.name} (${fx.id})`);
|
|
79
81
|
const accessory = new this.api.platformAccessory(fx.name, uuid);
|
|
80
82
|
accessory.context.fixture = { id: fx.id, controllerId: fx.controller.id };
|
|
81
|
-
new StickFixture(this, accessory, fx, controller);
|
|
83
|
+
new StickFixture(this, accessory, fx, controller, this.registry);
|
|
82
84
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
83
85
|
this.accessories.push(accessory);
|
|
84
86
|
}
|
|
@@ -91,13 +93,13 @@ export class DmxPlatform {
|
|
|
91
93
|
if (existing) {
|
|
92
94
|
this.log.debug(`wiring cached zone ${zone.id}`);
|
|
93
95
|
existing.context.zone = { id: zone.id };
|
|
94
|
-
new ZoneFixture(this, existing, zone, this.controllers);
|
|
96
|
+
new ZoneFixture(this, existing, zone, this.controllers, this.registry);
|
|
95
97
|
}
|
|
96
98
|
else {
|
|
97
99
|
this.log.info(`registering new zone: ${zone.name} (${zone.id}, ${zone.members.length} members)`);
|
|
98
100
|
const accessory = new this.api.platformAccessory(zone.name, uuid);
|
|
99
101
|
accessory.context.zone = { id: zone.id };
|
|
100
|
-
new ZoneFixture(this, accessory, zone, this.controllers);
|
|
102
|
+
new ZoneFixture(this, accessory, zone, this.controllers, this.registry);
|
|
101
103
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
102
104
|
this.accessories.push(accessory);
|
|
103
105
|
}
|
package/dist/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,kBAAkB;AAYlB,OAAO,EAA4B,UAAU,EAAa,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,OAAO,WAAW;
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,17 +2,17 @@ import type { PlatformAccessory } from 'homebridge';
|
|
|
2
2
|
import { Fixture } from './config.js';
|
|
3
3
|
import { StickController } from './controller.js';
|
|
4
4
|
import type { DmxPlatform } from './platform.js';
|
|
5
|
+
import { StateRegistry } from './stateRegistry.js';
|
|
5
6
|
export declare class StickFixture {
|
|
6
7
|
private readonly platform;
|
|
7
8
|
private readonly accessory;
|
|
8
9
|
private readonly fixture;
|
|
9
10
|
private readonly controller;
|
|
10
|
-
|
|
11
|
-
private state;
|
|
12
|
-
/** Debounce timer for HomeKit's multi-characteristic dispatch
|
|
13
|
-
* (Hue/Saturation/Brightness arrive as 3 separate set calls). */
|
|
11
|
+
private readonly registry;
|
|
14
12
|
private pending;
|
|
15
|
-
|
|
13
|
+
private bulb;
|
|
14
|
+
constructor(platform: DmxPlatform, accessory: PlatformAccessory, fixture: Fixture, controller: StickController, registry: StateRegistry);
|
|
15
|
+
private pushFromRegistry;
|
|
16
16
|
private schedule;
|
|
17
17
|
}
|
|
18
18
|
//# sourceMappingURL=platformAccessory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platformAccessory.d.ts","sourceRoot":"","sources":["../src/platformAccessory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"platformAccessory.d.ts","sourceRoot":"","sources":["../src/platformAccessory.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,qBAAa,YAAY;IAKrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,IAAI,CAAU;gBAGH,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,eAAe,EAC3B,QAAQ,EAAE,aAAa;IAoE1C,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,QAAQ;CAcjB"}
|
|
@@ -1,80 +1,109 @@
|
|
|
1
1
|
// StickFixture — one HomeKit Lightbulb accessory per patched fixture.
|
|
2
|
+
//
|
|
3
|
+
// State is owned by the platform-wide StateRegistry. We read on .onGet,
|
|
4
|
+
// write on .onSet, and subscribe to changes so the HomeKit-displayed
|
|
5
|
+
// values track the registry (e.g. when a zone updates this fixture).
|
|
2
6
|
import { CHARACTERISTIC_UPDATE_DELAY_MS } from './settings.js';
|
|
3
7
|
export class StickFixture {
|
|
4
8
|
platform;
|
|
5
9
|
accessory;
|
|
6
10
|
fixture;
|
|
7
11
|
controller;
|
|
8
|
-
|
|
9
|
-
state = { on: false, brightness: 100 };
|
|
10
|
-
/** Debounce timer for HomeKit's multi-characteristic dispatch
|
|
11
|
-
* (Hue/Saturation/Brightness arrive as 3 separate set calls). */
|
|
12
|
+
registry;
|
|
12
13
|
pending = null;
|
|
13
|
-
|
|
14
|
+
bulb;
|
|
15
|
+
constructor(platform, accessory, fixture, controller, registry) {
|
|
14
16
|
this.platform = platform;
|
|
15
17
|
this.accessory = accessory;
|
|
16
18
|
this.fixture = fixture;
|
|
17
19
|
this.controller = controller;
|
|
20
|
+
this.registry = registry;
|
|
18
21
|
const C = platform.Characteristic;
|
|
19
22
|
const info = accessory.getService(platform.Service.AccessoryInformation);
|
|
20
23
|
info?.setCharacteristic(C.Manufacturer, 'Stick-DE3 DMX')
|
|
21
24
|
?.setCharacteristic(C.Model, fixture.profile.name)
|
|
22
25
|
?.setCharacteristic(C.SerialNumber, fixture.id);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
this.bulb =
|
|
27
|
+
accessory.getService(platform.Service.Lightbulb)
|
|
28
|
+
?? accessory.addService(platform.Service.Lightbulb, fixture.name);
|
|
29
|
+
this.bulb.setCharacteristic(C.Name, fixture.name);
|
|
26
30
|
const chars = fixture.profile.model.characteristics;
|
|
27
|
-
bulb.getCharacteristic(C.On)
|
|
28
|
-
.onGet(() =>
|
|
29
|
-
.onSet((v) => {
|
|
31
|
+
this.bulb.getCharacteristic(C.On)
|
|
32
|
+
.onGet(() => registry.get(fixture.id).on)
|
|
33
|
+
.onSet((v) => {
|
|
34
|
+
registry.update(fixture.id, { on: Boolean(v) });
|
|
35
|
+
this.schedule();
|
|
36
|
+
});
|
|
30
37
|
if (chars.includes('Brightness')) {
|
|
31
|
-
bulb.getCharacteristic(C.Brightness)
|
|
32
|
-
.onGet(() =>
|
|
38
|
+
this.bulb.getCharacteristic(C.Brightness)
|
|
39
|
+
.onGet(() => registry.get(fixture.id).brightness)
|
|
33
40
|
.onSet((v) => {
|
|
34
|
-
|
|
41
|
+
registry.update(fixture.id, { brightness: Number(v) });
|
|
35
42
|
this.schedule();
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
if (chars.includes('Hue')) {
|
|
39
|
-
bulb.getCharacteristic(C.Hue)
|
|
40
|
-
.onGet(() =>
|
|
46
|
+
this.bulb.getCharacteristic(C.Hue)
|
|
47
|
+
.onGet(() => registry.get(fixture.id).hue ?? 0)
|
|
41
48
|
.onSet((v) => {
|
|
42
|
-
|
|
43
|
-
// setting hue implies color mode (sat>0); leave saturation as-is
|
|
49
|
+
registry.update(fixture.id, { hue: Number(v) });
|
|
44
50
|
this.schedule();
|
|
45
51
|
});
|
|
46
52
|
}
|
|
47
53
|
if (chars.includes('Saturation')) {
|
|
48
|
-
bulb.getCharacteristic(C.Saturation)
|
|
49
|
-
.onGet(() =>
|
|
54
|
+
this.bulb.getCharacteristic(C.Saturation)
|
|
55
|
+
.onGet(() => registry.get(fixture.id).saturation ?? 0)
|
|
50
56
|
.onSet((v) => {
|
|
51
|
-
|
|
57
|
+
registry.update(fixture.id, { saturation: Number(v) });
|
|
52
58
|
this.schedule();
|
|
53
59
|
});
|
|
54
60
|
}
|
|
55
61
|
if (chars.includes('ColorTemperature')) {
|
|
56
|
-
bulb.getCharacteristic(C.ColorTemperature)
|
|
57
|
-
.onGet(() =>
|
|
62
|
+
this.bulb.getCharacteristic(C.ColorTemperature)
|
|
63
|
+
.onGet(() => registry.get(fixture.id).colorTemperatureMireds ?? 200)
|
|
58
64
|
.onSet((v) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
// CCT change implies white mode; zero saturation.
|
|
66
|
+
registry.update(fixture.id, {
|
|
67
|
+
colorTemperatureMireds: Number(v),
|
|
68
|
+
saturation: 0,
|
|
69
|
+
});
|
|
63
70
|
this.schedule();
|
|
64
71
|
});
|
|
65
72
|
}
|
|
73
|
+
// Refresh HomeKit characteristics whenever this fixture's state changes
|
|
74
|
+
// in the registry (e.g. via a zone set, or another channel of itself).
|
|
75
|
+
registry.onChange((changedId) => {
|
|
76
|
+
if (changedId !== fixture.id)
|
|
77
|
+
return;
|
|
78
|
+
this.pushFromRegistry();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
pushFromRegistry() {
|
|
82
|
+
const s = this.registry.get(this.fixture.id);
|
|
83
|
+
const C = this.platform.Characteristic;
|
|
84
|
+
const chars = this.fixture.profile.model.characteristics;
|
|
85
|
+
this.bulb.updateCharacteristic(C.On, s.on);
|
|
86
|
+
if (chars.includes('Brightness'))
|
|
87
|
+
this.bulb.updateCharacteristic(C.Brightness, s.brightness);
|
|
88
|
+
if (chars.includes('Hue') && s.hue != null)
|
|
89
|
+
this.bulb.updateCharacteristic(C.Hue, s.hue);
|
|
90
|
+
if (chars.includes('Saturation') && s.saturation != null)
|
|
91
|
+
this.bulb.updateCharacteristic(C.Saturation, s.saturation);
|
|
92
|
+
if (chars.includes('ColorTemperature') && s.colorTemperatureMireds != null) {
|
|
93
|
+
this.bulb.updateCharacteristic(C.ColorTemperature, s.colorTemperatureMireds);
|
|
94
|
+
}
|
|
66
95
|
}
|
|
67
96
|
schedule() {
|
|
68
97
|
if (this.pending)
|
|
69
98
|
clearTimeout(this.pending);
|
|
70
99
|
this.pending = setTimeout(() => {
|
|
71
100
|
this.pending = null;
|
|
72
|
-
const s = this.
|
|
101
|
+
const s = this.registry.get(this.fixture.id);
|
|
73
102
|
this.platform.log.info(`[${this.fixture.id}] HK set: on=${s.on} br=${s.brightness}` +
|
|
74
103
|
(s.hue != null ? ` h=${s.hue}` : '') +
|
|
75
104
|
(s.saturation != null ? ` s=${s.saturation}` : '') +
|
|
76
105
|
(s.colorTemperatureMireds != null ? ` ct=${s.colorTemperatureMireds}` : ''));
|
|
77
|
-
this.controller.setFixture(this.fixture,
|
|
106
|
+
this.controller.setFixture(this.fixture, s);
|
|
78
107
|
}, CHARACTERISTIC_UPDATE_DELAY_MS);
|
|
79
108
|
}
|
|
80
109
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platformAccessory.js","sourceRoot":"","sources":["../src/platformAccessory.ts"],"names":[],"mappings":"AAAA,sEAAsE;
|
|
1
|
+
{"version":3,"file":"platformAccessory.js","sourceRoot":"","sources":["../src/platformAccessory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HomeKitLightState } from './color/types.js';
|
|
2
|
+
export type StateChangeListener = (fixtureId: string) => void;
|
|
3
|
+
export declare class StateRegistry {
|
|
4
|
+
private states;
|
|
5
|
+
private listeners;
|
|
6
|
+
get(id: string): HomeKitLightState;
|
|
7
|
+
/** Replace a fixture's state and fire change listeners. */
|
|
8
|
+
set(id: string, state: HomeKitLightState): void;
|
|
9
|
+
/** Merge a partial update into a fixture's state, returning the new full
|
|
10
|
+
* state. Fires listeners. */
|
|
11
|
+
update(id: string, patch: Partial<HomeKitLightState>): HomeKitLightState;
|
|
12
|
+
onChange(cb: StateChangeListener): () => void;
|
|
13
|
+
}
|
|
14
|
+
/** Bucket members' values; return the value with the largest bucket. Ties
|
|
15
|
+
* break in favour of the value held by the first member in
|
|
16
|
+
* alphabetical-id order. */
|
|
17
|
+
export declare function pickMajority<T>(items: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
v: T;
|
|
20
|
+
}>): T | undefined;
|
|
21
|
+
//# sourceMappingURL=stateRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateRegistry.d.ts","sourceRoot":"","sources":["../src/stateRegistry.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,mBAAmB,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;AAI9D,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAwC;IACtD,OAAO,CAAC,SAAS,CAAkC;IAEnD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB;IAIlC,2DAA2D;IAC3D,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAK/C;kCAC8B;IAC9B,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB;IAMxE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,GAAG,MAAM,IAAI;CAI9C;AAED;;6BAE6B;AAC7B,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAoBjF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// StateRegistry — central store of every fixture's HomeKit-visible state.
|
|
2
|
+
//
|
|
3
|
+
// Every StickFixture and ZoneFixture reads/writes through here. Whenever a
|
|
4
|
+
// fixture's state changes, listeners fire so zones can refresh their own
|
|
5
|
+
// HomeKit characteristics (and vice versa: setting a zone updates each
|
|
6
|
+
// member's state in the registry, which fires events, which prompt the
|
|
7
|
+
// per-fixture accessories to update their HomeKit-displayed state).
|
|
8
|
+
//
|
|
9
|
+
// Majority rule for zone state: see pickMajority() — count occurrences of
|
|
10
|
+
// each value across (alphabetically-sorted) members; largest count wins;
|
|
11
|
+
// ties are broken in favour of the value held by the alphabetically-first
|
|
12
|
+
// member.
|
|
13
|
+
const DEFAULT_STATE = { on: false, brightness: 100 };
|
|
14
|
+
export class StateRegistry {
|
|
15
|
+
states = new Map();
|
|
16
|
+
listeners = new Set();
|
|
17
|
+
get(id) {
|
|
18
|
+
return this.states.get(id) ?? { ...DEFAULT_STATE };
|
|
19
|
+
}
|
|
20
|
+
/** Replace a fixture's state and fire change listeners. */
|
|
21
|
+
set(id, state) {
|
|
22
|
+
this.states.set(id, { ...state });
|
|
23
|
+
for (const l of this.listeners)
|
|
24
|
+
l(id);
|
|
25
|
+
}
|
|
26
|
+
/** Merge a partial update into a fixture's state, returning the new full
|
|
27
|
+
* state. Fires listeners. */
|
|
28
|
+
update(id, patch) {
|
|
29
|
+
const next = { ...this.get(id), ...patch };
|
|
30
|
+
this.set(id, next);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
onChange(cb) {
|
|
34
|
+
this.listeners.add(cb);
|
|
35
|
+
return () => this.listeners.delete(cb);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Bucket members' values; return the value with the largest bucket. Ties
|
|
39
|
+
* break in favour of the value held by the first member in
|
|
40
|
+
* alphabetical-id order. */
|
|
41
|
+
export function pickMajority(items) {
|
|
42
|
+
if (items.length === 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
const sorted = items.slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
45
|
+
const counts = new Map();
|
|
46
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
47
|
+
const v = sorted[i].v;
|
|
48
|
+
const rec = counts.get(v);
|
|
49
|
+
if (rec)
|
|
50
|
+
rec.count++;
|
|
51
|
+
else
|
|
52
|
+
counts.set(v, { count: 1, firstIdx: i });
|
|
53
|
+
}
|
|
54
|
+
let bestV = sorted[0].v;
|
|
55
|
+
let bestRec = counts.get(bestV);
|
|
56
|
+
for (const [v, rec] of counts) {
|
|
57
|
+
if (rec.count > bestRec.count ||
|
|
58
|
+
(rec.count === bestRec.count && rec.firstIdx < bestRec.firstIdx)) {
|
|
59
|
+
bestV = v;
|
|
60
|
+
bestRec = rec;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return bestV;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=stateRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateRegistry.js","sourceRoot":"","sources":["../src/stateRegistry.ts"],"names":[],"mappings":"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"}
|
package/dist/zoneAccessory.d.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import type { PlatformAccessory } from 'homebridge';
|
|
2
|
-
import { Zone } from './config.js';
|
|
2
|
+
import { Fixture, Zone } from './config.js';
|
|
3
3
|
import { StickController } from './controller.js';
|
|
4
4
|
import type { DmxPlatform } from './platform.js';
|
|
5
|
+
import { StateRegistry } from './stateRegistry.js';
|
|
5
6
|
export declare class ZoneFixture {
|
|
6
7
|
private readonly platform;
|
|
7
8
|
private readonly zone;
|
|
8
9
|
private readonly controllers;
|
|
9
|
-
private
|
|
10
|
+
private readonly registry;
|
|
10
11
|
private pending;
|
|
11
|
-
|
|
12
|
-
private
|
|
12
|
+
private refreshPending;
|
|
13
|
+
private bulb;
|
|
14
|
+
constructor(platform: DmxPlatform, accessory: PlatformAccessory, zone: Zone, controllers: Map<string, StickController>, registry: StateRegistry);
|
|
15
|
+
/** Compute majority state across members, per characteristic. */
|
|
16
|
+
private majorityState;
|
|
17
|
+
/** A HomeKit set on the zone: build the new state from the current
|
|
18
|
+
* majority + the patch the user is applying, write it into every
|
|
19
|
+
* member, and trigger the controller after a small debounce so
|
|
20
|
+
* multi-characteristic dispatch coalesces. */
|
|
21
|
+
private applyToMembers;
|
|
22
|
+
/** Debounced controller dispatch. Sends current registry state for each
|
|
23
|
+
* member to the appropriate StickController. */
|
|
24
|
+
private scheduleDispatch;
|
|
25
|
+
/** Debounced refresh: when members change, push the new majority to
|
|
26
|
+
* the zone's HomeKit characteristics. */
|
|
27
|
+
private scheduleRefresh;
|
|
13
28
|
}
|
|
29
|
+
export type _MemberRef = Fixture;
|
|
14
30
|
//# sourceMappingURL=zoneAccessory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zoneAccessory.d.ts","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"zoneAccessory.d.ts","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAgB,MAAM,oBAAoB,CAAC;AAEjE,qBAAa,WAAW;IAMpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAT3B,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,IAAI,CAAU;gBAGH,QAAQ,EAAE,WAAW,EACtC,SAAS,EAAE,iBAAiB,EACX,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,QAAQ,EAAE,aAAa;IAmD1C,iEAAiE;IACjE,OAAO,CAAC,aAAa;IAWrB;;;mDAG+C;IAC/C,OAAO,CAAC,cAAc;IAWtB;qDACiD;IACjD,OAAO,CAAC,gBAAgB;IAuBxB;8CAC0C;IAC1C,OAAO,CAAC,eAAe;CAqBxB;AAGD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC"}
|
package/dist/zoneAccessory.js
CHANGED
|
@@ -1,83 +1,146 @@
|
|
|
1
|
-
// ZoneFixture — virtual HomeKit Lightbulb that
|
|
2
|
-
//
|
|
3
|
-
// zone's local state, then calls controller.setFixture for each member
|
|
4
|
-
// using the zone's current state. The single controller debounce
|
|
5
|
-
// coalesces all member updates into one subprocess transaction.
|
|
1
|
+
// ZoneFixture — virtual HomeKit Lightbulb that aggregates a set of member
|
|
2
|
+
// fixtures.
|
|
6
3
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Reads: each .onGet returns the MAJORITY value of that characteristic
|
|
5
|
+
// across the zone's members. Ties are broken in favour of the value held
|
|
6
|
+
// by the alphabetically-first member. This gives a deterministic display
|
|
7
|
+
// even when members are in split states.
|
|
8
|
+
//
|
|
9
|
+
// Writes: each .onSet updates every member's state in the registry (which
|
|
10
|
+
// fires per-member change events so the individual fixture accessories
|
|
11
|
+
// refresh their own HomeKit displays), then triggers the controller for
|
|
12
|
+
// each member so the DMX wire reflects the change.
|
|
13
|
+
//
|
|
14
|
+
// When ANY member's state changes (by any path), the zone's HomeKit
|
|
15
|
+
// characteristics push the new majority — so zones containing overlapping
|
|
16
|
+
// members stay coherent with each other.
|
|
10
17
|
import { CHARACTERISTIC_UPDATE_DELAY_MS } from './settings.js';
|
|
18
|
+
import { pickMajority } from './stateRegistry.js';
|
|
11
19
|
export class ZoneFixture {
|
|
12
20
|
platform;
|
|
13
21
|
zone;
|
|
14
22
|
controllers;
|
|
15
|
-
|
|
23
|
+
registry;
|
|
16
24
|
pending = null;
|
|
17
|
-
|
|
25
|
+
refreshPending = null;
|
|
26
|
+
bulb;
|
|
27
|
+
constructor(platform, accessory, zone, controllers, registry) {
|
|
18
28
|
this.platform = platform;
|
|
19
29
|
this.zone = zone;
|
|
20
30
|
this.controllers = controllers;
|
|
31
|
+
this.registry = registry;
|
|
21
32
|
const C = platform.Characteristic;
|
|
22
33
|
const info = accessory.getService(platform.Service.AccessoryInformation);
|
|
23
34
|
info?.setCharacteristic(C.Manufacturer, 'DMX')
|
|
24
35
|
?.setCharacteristic(C.Model, `Zone (${zone.members.length} fixtures)`)
|
|
25
36
|
?.setCharacteristic(C.SerialNumber, `zone-${zone.id}`);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
bulb.getCharacteristic(C.On)
|
|
31
|
-
.onGet(() => this.state.on)
|
|
32
|
-
.onSet((v) => { this.state.on = Boolean(v); this.schedule(); });
|
|
37
|
+
this.bulb =
|
|
38
|
+
accessory.getService(platform.Service.Lightbulb)
|
|
39
|
+
?? accessory.addService(platform.Service.Lightbulb, zone.name);
|
|
40
|
+
this.bulb.setCharacteristic(C.Name, zone.name);
|
|
33
41
|
const has = (k) => zone.characteristics.includes(k);
|
|
42
|
+
this.bulb.getCharacteristic(C.On)
|
|
43
|
+
.onGet(() => this.majorityState().on)
|
|
44
|
+
.onSet((v) => this.applyToMembers({ on: Boolean(v) }));
|
|
34
45
|
if (has('Brightness')) {
|
|
35
|
-
bulb.getCharacteristic(C.Brightness)
|
|
36
|
-
.onGet(() => this.
|
|
37
|
-
.onSet((v) => {
|
|
38
|
-
this.state.brightness = Number(v);
|
|
39
|
-
this.schedule();
|
|
40
|
-
});
|
|
46
|
+
this.bulb.getCharacteristic(C.Brightness)
|
|
47
|
+
.onGet(() => this.majorityState().brightness)
|
|
48
|
+
.onSet((v) => this.applyToMembers({ brightness: Number(v) }));
|
|
41
49
|
}
|
|
42
50
|
if (has('Hue')) {
|
|
43
|
-
bulb.getCharacteristic(C.Hue)
|
|
44
|
-
.onGet(() => this.
|
|
45
|
-
.onSet((v) => {
|
|
51
|
+
this.bulb.getCharacteristic(C.Hue)
|
|
52
|
+
.onGet(() => this.majorityState().hue ?? 0)
|
|
53
|
+
.onSet((v) => this.applyToMembers({ hue: Number(v) }));
|
|
46
54
|
}
|
|
47
55
|
if (has('Saturation')) {
|
|
48
|
-
bulb.getCharacteristic(C.Saturation)
|
|
49
|
-
.onGet(() => this.
|
|
50
|
-
.onSet((v) => {
|
|
56
|
+
this.bulb.getCharacteristic(C.Saturation)
|
|
57
|
+
.onGet(() => this.majorityState().saturation ?? 0)
|
|
58
|
+
.onSet((v) => this.applyToMembers({ saturation: Number(v) }));
|
|
51
59
|
}
|
|
52
60
|
if (has('ColorTemperature')) {
|
|
53
|
-
bulb.getCharacteristic(C.ColorTemperature)
|
|
54
|
-
.onGet(() => this.
|
|
55
|
-
.onSet((v) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
this.bulb.getCharacteristic(C.ColorTemperature)
|
|
62
|
+
.onGet(() => this.majorityState().colorTemperatureMireds ?? 200)
|
|
63
|
+
.onSet((v) => this.applyToMembers({
|
|
64
|
+
colorTemperatureMireds: Number(v),
|
|
65
|
+
saturation: 0,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
// Refresh on any member change.
|
|
69
|
+
const memberIds = new Set(zone.members.map((m) => m.id));
|
|
70
|
+
registry.onChange((id) => {
|
|
71
|
+
if (memberIds.has(id))
|
|
72
|
+
this.scheduleRefresh();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Compute majority state across members, per characteristic. */
|
|
76
|
+
majorityState() {
|
|
77
|
+
const ss = this.zone.members.map((m) => ({ id: m.id, state: this.registry.get(m.id) }));
|
|
78
|
+
return {
|
|
79
|
+
on: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.on }))) ?? false,
|
|
80
|
+
brightness: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.brightness }))) ?? 100,
|
|
81
|
+
hue: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.hue }))),
|
|
82
|
+
saturation: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.saturation }))),
|
|
83
|
+
colorTemperatureMireds: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.colorTemperatureMireds }))),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** A HomeKit set on the zone: build the new state from the current
|
|
87
|
+
* majority + the patch the user is applying, write it into every
|
|
88
|
+
* member, and trigger the controller after a small debounce so
|
|
89
|
+
* multi-characteristic dispatch coalesces. */
|
|
90
|
+
applyToMembers(patch) {
|
|
91
|
+
const next = { ...this.majorityState(), ...patch };
|
|
92
|
+
// Push the next state into every member's registry entry. This fires
|
|
93
|
+
// per-fixture change events so the individual StickFixture accessories
|
|
94
|
+
// refresh their own HomeKit displays.
|
|
95
|
+
for (const m of this.zone.members) {
|
|
96
|
+
this.registry.update(m.id, next);
|
|
60
97
|
}
|
|
98
|
+
this.scheduleDispatch();
|
|
61
99
|
}
|
|
62
|
-
|
|
100
|
+
/** Debounced controller dispatch. Sends current registry state for each
|
|
101
|
+
* member to the appropriate StickController. */
|
|
102
|
+
scheduleDispatch() {
|
|
63
103
|
if (this.pending)
|
|
64
104
|
clearTimeout(this.pending);
|
|
65
105
|
this.pending = setTimeout(() => {
|
|
66
106
|
this.pending = null;
|
|
67
|
-
const s = this.
|
|
107
|
+
const s = this.majorityState();
|
|
68
108
|
this.platform.log.info(`[zone:${this.zone.id}] HK set: on=${s.on} br=${s.brightness}` +
|
|
69
109
|
(s.hue != null ? ` h=${s.hue}` : '') +
|
|
70
110
|
(s.saturation != null ? ` s=${s.saturation}` : '') +
|
|
71
111
|
(s.colorTemperatureMireds != null ? ` ct=${s.colorTemperatureMireds}` : '') +
|
|
72
112
|
` → ${this.zone.members.length} members`);
|
|
73
|
-
// Dispatch the zone's state to each member via that member's controller.
|
|
74
113
|
for (const m of this.zone.members) {
|
|
75
114
|
const c = this.controllers.get(m.controller.id);
|
|
76
115
|
if (!c) {
|
|
77
116
|
this.platform.log.error(`zone "${this.zone.id}": no controller for "${m.id}"`);
|
|
78
117
|
continue;
|
|
79
118
|
}
|
|
80
|
-
c.setFixture(m, this.
|
|
119
|
+
c.setFixture(m, this.registry.get(m.id));
|
|
120
|
+
}
|
|
121
|
+
}, CHARACTERISTIC_UPDATE_DELAY_MS);
|
|
122
|
+
}
|
|
123
|
+
/** Debounced refresh: when members change, push the new majority to
|
|
124
|
+
* the zone's HomeKit characteristics. */
|
|
125
|
+
scheduleRefresh() {
|
|
126
|
+
if (this.refreshPending)
|
|
127
|
+
clearTimeout(this.refreshPending);
|
|
128
|
+
this.refreshPending = setTimeout(() => {
|
|
129
|
+
this.refreshPending = null;
|
|
130
|
+
const s = this.majorityState();
|
|
131
|
+
const C = this.platform.Characteristic;
|
|
132
|
+
this.bulb.updateCharacteristic(C.On, s.on);
|
|
133
|
+
if (this.zone.characteristics.includes('Brightness')) {
|
|
134
|
+
this.bulb.updateCharacteristic(C.Brightness, s.brightness);
|
|
135
|
+
}
|
|
136
|
+
if (this.zone.characteristics.includes('Hue') && s.hue != null) {
|
|
137
|
+
this.bulb.updateCharacteristic(C.Hue, s.hue);
|
|
138
|
+
}
|
|
139
|
+
if (this.zone.characteristics.includes('Saturation') && s.saturation != null) {
|
|
140
|
+
this.bulb.updateCharacteristic(C.Saturation, s.saturation);
|
|
141
|
+
}
|
|
142
|
+
if (this.zone.characteristics.includes('ColorTemperature') && s.colorTemperatureMireds != null) {
|
|
143
|
+
this.bulb.updateCharacteristic(C.ColorTemperature, s.colorTemperatureMireds);
|
|
81
144
|
}
|
|
82
145
|
}, CHARACTERISTIC_UPDATE_DELAY_MS);
|
|
83
146
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zoneAccessory.js","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"zoneAccessory.js","sourceRoot":"","sources":["../src/zoneAccessory.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/platform.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
import { Controller, LoadedConfig, loadConfig, RawConfig } from './config.js';
|
|
18
18
|
import { StickController } from './controller.js';
|
|
19
19
|
import { StickFixture } from './platformAccessory.js';
|
|
20
|
+
import { StateRegistry } from './stateRegistry.js';
|
|
20
21
|
import { ZoneFixture } from './zoneAccessory.js';
|
|
21
22
|
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
|
|
22
23
|
|
|
@@ -27,6 +28,7 @@ export class DmxPlatform implements DynamicPlatformPlugin {
|
|
|
27
28
|
|
|
28
29
|
private cfg: LoadedConfig | null = null;
|
|
29
30
|
private controllers = new Map<string, StickController>();
|
|
31
|
+
private registry = new StateRegistry();
|
|
30
32
|
|
|
31
33
|
constructor(
|
|
32
34
|
public readonly log: Logger,
|
|
@@ -96,12 +98,12 @@ export class DmxPlatform implements DynamicPlatformPlugin {
|
|
|
96
98
|
if (existing) {
|
|
97
99
|
this.log.debug(`wiring cached accessory ${fx.id}`);
|
|
98
100
|
existing.context.fixture = { id: fx.id, controllerId: fx.controller.id };
|
|
99
|
-
new StickFixture(this, existing, fx, controller);
|
|
101
|
+
new StickFixture(this, existing, fx, controller, this.registry);
|
|
100
102
|
} else {
|
|
101
103
|
this.log.info(`registering new accessory: ${fx.name} (${fx.id})`);
|
|
102
104
|
const accessory = new this.api.platformAccessory(fx.name, uuid);
|
|
103
105
|
accessory.context.fixture = { id: fx.id, controllerId: fx.controller.id };
|
|
104
|
-
new StickFixture(this, accessory, fx, controller);
|
|
106
|
+
new StickFixture(this, accessory, fx, controller, this.registry);
|
|
105
107
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
106
108
|
this.accessories.push(accessory);
|
|
107
109
|
}
|
|
@@ -115,12 +117,12 @@ export class DmxPlatform implements DynamicPlatformPlugin {
|
|
|
115
117
|
if (existing) {
|
|
116
118
|
this.log.debug(`wiring cached zone ${zone.id}`);
|
|
117
119
|
existing.context.zone = { id: zone.id };
|
|
118
|
-
new ZoneFixture(this, existing, zone, this.controllers);
|
|
120
|
+
new ZoneFixture(this, existing, zone, this.controllers, this.registry);
|
|
119
121
|
} else {
|
|
120
122
|
this.log.info(`registering new zone: ${zone.name} (${zone.id}, ${zone.members.length} members)`);
|
|
121
123
|
const accessory = new this.api.platformAccessory(zone.name, uuid);
|
|
122
124
|
accessory.context.zone = { id: zone.id };
|
|
123
|
-
new ZoneFixture(this, accessory, zone, this.controllers);
|
|
125
|
+
new ZoneFixture(this, accessory, zone, this.controllers, this.registry);
|
|
124
126
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
125
127
|
this.accessories.push(accessory);
|
|
126
128
|
}
|
package/src/platformAccessory.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// StickFixture — one HomeKit Lightbulb accessory per patched fixture.
|
|
2
|
+
//
|
|
3
|
+
// State is owned by the platform-wide StateRegistry. We read on .onGet,
|
|
4
|
+
// write on .onSet, and subscribe to changes so the HomeKit-displayed
|
|
5
|
+
// values track the registry (e.g. when a zone updates this fixture).
|
|
2
6
|
|
|
3
7
|
import type {
|
|
4
8
|
CharacteristicValue,
|
|
@@ -11,20 +15,18 @@ import { HomeKitLightState } from './color/types.js';
|
|
|
11
15
|
import { StickController } from './controller.js';
|
|
12
16
|
import type { DmxPlatform } from './platform.js';
|
|
13
17
|
import { CHARACTERISTIC_UPDATE_DELAY_MS } from './settings.js';
|
|
18
|
+
import { StateRegistry } from './stateRegistry.js';
|
|
14
19
|
|
|
15
20
|
export class StickFixture {
|
|
16
|
-
/** Current HomeKit state we'll send to the Stick on the next push. */
|
|
17
|
-
private state: HomeKitLightState = { on: false, brightness: 100 };
|
|
18
|
-
|
|
19
|
-
/** Debounce timer for HomeKit's multi-characteristic dispatch
|
|
20
|
-
* (Hue/Saturation/Brightness arrive as 3 separate set calls). */
|
|
21
21
|
private pending: NodeJS.Timeout | null = null;
|
|
22
|
+
private bulb: Service;
|
|
22
23
|
|
|
23
24
|
constructor(
|
|
24
25
|
private readonly platform: DmxPlatform,
|
|
25
26
|
private readonly accessory: PlatformAccessory,
|
|
26
27
|
private readonly fixture: Fixture,
|
|
27
28
|
private readonly controller: StickController,
|
|
29
|
+
private readonly registry: StateRegistry,
|
|
28
30
|
) {
|
|
29
31
|
const C = platform.Characteristic;
|
|
30
32
|
const info = accessory.getService(platform.Service.AccessoryInformation);
|
|
@@ -32,71 +34,91 @@ export class StickFixture {
|
|
|
32
34
|
?.setCharacteristic(C.Model, fixture.profile.name)
|
|
33
35
|
?.setCharacteristic(C.SerialNumber, fixture.id);
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
this.bulb =
|
|
36
38
|
accessory.getService(platform.Service.Lightbulb)
|
|
37
39
|
?? accessory.addService(platform.Service.Lightbulb, fixture.name);
|
|
38
40
|
|
|
39
|
-
bulb.setCharacteristic(C.Name, fixture.name);
|
|
41
|
+
this.bulb.setCharacteristic(C.Name, fixture.name);
|
|
40
42
|
|
|
41
43
|
const chars = fixture.profile.model.characteristics;
|
|
42
44
|
|
|
43
|
-
bulb.getCharacteristic(C.On)
|
|
44
|
-
.onGet(() =>
|
|
45
|
-
.onSet((v) => {
|
|
45
|
+
this.bulb.getCharacteristic(C.On)
|
|
46
|
+
.onGet(() => registry.get(fixture.id).on)
|
|
47
|
+
.onSet((v) => {
|
|
48
|
+
registry.update(fixture.id, { on: Boolean(v) });
|
|
49
|
+
this.schedule();
|
|
50
|
+
});
|
|
46
51
|
|
|
47
52
|
if (chars.includes('Brightness')) {
|
|
48
|
-
bulb.getCharacteristic(C.Brightness)
|
|
49
|
-
.onGet(() =>
|
|
53
|
+
this.bulb.getCharacteristic(C.Brightness)
|
|
54
|
+
.onGet(() => registry.get(fixture.id).brightness)
|
|
50
55
|
.onSet((v: CharacteristicValue) => {
|
|
51
|
-
|
|
56
|
+
registry.update(fixture.id, { brightness: Number(v) });
|
|
52
57
|
this.schedule();
|
|
53
58
|
});
|
|
54
59
|
}
|
|
55
|
-
|
|
56
60
|
if (chars.includes('Hue')) {
|
|
57
|
-
bulb.getCharacteristic(C.Hue)
|
|
58
|
-
.onGet(() =>
|
|
61
|
+
this.bulb.getCharacteristic(C.Hue)
|
|
62
|
+
.onGet(() => registry.get(fixture.id).hue ?? 0)
|
|
59
63
|
.onSet((v) => {
|
|
60
|
-
|
|
61
|
-
// setting hue implies color mode (sat>0); leave saturation as-is
|
|
64
|
+
registry.update(fixture.id, { hue: Number(v) });
|
|
62
65
|
this.schedule();
|
|
63
66
|
});
|
|
64
67
|
}
|
|
65
|
-
|
|
66
68
|
if (chars.includes('Saturation')) {
|
|
67
|
-
bulb.getCharacteristic(C.Saturation)
|
|
68
|
-
.onGet(() =>
|
|
69
|
+
this.bulb.getCharacteristic(C.Saturation)
|
|
70
|
+
.onGet(() => registry.get(fixture.id).saturation ?? 0)
|
|
69
71
|
.onSet((v) => {
|
|
70
|
-
|
|
72
|
+
registry.update(fixture.id, { saturation: Number(v) });
|
|
71
73
|
this.schedule();
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
|
-
|
|
75
76
|
if (chars.includes('ColorTemperature')) {
|
|
76
|
-
bulb.getCharacteristic(C.ColorTemperature)
|
|
77
|
-
.onGet(() =>
|
|
77
|
+
this.bulb.getCharacteristic(C.ColorTemperature)
|
|
78
|
+
.onGet(() => registry.get(fixture.id).colorTemperatureMireds ?? 200)
|
|
78
79
|
.onSet((v) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// CCT change implies white mode; zero saturation.
|
|
81
|
+
registry.update(fixture.id, {
|
|
82
|
+
colorTemperatureMireds: Number(v),
|
|
83
|
+
saturation: 0,
|
|
84
|
+
});
|
|
83
85
|
this.schedule();
|
|
84
86
|
});
|
|
85
87
|
}
|
|
88
|
+
|
|
89
|
+
// Refresh HomeKit characteristics whenever this fixture's state changes
|
|
90
|
+
// in the registry (e.g. via a zone set, or another channel of itself).
|
|
91
|
+
registry.onChange((changedId) => {
|
|
92
|
+
if (changedId !== fixture.id) return;
|
|
93
|
+
this.pushFromRegistry();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private pushFromRegistry(): void {
|
|
98
|
+
const s = this.registry.get(this.fixture.id);
|
|
99
|
+
const C = this.platform.Characteristic;
|
|
100
|
+
const chars = this.fixture.profile.model.characteristics;
|
|
101
|
+
this.bulb.updateCharacteristic(C.On, s.on);
|
|
102
|
+
if (chars.includes('Brightness')) this.bulb.updateCharacteristic(C.Brightness, s.brightness);
|
|
103
|
+
if (chars.includes('Hue') && s.hue != null) this.bulb.updateCharacteristic(C.Hue, s.hue);
|
|
104
|
+
if (chars.includes('Saturation') && s.saturation != null) this.bulb.updateCharacteristic(C.Saturation, s.saturation);
|
|
105
|
+
if (chars.includes('ColorTemperature') && s.colorTemperatureMireds != null) {
|
|
106
|
+
this.bulb.updateCharacteristic(C.ColorTemperature, s.colorTemperatureMireds);
|
|
107
|
+
}
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
private schedule(): void {
|
|
89
111
|
if (this.pending) clearTimeout(this.pending);
|
|
90
112
|
this.pending = setTimeout(() => {
|
|
91
113
|
this.pending = null;
|
|
92
|
-
const s = this.
|
|
114
|
+
const s = this.registry.get(this.fixture.id);
|
|
93
115
|
this.platform.log.info(
|
|
94
116
|
`[${this.fixture.id}] HK set: on=${s.on} br=${s.brightness}` +
|
|
95
117
|
(s.hue != null ? ` h=${s.hue}` : '') +
|
|
96
118
|
(s.saturation != null ? ` s=${s.saturation}` : '') +
|
|
97
119
|
(s.colorTemperatureMireds != null ? ` ct=${s.colorTemperatureMireds}` : ''),
|
|
98
120
|
);
|
|
99
|
-
this.controller.setFixture(this.fixture,
|
|
121
|
+
this.controller.setFixture(this.fixture, s);
|
|
100
122
|
}, CHARACTERISTIC_UPDATE_DELAY_MS);
|
|
101
123
|
}
|
|
102
124
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// StateRegistry — central store of every fixture's HomeKit-visible state.
|
|
2
|
+
//
|
|
3
|
+
// Every StickFixture and ZoneFixture reads/writes through here. Whenever a
|
|
4
|
+
// fixture's state changes, listeners fire so zones can refresh their own
|
|
5
|
+
// HomeKit characteristics (and vice versa: setting a zone updates each
|
|
6
|
+
// member's state in the registry, which fires events, which prompt the
|
|
7
|
+
// per-fixture accessories to update their HomeKit-displayed state).
|
|
8
|
+
//
|
|
9
|
+
// Majority rule for zone state: see pickMajority() — count occurrences of
|
|
10
|
+
// each value across (alphabetically-sorted) members; largest count wins;
|
|
11
|
+
// ties are broken in favour of the value held by the alphabetically-first
|
|
12
|
+
// member.
|
|
13
|
+
|
|
14
|
+
import { HomeKitLightState } from './color/types.js';
|
|
15
|
+
|
|
16
|
+
export type StateChangeListener = (fixtureId: string) => void;
|
|
17
|
+
|
|
18
|
+
const DEFAULT_STATE: HomeKitLightState = { on: false, brightness: 100 };
|
|
19
|
+
|
|
20
|
+
export class StateRegistry {
|
|
21
|
+
private states = new Map<string, HomeKitLightState>();
|
|
22
|
+
private listeners = new Set<StateChangeListener>();
|
|
23
|
+
|
|
24
|
+
get(id: string): HomeKitLightState {
|
|
25
|
+
return this.states.get(id) ?? { ...DEFAULT_STATE };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Replace a fixture's state and fire change listeners. */
|
|
29
|
+
set(id: string, state: HomeKitLightState): void {
|
|
30
|
+
this.states.set(id, { ...state });
|
|
31
|
+
for (const l of this.listeners) l(id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Merge a partial update into a fixture's state, returning the new full
|
|
35
|
+
* state. Fires listeners. */
|
|
36
|
+
update(id: string, patch: Partial<HomeKitLightState>): HomeKitLightState {
|
|
37
|
+
const next: HomeKitLightState = { ...this.get(id), ...patch };
|
|
38
|
+
this.set(id, next);
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onChange(cb: StateChangeListener): () => void {
|
|
43
|
+
this.listeners.add(cb);
|
|
44
|
+
return () => this.listeners.delete(cb);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Bucket members' values; return the value with the largest bucket. Ties
|
|
49
|
+
* break in favour of the value held by the first member in
|
|
50
|
+
* alphabetical-id order. */
|
|
51
|
+
export function pickMajority<T>(items: Array<{ id: string; v: T }>): T | undefined {
|
|
52
|
+
if (items.length === 0) return undefined;
|
|
53
|
+
const sorted = items.slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
54
|
+
const counts = new Map<T, { count: number; firstIdx: number }>();
|
|
55
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
56
|
+
const v = sorted[i].v;
|
|
57
|
+
const rec = counts.get(v);
|
|
58
|
+
if (rec) rec.count++;
|
|
59
|
+
else counts.set(v, { count: 1, firstIdx: i });
|
|
60
|
+
}
|
|
61
|
+
let bestV: T = sorted[0].v;
|
|
62
|
+
let bestRec = counts.get(bestV)!;
|
|
63
|
+
for (const [v, rec] of counts) {
|
|
64
|
+
if (rec.count > bestRec.count ||
|
|
65
|
+
(rec.count === bestRec.count && rec.firstIdx < bestRec.firstIdx)) {
|
|
66
|
+
bestV = v;
|
|
67
|
+
bestRec = rec;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return bestV;
|
|
71
|
+
}
|
package/src/zoneAccessory.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
// ZoneFixture — virtual HomeKit Lightbulb that
|
|
2
|
-
//
|
|
3
|
-
// zone's local state, then calls controller.setFixture for each member
|
|
4
|
-
// using the zone's current state. The single controller debounce
|
|
5
|
-
// coalesces all member updates into one subprocess transaction.
|
|
1
|
+
// ZoneFixture — virtual HomeKit Lightbulb that aggregates a set of member
|
|
2
|
+
// fixtures.
|
|
6
3
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Reads: each .onGet returns the MAJORITY value of that characteristic
|
|
5
|
+
// across the zone's members. Ties are broken in favour of the value held
|
|
6
|
+
// by the alphabetically-first member. This gives a deterministic display
|
|
7
|
+
// even when members are in split states.
|
|
8
|
+
//
|
|
9
|
+
// Writes: each .onSet updates every member's state in the registry (which
|
|
10
|
+
// fires per-member change events so the individual fixture accessories
|
|
11
|
+
// refresh their own HomeKit displays), then triggers the controller for
|
|
12
|
+
// each member so the DMX wire reflects the change.
|
|
13
|
+
//
|
|
14
|
+
// When ANY member's state changes (by any path), the zone's HomeKit
|
|
15
|
+
// characteristics push the new majority — so zones containing overlapping
|
|
16
|
+
// members stay coherent with each other.
|
|
10
17
|
|
|
11
18
|
import type {
|
|
12
19
|
CharacteristicValue,
|
|
@@ -14,21 +21,24 @@ import type {
|
|
|
14
21
|
Service,
|
|
15
22
|
} from 'homebridge';
|
|
16
23
|
|
|
17
|
-
import { Zone } from './config.js';
|
|
24
|
+
import { Fixture, Zone } from './config.js';
|
|
18
25
|
import { HKCharacteristic, HomeKitLightState } from './color/types.js';
|
|
19
26
|
import { StickController } from './controller.js';
|
|
20
27
|
import type { DmxPlatform } from './platform.js';
|
|
21
28
|
import { CHARACTERISTIC_UPDATE_DELAY_MS } from './settings.js';
|
|
29
|
+
import { StateRegistry, pickMajority } from './stateRegistry.js';
|
|
22
30
|
|
|
23
31
|
export class ZoneFixture {
|
|
24
|
-
private state: HomeKitLightState = { on: false, brightness: 100 };
|
|
25
32
|
private pending: NodeJS.Timeout | null = null;
|
|
33
|
+
private refreshPending: NodeJS.Timeout | null = null;
|
|
34
|
+
private bulb: Service;
|
|
26
35
|
|
|
27
36
|
constructor(
|
|
28
37
|
private readonly platform: DmxPlatform,
|
|
29
38
|
accessory: PlatformAccessory,
|
|
30
39
|
private readonly zone: Zone,
|
|
31
40
|
private readonly controllers: Map<string, StickController>,
|
|
41
|
+
private readonly registry: StateRegistry,
|
|
32
42
|
) {
|
|
33
43
|
const C = platform.Characteristic;
|
|
34
44
|
const info = accessory.getService(platform.Service.AccessoryInformation);
|
|
@@ -36,53 +46,83 @@ export class ZoneFixture {
|
|
|
36
46
|
?.setCharacteristic(C.Model, `Zone (${zone.members.length} fixtures)`)
|
|
37
47
|
?.setCharacteristic(C.SerialNumber, `zone-${zone.id}`);
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
this.bulb =
|
|
40
50
|
accessory.getService(platform.Service.Lightbulb)
|
|
41
51
|
?? accessory.addService(platform.Service.Lightbulb, zone.name);
|
|
42
52
|
|
|
43
|
-
bulb.setCharacteristic(C.Name, zone.name);
|
|
44
|
-
|
|
45
|
-
// Always expose On.
|
|
46
|
-
bulb.getCharacteristic(C.On)
|
|
47
|
-
.onGet(() => this.state.on)
|
|
48
|
-
.onSet((v) => { this.state.on = Boolean(v); this.schedule(); });
|
|
53
|
+
this.bulb.setCharacteristic(C.Name, zone.name);
|
|
49
54
|
|
|
50
55
|
const has = (k: HKCharacteristic): boolean => zone.characteristics.includes(k);
|
|
51
56
|
|
|
57
|
+
this.bulb.getCharacteristic(C.On)
|
|
58
|
+
.onGet(() => this.majorityState().on)
|
|
59
|
+
.onSet((v) => this.applyToMembers({ on: Boolean(v) }));
|
|
60
|
+
|
|
52
61
|
if (has('Brightness')) {
|
|
53
|
-
bulb.getCharacteristic(C.Brightness)
|
|
54
|
-
.onGet(() => this.
|
|
55
|
-
.onSet((v: CharacteristicValue) => {
|
|
56
|
-
this.state.brightness = Number(v);
|
|
57
|
-
this.schedule();
|
|
58
|
-
});
|
|
62
|
+
this.bulb.getCharacteristic(C.Brightness)
|
|
63
|
+
.onGet(() => this.majorityState().brightness)
|
|
64
|
+
.onSet((v: CharacteristicValue) => this.applyToMembers({ brightness: Number(v) }));
|
|
59
65
|
}
|
|
60
66
|
if (has('Hue')) {
|
|
61
|
-
bulb.getCharacteristic(C.Hue)
|
|
62
|
-
.onGet(() => this.
|
|
63
|
-
.onSet((v) => {
|
|
67
|
+
this.bulb.getCharacteristic(C.Hue)
|
|
68
|
+
.onGet(() => this.majorityState().hue ?? 0)
|
|
69
|
+
.onSet((v) => this.applyToMembers({ hue: Number(v) }));
|
|
64
70
|
}
|
|
65
71
|
if (has('Saturation')) {
|
|
66
|
-
bulb.getCharacteristic(C.Saturation)
|
|
67
|
-
.onGet(() => this.
|
|
68
|
-
.onSet((v) => {
|
|
72
|
+
this.bulb.getCharacteristic(C.Saturation)
|
|
73
|
+
.onGet(() => this.majorityState().saturation ?? 0)
|
|
74
|
+
.onSet((v) => this.applyToMembers({ saturation: Number(v) }));
|
|
69
75
|
}
|
|
70
76
|
if (has('ColorTemperature')) {
|
|
71
|
-
bulb.getCharacteristic(C.ColorTemperature)
|
|
72
|
-
.onGet(() => this.
|
|
73
|
-
.onSet((v) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
this.bulb.getCharacteristic(C.ColorTemperature)
|
|
78
|
+
.onGet(() => this.majorityState().colorTemperatureMireds ?? 200)
|
|
79
|
+
.onSet((v) => this.applyToMembers({
|
|
80
|
+
colorTemperatureMireds: Number(v),
|
|
81
|
+
saturation: 0,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Refresh on any member change.
|
|
86
|
+
const memberIds = new Set(zone.members.map((m) => m.id));
|
|
87
|
+
registry.onChange((id) => {
|
|
88
|
+
if (memberIds.has(id)) this.scheduleRefresh();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Compute majority state across members, per characteristic. */
|
|
93
|
+
private majorityState(): HomeKitLightState {
|
|
94
|
+
const ss = this.zone.members.map((m) => ({ id: m.id, state: this.registry.get(m.id) }));
|
|
95
|
+
return {
|
|
96
|
+
on: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.on }))) ?? false,
|
|
97
|
+
brightness: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.brightness }))) ?? 100,
|
|
98
|
+
hue: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.hue }))),
|
|
99
|
+
saturation: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.saturation }))),
|
|
100
|
+
colorTemperatureMireds: pickMajority(ss.map((s) => ({ id: s.id, v: s.state.colorTemperatureMireds }))),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A HomeKit set on the zone: build the new state from the current
|
|
105
|
+
* majority + the patch the user is applying, write it into every
|
|
106
|
+
* member, and trigger the controller after a small debounce so
|
|
107
|
+
* multi-characteristic dispatch coalesces. */
|
|
108
|
+
private applyToMembers(patch: Partial<HomeKitLightState>): void {
|
|
109
|
+
const next = { ...this.majorityState(), ...patch };
|
|
110
|
+
// Push the next state into every member's registry entry. This fires
|
|
111
|
+
// per-fixture change events so the individual StickFixture accessories
|
|
112
|
+
// refresh their own HomeKit displays.
|
|
113
|
+
for (const m of this.zone.members) {
|
|
114
|
+
this.registry.update(m.id, next);
|
|
78
115
|
}
|
|
116
|
+
this.scheduleDispatch();
|
|
79
117
|
}
|
|
80
118
|
|
|
81
|
-
|
|
119
|
+
/** Debounced controller dispatch. Sends current registry state for each
|
|
120
|
+
* member to the appropriate StickController. */
|
|
121
|
+
private scheduleDispatch(): void {
|
|
82
122
|
if (this.pending) clearTimeout(this.pending);
|
|
83
123
|
this.pending = setTimeout(() => {
|
|
84
124
|
this.pending = null;
|
|
85
|
-
const s = this.
|
|
125
|
+
const s = this.majorityState();
|
|
86
126
|
this.platform.log.info(
|
|
87
127
|
`[zone:${this.zone.id}] HK set: on=${s.on} br=${s.brightness}` +
|
|
88
128
|
(s.hue != null ? ` h=${s.hue}` : '') +
|
|
@@ -90,15 +130,41 @@ export class ZoneFixture {
|
|
|
90
130
|
(s.colorTemperatureMireds != null ? ` ct=${s.colorTemperatureMireds}` : '') +
|
|
91
131
|
` → ${this.zone.members.length} members`,
|
|
92
132
|
);
|
|
93
|
-
// Dispatch the zone's state to each member via that member's controller.
|
|
94
133
|
for (const m of this.zone.members) {
|
|
95
134
|
const c = this.controllers.get(m.controller.id);
|
|
96
135
|
if (!c) {
|
|
97
136
|
this.platform.log.error(`zone "${this.zone.id}": no controller for "${m.id}"`);
|
|
98
137
|
continue;
|
|
99
138
|
}
|
|
100
|
-
c.setFixture(m, this.
|
|
139
|
+
c.setFixture(m, this.registry.get(m.id));
|
|
140
|
+
}
|
|
141
|
+
}, CHARACTERISTIC_UPDATE_DELAY_MS);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Debounced refresh: when members change, push the new majority to
|
|
145
|
+
* the zone's HomeKit characteristics. */
|
|
146
|
+
private scheduleRefresh(): void {
|
|
147
|
+
if (this.refreshPending) clearTimeout(this.refreshPending);
|
|
148
|
+
this.refreshPending = setTimeout(() => {
|
|
149
|
+
this.refreshPending = null;
|
|
150
|
+
const s = this.majorityState();
|
|
151
|
+
const C = this.platform.Characteristic;
|
|
152
|
+
this.bulb.updateCharacteristic(C.On, s.on);
|
|
153
|
+
if (this.zone.characteristics.includes('Brightness')) {
|
|
154
|
+
this.bulb.updateCharacteristic(C.Brightness, s.brightness);
|
|
155
|
+
}
|
|
156
|
+
if (this.zone.characteristics.includes('Hue') && s.hue != null) {
|
|
157
|
+
this.bulb.updateCharacteristic(C.Hue, s.hue);
|
|
158
|
+
}
|
|
159
|
+
if (this.zone.characteristics.includes('Saturation') && s.saturation != null) {
|
|
160
|
+
this.bulb.updateCharacteristic(C.Saturation, s.saturation);
|
|
161
|
+
}
|
|
162
|
+
if (this.zone.characteristics.includes('ColorTemperature') && s.colorTemperatureMireds != null) {
|
|
163
|
+
this.bulb.updateCharacteristic(C.ColorTemperature, s.colorTemperatureMireds);
|
|
101
164
|
}
|
|
102
165
|
}, CHARACTERISTIC_UPDATE_DELAY_MS);
|
|
103
166
|
}
|
|
104
167
|
}
|
|
168
|
+
|
|
169
|
+
// Silence "Fixture imported but unused" — kept for potential future use.
|
|
170
|
+
export type _MemberRef = Fixture;
|