homebridge-broadlink-rm-blaster 1.8.0 → 1.10.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 +25 -0
- package/README.md +1 -1
- package/config.schema.json +47 -10
- package/dist/accessories/advancedAccessory.d.ts +4 -1
- package/dist/accessories/advancedAccessory.js +26 -8
- package/dist/accessories/advancedAccessory.js.map +1 -1
- package/dist/accessories/basicAccessory.d.ts +3 -1
- package/dist/accessories/basicAccessory.js +30 -6
- package/dist/accessories/basicAccessory.js.map +1 -1
- package/dist/accessories/fanAccessory.d.ts +3 -0
- package/dist/accessories/fanAccessory.js +41 -4
- package/dist/accessories/fanAccessory.js.map +1 -1
- package/dist/accessories/tvAccessory.d.ts +4 -1
- package/dist/accessories/tvAccessory.js +32 -8
- package/dist/accessories/tvAccessory.js.map +1 -1
- package/dist/configFile.d.ts +6 -0
- package/dist/configFile.js +29 -0
- package/dist/configFile.js.map +1 -1
- package/dist/configTypes.d.ts +6 -0
- package/dist/mqttBroker.d.ts +7 -0
- package/dist/mqttBroker.js +53 -0
- package/dist/mqttBroker.js.map +1 -0
- package/dist/platform.js +35 -10
- package/dist/platform.js.map +1 -1
- package/dist/switchCooldown.d.ts +10 -0
- package/dist/switchCooldown.js +73 -0
- package/dist/switchCooldown.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## [1.10.0] - 2026-08-21
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- MQTT settings: the broker is now a single "Broker" field taking `host:port`
|
|
13
|
+
(default `localhost:1883`) rather than separate Host and Port fields, and
|
|
14
|
+
Username/Password only appear once a new "Broker requires authentication"
|
|
15
|
+
checkbox is ticked.
|
|
16
|
+
- An existing config is folded over to the new shape automatically on startup -
|
|
17
|
+
the old port is merged into the broker address and the authentication checkbox
|
|
18
|
+
is set from whether credentials were already filled in. `config.json` is backed
|
|
19
|
+
up to `config.json.backup` first, same as the existing blank-row cleanup, and
|
|
20
|
+
nothing is written when there's nothing to change.
|
|
21
|
+
|
|
22
|
+
## [1.9.0] - 2026-08-21
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Simple on/off accessories, advanced accessories, TVs and fans: a "Minimum Time
|
|
26
|
+
Between Switching On and Off" option (default 1 second). A signal that arrives
|
|
27
|
+
before that much time has passed since the last accepted one is not sent straight
|
|
28
|
+
away, and the Home app tile snaps back to reflect the real, unchanged state. It
|
|
29
|
+
isn't lost either - the first such signal is held and applied once the window
|
|
30
|
+
clears. Any further signal during that same window is dropped, so a burst of rapid
|
|
31
|
+
taps or MQTT commands settles as one signal, then one more. Both HomeKit and MQTT
|
|
32
|
+
behave the same way.
|
|
33
|
+
|
|
9
34
|
## [1.8.0] - 2026-08-19
|
|
10
35
|
|
|
11
36
|
### Added
|
package/README.md
CHANGED
package/config.schema.json
CHANGED
|
@@ -20,19 +20,20 @@
|
|
|
20
20
|
"description": "Publish temperature/humidity readings from RM devices to an MQTT broker."
|
|
21
21
|
},
|
|
22
22
|
"mqttHost": {
|
|
23
|
-
"title": "
|
|
23
|
+
"title": "Broker",
|
|
24
24
|
"type": "string",
|
|
25
|
-
"default": "localhost",
|
|
25
|
+
"default": "localhost:1883",
|
|
26
|
+
"placeholder": "localhost:1883",
|
|
26
27
|
"required": false,
|
|
27
|
-
"description": "Broker hostname or IP address.",
|
|
28
|
+
"description": "Broker hostname or IP address, optionally followed by a port. Without one, port 1883 is used.",
|
|
28
29
|
"condition": {
|
|
29
30
|
"functionBody": "return model.enableMqtt === true;"
|
|
30
31
|
}
|
|
31
32
|
},
|
|
32
|
-
"
|
|
33
|
-
"title": "
|
|
34
|
-
"type": "
|
|
35
|
-
"default":
|
|
33
|
+
"mqttRequiresAuth": {
|
|
34
|
+
"title": "Broker requires authentication",
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"default": false,
|
|
36
37
|
"required": false,
|
|
37
38
|
"condition": {
|
|
38
39
|
"functionBody": "return model.enableMqtt === true;"
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"type": "string",
|
|
44
45
|
"required": false,
|
|
45
46
|
"condition": {
|
|
46
|
-
"functionBody": "return model.enableMqtt === true;"
|
|
47
|
+
"functionBody": "return model.enableMqtt === true && model.mqttRequiresAuth === true;"
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
"mqttPassword": {
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"type": "string",
|
|
52
53
|
"required": false,
|
|
53
54
|
"condition": {
|
|
54
|
-
"functionBody": "return model.enableMqtt === true;"
|
|
55
|
+
"functionBody": "return model.enableMqtt === true && model.mqttRequiresAuth === true;"
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
"mqttBaseTopic": {
|
|
@@ -242,6 +243,14 @@
|
|
|
242
243
|
"type": "string",
|
|
243
244
|
"required": false
|
|
244
245
|
},
|
|
246
|
+
"switchCooldownSeconds": {
|
|
247
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
248
|
+
"type": "number",
|
|
249
|
+
"minimum": 0,
|
|
250
|
+
"default": 1,
|
|
251
|
+
"required": false,
|
|
252
|
+
"description": "A signal that arrives before this much time has passed since the last accepted one is held rather than sent straight away, and applied once the time is up. Any further signal in that same window is dropped."
|
|
253
|
+
},
|
|
245
254
|
"mqttSubscribe": {
|
|
246
255
|
"title": "Control via MQTT",
|
|
247
256
|
"type": "boolean",
|
|
@@ -296,6 +305,14 @@
|
|
|
296
305
|
"type": "string",
|
|
297
306
|
"required": false
|
|
298
307
|
},
|
|
308
|
+
"switchCooldownSeconds": {
|
|
309
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
310
|
+
"type": "number",
|
|
311
|
+
"minimum": 0,
|
|
312
|
+
"default": 1,
|
|
313
|
+
"required": false,
|
|
314
|
+
"description": "A signal that arrives before this much time has passed since the last accepted one is held rather than sent straight away, and applied once the time is up. Any further signal in that same window is dropped."
|
|
315
|
+
},
|
|
299
316
|
"timeoutSeconds": {
|
|
300
317
|
"title": "Timeout Between Signals (seconds)",
|
|
301
318
|
"type": "number",
|
|
@@ -454,6 +471,14 @@
|
|
|
454
471
|
"type": "string",
|
|
455
472
|
"required": false
|
|
456
473
|
},
|
|
474
|
+
"switchCooldownSeconds": {
|
|
475
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
476
|
+
"type": "number",
|
|
477
|
+
"minimum": 0,
|
|
478
|
+
"default": 1,
|
|
479
|
+
"required": false,
|
|
480
|
+
"description": "A signal that arrives before this much time has passed since the last accepted one is held rather than sent straight away, and applied once the time is up. Any further signal in that same window is dropped."
|
|
481
|
+
},
|
|
457
482
|
"onFollowUpName": {
|
|
458
483
|
"title": "Signal After Power On - Name",
|
|
459
484
|
"type": "string",
|
|
@@ -750,6 +775,14 @@
|
|
|
750
775
|
"type": "string",
|
|
751
776
|
"required": false
|
|
752
777
|
},
|
|
778
|
+
"switchCooldownSeconds": {
|
|
779
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
780
|
+
"type": "number",
|
|
781
|
+
"minimum": 0,
|
|
782
|
+
"default": 1,
|
|
783
|
+
"required": false,
|
|
784
|
+
"description": "A signal that arrives before this much time has passed since the last accepted one is held rather than sent straight away, and applied once the time is up. Any further signal in that same window is dropped."
|
|
785
|
+
},
|
|
753
786
|
"volumeUpCode": {
|
|
754
787
|
"title": "Volume Up Signal",
|
|
755
788
|
"type": "string",
|
|
@@ -865,7 +898,7 @@
|
|
|
865
898
|
"items": [
|
|
866
899
|
"enableMqtt",
|
|
867
900
|
"mqttHost",
|
|
868
|
-
"
|
|
901
|
+
"mqttRequiresAuth",
|
|
869
902
|
"mqttUsername",
|
|
870
903
|
{
|
|
871
904
|
"key": "mqttPassword",
|
|
@@ -891,6 +924,7 @@
|
|
|
891
924
|
"accessories[].accessoryType",
|
|
892
925
|
"accessories[].powerOnCode",
|
|
893
926
|
"accessories[].powerOffCode",
|
|
927
|
+
"accessories[].switchCooldownSeconds",
|
|
894
928
|
"accessories[].mqttSubscribe",
|
|
895
929
|
"accessories[].mqttTopic",
|
|
896
930
|
"accessories[].mqttRetain"
|
|
@@ -910,6 +944,7 @@
|
|
|
910
944
|
"advancedAccessories[].name",
|
|
911
945
|
"advancedAccessories[].rmDevice",
|
|
912
946
|
"advancedAccessories[].offCode",
|
|
947
|
+
"advancedAccessories[].switchCooldownSeconds",
|
|
913
948
|
"advancedAccessories[].timeoutSeconds",
|
|
914
949
|
"advancedAccessories[].mqttSubscribe",
|
|
915
950
|
"advancedAccessories[].mqttTopic",
|
|
@@ -955,6 +990,7 @@
|
|
|
955
990
|
"fans[].powerToggleCode",
|
|
956
991
|
"fans[].powerOnCode",
|
|
957
992
|
"fans[].powerOffCode",
|
|
993
|
+
"fans[].switchCooldownSeconds",
|
|
958
994
|
"fans[].onFollowUpName",
|
|
959
995
|
"fans[].onFollowUpCode",
|
|
960
996
|
"fans[].onFollowUpPressCount",
|
|
@@ -1035,6 +1071,7 @@
|
|
|
1035
1071
|
"tvs[].rmDevice",
|
|
1036
1072
|
"tvs[].powerOnCode",
|
|
1037
1073
|
"tvs[].powerOffCode",
|
|
1074
|
+
"tvs[].switchCooldownSeconds",
|
|
1038
1075
|
"tvs[].volumeUpCode",
|
|
1039
1076
|
"tvs[].volumeDownCode",
|
|
1040
1077
|
"tvs[].muteCode",
|
|
@@ -6,10 +6,13 @@ export declare class AdvancedAccessory {
|
|
|
6
6
|
private readonly accessory;
|
|
7
7
|
private readonly config;
|
|
8
8
|
private readonly ip;
|
|
9
|
+
private readonly service;
|
|
9
10
|
private readonly mqtt;
|
|
11
|
+
private readonly cooldown;
|
|
10
12
|
constructor(platform: BroadlinkRMBlasterPlatform, accessory: PlatformAccessory, config: AdvancedAccessoryConfig, ip: string);
|
|
11
13
|
private getOn;
|
|
12
|
-
private
|
|
14
|
+
private setOnFromHomeKit;
|
|
15
|
+
private applySetOn;
|
|
13
16
|
private turnOff;
|
|
14
17
|
private sendSequence;
|
|
15
18
|
private scheduleAutoReset;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AdvancedAccessory = void 0;
|
|
4
4
|
const mqttLink_1 = require("../mqttLink");
|
|
5
|
+
const switchCooldown_1 = require("../switchCooldown");
|
|
5
6
|
const DEFAULT_TIMEOUT_SECONDS = 0.5;
|
|
7
|
+
const DEFAULT_SWITCH_COOLDOWN_SECONDS = 1;
|
|
6
8
|
// How long to wait before resetting an auto-resetting (no offCode) trigger
|
|
7
9
|
// back to off. Not a stylistic choice - HomeKit's own optimistic UI update
|
|
8
10
|
// ignores a follow-up characteristic update that arrives too soon after the
|
|
@@ -19,7 +21,9 @@ class AdvancedAccessory {
|
|
|
19
21
|
accessory;
|
|
20
22
|
config;
|
|
21
23
|
ip;
|
|
24
|
+
service;
|
|
22
25
|
mqtt;
|
|
26
|
+
cooldown;
|
|
23
27
|
constructor(platform, accessory, config, ip) {
|
|
24
28
|
this.platform = platform;
|
|
25
29
|
this.accessory = accessory;
|
|
@@ -27,15 +31,17 @@ class AdvancedAccessory {
|
|
|
27
31
|
this.ip = ip;
|
|
28
32
|
const service = this.accessory.getService(this.platform.Service.Switch)
|
|
29
33
|
?? this.accessory.addService(this.platform.Service.Switch);
|
|
34
|
+
this.service = service;
|
|
30
35
|
service.setCharacteristic(this.platform.Characteristic.Name, this.config.name);
|
|
36
|
+
this.cooldown = new switchCooldown_1.SwitchCooldown((this.config.switchCooldownSeconds ?? DEFAULT_SWITCH_COOLDOWN_SECONDS) * 1000);
|
|
31
37
|
service.getCharacteristic(this.platform.Characteristic.On)
|
|
32
38
|
.onGet(() => this.getOn())
|
|
33
|
-
.onSet((value) => this.
|
|
34
|
-
this.mqtt = new mqttLink_1.MqttLink(platform, config.name, config,
|
|
35
|
-
if (command.state
|
|
36
|
-
|
|
37
|
-
service.updateCharacteristic(this.platform.Characteristic.On, this.getOn());
|
|
39
|
+
.onSet((value) => this.setOnFromHomeKit(value));
|
|
40
|
+
this.mqtt = new mqttLink_1.MqttLink(platform, config.name, config, (command) => {
|
|
41
|
+
if (command.state === undefined) {
|
|
42
|
+
return;
|
|
38
43
|
}
|
|
44
|
+
return this.cooldown.applyWhenReady(Date.now(), command.state === 'on', (on) => this.applySetOn(on), undefined, (error) => this.platform.log.error(`Failed to apply a deferred MQTT On/Off to "${this.config.name}": ${error.message}`));
|
|
39
45
|
});
|
|
40
46
|
}
|
|
41
47
|
// Same as every other blaster accessory - no feedback from the device
|
|
@@ -43,8 +49,18 @@ class AdvancedAccessory {
|
|
|
43
49
|
getOn() {
|
|
44
50
|
return Boolean(this.accessory.context.on);
|
|
45
51
|
}
|
|
46
|
-
|
|
52
|
+
// Unlike the other accessory types, a same-direction repeat isn't a no-op
|
|
53
|
+
// here - every accepted On re-fires the signal sequence, which is exactly
|
|
54
|
+
// what the cooldown needs to guard against, so it gates every accepted
|
|
55
|
+
// call rather than only genuine state flips.
|
|
56
|
+
setOnFromHomeKit(value) {
|
|
47
57
|
const on = Boolean(value);
|
|
58
|
+
return this.cooldown.applyWhenReady(Date.now(), on, (v) => this.applySetOn(v), () => {
|
|
59
|
+
this.platform.log.warn(`Deferring ${on ? 'On' : 'Off'} for "${this.config.name}" - within the minimum switch interval.`);
|
|
60
|
+
setTimeout(() => this.service.updateCharacteristic(this.platform.Characteristic.On, this.getOn()), RESET_DELAY_MS);
|
|
61
|
+
}, (error) => this.platform.log.error(`Failed to apply a deferred On/Off to "${this.config.name}": ${error.message}`));
|
|
62
|
+
}
|
|
63
|
+
async applySetOn(on) {
|
|
48
64
|
if (!on) {
|
|
49
65
|
await this.turnOff();
|
|
50
66
|
return;
|
|
@@ -53,6 +69,7 @@ class AdvancedAccessory {
|
|
|
53
69
|
await this.sendSequence();
|
|
54
70
|
this.accessory.context.on = true;
|
|
55
71
|
this.mqtt.publishState(true);
|
|
72
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, true);
|
|
56
73
|
this.platform.log.info(`Sent signal sequence to ${this.config.name}`);
|
|
57
74
|
}
|
|
58
75
|
catch (error) {
|
|
@@ -69,12 +86,14 @@ class AdvancedAccessory {
|
|
|
69
86
|
// Momentary trigger - nothing to send, just reflect the state.
|
|
70
87
|
this.accessory.context.on = false;
|
|
71
88
|
this.mqtt.publishState(false);
|
|
89
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, false);
|
|
72
90
|
return;
|
|
73
91
|
}
|
|
74
92
|
try {
|
|
75
93
|
await this.platform.broadlinkClient.sendCode(this.ip, this.config.offCode);
|
|
76
94
|
this.accessory.context.on = false;
|
|
77
95
|
this.mqtt.publishState(false);
|
|
96
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, false);
|
|
78
97
|
this.platform.log.info(`Sent Off to ${this.config.name}`);
|
|
79
98
|
}
|
|
80
99
|
catch (error) {
|
|
@@ -97,8 +116,7 @@ class AdvancedAccessory {
|
|
|
97
116
|
setTimeout(() => {
|
|
98
117
|
this.accessory.context.on = false;
|
|
99
118
|
this.mqtt.publishState(false);
|
|
100
|
-
this.
|
|
101
|
-
?.updateCharacteristic(this.platform.Characteristic.On, false);
|
|
119
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, false);
|
|
102
120
|
}, RESET_DELAY_MS);
|
|
103
121
|
}
|
|
104
122
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"advancedAccessory.js","sourceRoot":"","sources":["../../src/accessories/advancedAccessory.ts"],"names":[],"mappings":";;;AAIA,0CAAuC;
|
|
1
|
+
{"version":3,"file":"advancedAccessory.js","sourceRoot":"","sources":["../../src/accessories/advancedAccessory.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,8 +10,10 @@ export declare class BasicAccessory {
|
|
|
10
10
|
private readonly ip;
|
|
11
11
|
private readonly service;
|
|
12
12
|
private readonly mqtt;
|
|
13
|
+
private readonly cooldown;
|
|
13
14
|
constructor(platform: BroadlinkRMBlasterPlatform, accessory: PlatformAccessory, config: BasicAccessoryConfig, ip: string);
|
|
14
15
|
private getOrCreateService;
|
|
15
16
|
private getOn;
|
|
16
|
-
private
|
|
17
|
+
private setOnFromHomeKit;
|
|
18
|
+
private applySetOn;
|
|
17
19
|
}
|
|
@@ -4,6 +4,11 @@ exports.BasicAccessory = void 0;
|
|
|
4
4
|
exports.selectPowerCode = selectPowerCode;
|
|
5
5
|
exports.powerSignalName = powerSignalName;
|
|
6
6
|
const mqttLink_1 = require("../mqttLink");
|
|
7
|
+
const switchCooldown_1 = require("../switchCooldown");
|
|
8
|
+
const DEFAULT_SWITCH_COOLDOWN_SECONDS = 1;
|
|
9
|
+
// Long enough that HomeKit's own optimistic UI doesn't ignore the update
|
|
10
|
+
// that snaps a refused switch back to its real state.
|
|
11
|
+
const REJECT_RESET_DELAY_MS = 1000;
|
|
7
12
|
function selectPowerCode(config, on) {
|
|
8
13
|
return on ? config.powerOnCode : (config.powerOffCode ?? config.powerOnCode);
|
|
9
14
|
}
|
|
@@ -23,6 +28,7 @@ class BasicAccessory {
|
|
|
23
28
|
ip;
|
|
24
29
|
service;
|
|
25
30
|
mqtt;
|
|
31
|
+
cooldown;
|
|
26
32
|
constructor(platform, accessory, config, ip) {
|
|
27
33
|
this.platform = platform;
|
|
28
34
|
this.accessory = accessory;
|
|
@@ -31,17 +37,18 @@ class BasicAccessory {
|
|
|
31
37
|
const service = this.getOrCreateService();
|
|
32
38
|
this.service = service;
|
|
33
39
|
service.setCharacteristic(this.platform.Characteristic.Name, this.config.name);
|
|
40
|
+
this.cooldown = new switchCooldown_1.SwitchCooldown((this.config.switchCooldownSeconds ?? DEFAULT_SWITCH_COOLDOWN_SECONDS) * 1000);
|
|
34
41
|
if (this.config.accessoryType === 'outlet') {
|
|
35
42
|
service.setCharacteristic(this.platform.Characteristic.OutletInUse, true);
|
|
36
43
|
}
|
|
37
44
|
service.getCharacteristic(this.platform.Characteristic.On)
|
|
38
45
|
.onGet(() => this.getOn())
|
|
39
|
-
.onSet((value) => this.
|
|
40
|
-
this.mqtt = new mqttLink_1.MqttLink(platform, config.name, config,
|
|
41
|
-
if (command.state
|
|
42
|
-
|
|
43
|
-
this.service.updateCharacteristic(this.platform.Characteristic.On, this.getOn());
|
|
46
|
+
.onSet((value) => this.setOnFromHomeKit(value));
|
|
47
|
+
this.mqtt = new mqttLink_1.MqttLink(platform, config.name, config, (command) => {
|
|
48
|
+
if (command.state === undefined) {
|
|
49
|
+
return;
|
|
44
50
|
}
|
|
51
|
+
return this.cooldown.applyWhenReady(Date.now(), command.state === 'on', (on) => this.applySetOn(on), undefined, (error) => this.platform.log.error(`Failed to apply a deferred MQTT On/Off to "${this.config.name}": ${error.message}`));
|
|
45
52
|
});
|
|
46
53
|
}
|
|
47
54
|
getOrCreateService() {
|
|
@@ -66,13 +73,30 @@ class BasicAccessory {
|
|
|
66
73
|
getOn() {
|
|
67
74
|
return Boolean(this.accessory.context.on);
|
|
68
75
|
}
|
|
69
|
-
|
|
76
|
+
// A signal within the minimum switch interval is refused - the tile
|
|
77
|
+
// snaps back to its real (unchanged) state shortly after, same
|
|
78
|
+
// "HomeKit's optimistic UI needs a real delay" lesson as every other
|
|
79
|
+
// auto-resetting switch in this codebase - but it isn't just dropped:
|
|
80
|
+
// the first refused signal in a window is still held and applied once
|
|
81
|
+
// the window clears (see SwitchCooldown.applyWhenReady). A same-state
|
|
82
|
+
// request is a pure no-op and never touches the cooldown at all.
|
|
83
|
+
setOnFromHomeKit(value) {
|
|
70
84
|
const on = Boolean(value);
|
|
85
|
+
if (on === this.getOn()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
return this.cooldown.applyWhenReady(Date.now(), on, (v) => this.applySetOn(v), () => {
|
|
89
|
+
this.platform.log.warn(`Deferring ${on ? 'On' : 'Off'} for "${this.config.name}" - within the minimum switch interval.`);
|
|
90
|
+
setTimeout(() => this.service.updateCharacteristic(this.platform.Characteristic.On, this.getOn()), REJECT_RESET_DELAY_MS);
|
|
91
|
+
}, (error) => this.platform.log.error(`Failed to apply a deferred On/Off to "${this.config.name}": ${error.message}`));
|
|
92
|
+
}
|
|
93
|
+
async applySetOn(on) {
|
|
71
94
|
const code = selectPowerCode(this.config, on);
|
|
72
95
|
try {
|
|
73
96
|
await this.platform.broadlinkClient.sendCode(this.ip, code);
|
|
74
97
|
this.accessory.context.on = on;
|
|
75
98
|
this.mqtt.publishState(on);
|
|
99
|
+
this.service.updateCharacteristic(this.platform.Characteristic.On, on);
|
|
76
100
|
this.platform.log.info(`Sent ${powerSignalName(this.config, on)} to ${this.config.name}`);
|
|
77
101
|
}
|
|
78
102
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basicAccessory.js","sourceRoot":"","sources":["../../src/accessories/basicAccessory.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"basicAccessory.js","sourceRoot":"","sources":["../../src/accessories/basicAccessory.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,12 +21,14 @@ export declare class FanAccessory {
|
|
|
21
21
|
private readonly speedCount;
|
|
22
22
|
private readonly modes;
|
|
23
23
|
private readonly mqtt;
|
|
24
|
+
private readonly cooldown;
|
|
24
25
|
private swingSwitchService?;
|
|
25
26
|
private speedDebounceTimer?;
|
|
26
27
|
private pendingSpeedPercent?;
|
|
27
28
|
private pendingActive;
|
|
28
29
|
constructor(platform: BroadlinkRMBlasterPlatform, accessory: PlatformAccessory, config: FanAccessoryConfig, ip: string);
|
|
29
30
|
private applyCommand;
|
|
31
|
+
private applyMqttActive;
|
|
30
32
|
private usableModes;
|
|
31
33
|
private pruneStaleServices;
|
|
32
34
|
private setUpModeService;
|
|
@@ -45,6 +47,7 @@ export declare class FanAccessory {
|
|
|
45
47
|
private repeat;
|
|
46
48
|
private fail;
|
|
47
49
|
private setActiveFromHomeKit;
|
|
50
|
+
private setActiveGated;
|
|
48
51
|
private setActive;
|
|
49
52
|
private powerOn;
|
|
50
53
|
private enterSpeedIfNeeded;
|
|
@@ -7,11 +7,15 @@ exports.levelForPercent = levelForPercent;
|
|
|
7
7
|
exports.computeCyclePresses = computeCyclePresses;
|
|
8
8
|
exports.computeStepPresses = computeStepPresses;
|
|
9
9
|
const mqttLink_1 = require("../mqttLink");
|
|
10
|
+
const switchCooldown_1 = require("../switchCooldown");
|
|
10
11
|
const DEFAULT_PRESS_INTERVAL_SECONDS = 0.5;
|
|
11
12
|
const DEFAULT_SPEED_DEBOUNCE_SECONDS = 0.5;
|
|
13
|
+
const DEFAULT_SWITCH_COOLDOWN_SECONDS = 1;
|
|
12
14
|
// Long enough that HomeKit's own optimistic UI doesn't ignore the update
|
|
13
15
|
// that puts a momentary switch back to off.
|
|
14
16
|
const RESYNC_RESET_DELAY_MS = 1000;
|
|
17
|
+
// Same lesson, for a signal refused by the switch cooldown instead.
|
|
18
|
+
const REJECT_RESET_DELAY_MS = 1000;
|
|
15
19
|
// Kept distinct from a feature's subtype, which is just its name.
|
|
16
20
|
const RESYNC_SUBTYPE = '__resync';
|
|
17
21
|
const SWING_SUBTYPE = '__swing';
|
|
@@ -70,6 +74,7 @@ class FanAccessory {
|
|
|
70
74
|
speedCount;
|
|
71
75
|
modes;
|
|
72
76
|
mqtt;
|
|
77
|
+
cooldown;
|
|
73
78
|
swingSwitchService;
|
|
74
79
|
speedDebounceTimer;
|
|
75
80
|
pendingSpeedPercent;
|
|
@@ -83,6 +88,7 @@ class FanAccessory {
|
|
|
83
88
|
this.speedDebounceMs = (this.config.speedDebounceSeconds ?? DEFAULT_SPEED_DEBOUNCE_SECONDS) * 1000;
|
|
84
89
|
this.speedCount = this.config.speedCount ?? 1;
|
|
85
90
|
this.modes = this.usableModes();
|
|
91
|
+
this.cooldown = new switchCooldown_1.SwitchCooldown((this.config.switchCooldownSeconds ?? DEFAULT_SWITCH_COOLDOWN_SECONDS) * 1000);
|
|
86
92
|
// getService() returns the first match for a UUID regardless of
|
|
87
93
|
// subtype, so find the fan by its lack of one.
|
|
88
94
|
this.fanService = this.accessory.services.find((service) => service.UUID === this.platform.Service.Fanv2.UUID && !service.subtype) ?? this.accessory.addService(this.platform.Service.Fanv2, this.config.name);
|
|
@@ -134,11 +140,11 @@ class FanAccessory {
|
|
|
134
140
|
if (command.state === 'off') {
|
|
135
141
|
// Off wins outright; a speed alongside it would just switch the fan
|
|
136
142
|
// straight back on.
|
|
137
|
-
await this.
|
|
143
|
+
await this.applyMqttActive(false);
|
|
138
144
|
return;
|
|
139
145
|
}
|
|
140
146
|
if (command.state === 'on') {
|
|
141
|
-
await this.
|
|
147
|
+
await this.applyMqttActive(true);
|
|
142
148
|
}
|
|
143
149
|
if (command.speedPercent !== undefined && this.speedCount > 1) {
|
|
144
150
|
await this.applySpeedPercent(command.speedPercent);
|
|
@@ -147,6 +153,13 @@ class FanAccessory {
|
|
|
147
153
|
await this.applySwing(command.swing);
|
|
148
154
|
}
|
|
149
155
|
}
|
|
156
|
+
// A command inside the switch cooldown window isn't dropped, same as a
|
|
157
|
+
// HomeKit one isn't - the first one in a window is held and applied once
|
|
158
|
+
// it clears (see SwitchCooldown.applyWhenReady). No onRefused here, since
|
|
159
|
+
// there's no tile to visually snap back for an MQTT-originated command.
|
|
160
|
+
applyMqttActive(on) {
|
|
161
|
+
return this.cooldown.applyWhenReady(Date.now(), on, (v) => this.setActive(v ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE), undefined, (error) => this.platform.log.error(`Failed to apply a deferred MQTT On/Off to "${this.config.name}": ${error.message}`));
|
|
162
|
+
}
|
|
150
163
|
// A feature's name becomes its service subtype, and HomeKit rejects a
|
|
151
164
|
// second service of the same type without a unique one - so a nameless
|
|
152
165
|
// entry (an empty row left behind in the config UI, say) would take the
|
|
@@ -260,6 +273,10 @@ class FanAccessory {
|
|
|
260
273
|
}
|
|
261
274
|
resyncState() {
|
|
262
275
|
this.cancelPendingSpeed();
|
|
276
|
+
// A resync abandons a held on/off signal too - it exists to forget
|
|
277
|
+
// everything the plugin assumes, and firing a queued signal moments
|
|
278
|
+
// later would immediately contradict that.
|
|
279
|
+
this.cooldown.cancelPending();
|
|
263
280
|
this.accessory.context.on = false;
|
|
264
281
|
this.accessory.context.speedLevel = 0;
|
|
265
282
|
this.accessory.context.speedEntered = false;
|
|
@@ -339,11 +356,29 @@ class FanAccessory {
|
|
|
339
356
|
setActiveFromHomeKit(value) {
|
|
340
357
|
const wantOn = value === this.platform.Characteristic.Active.ACTIVE;
|
|
341
358
|
if (!wantOn || this.speedCount <= 1) {
|
|
342
|
-
return this.
|
|
359
|
+
return this.setActiveGated(value);
|
|
343
360
|
}
|
|
344
361
|
this.pendingActive = true;
|
|
345
362
|
this.scheduleSpeedApply();
|
|
346
363
|
}
|
|
364
|
+
// The switch-cooldown gate for a HomeKit-originated transition: a signal
|
|
365
|
+
// within the minimum switch interval is refused - the tile snaps back to
|
|
366
|
+
// its real (unchanged) state shortly after, same lesson as every other
|
|
367
|
+
// auto-resetting switch in this codebase - but it isn't just dropped:
|
|
368
|
+
// the first refused signal in a window is still held and applied once
|
|
369
|
+
// the window clears (see SwitchCooldown.applyWhenReady). Shared by the
|
|
370
|
+
// immediate path above and the slider-debounce settle callback below,
|
|
371
|
+
// since both are "about to actually flip power" decision points.
|
|
372
|
+
async setActiveGated(value) {
|
|
373
|
+
const wantOn = value === this.platform.Characteristic.Active.ACTIVE;
|
|
374
|
+
if (wantOn === this.isOn()) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await this.cooldown.applyWhenReady(Date.now(), wantOn, (v) => this.setActive(v ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE), () => {
|
|
378
|
+
this.platform.log.warn(`Deferring ${wantOn ? 'On' : 'Off'} for "${this.config.name}" - within the minimum switch interval.`);
|
|
379
|
+
setTimeout(() => this.fanService.updateCharacteristic(this.platform.Characteristic.Active, this.getActive()), REJECT_RESET_DELAY_MS);
|
|
380
|
+
}, (error) => this.platform.log.error(`Failed to apply a deferred On/Off to "${this.config.name}": ${error.message}`));
|
|
381
|
+
}
|
|
347
382
|
async setActive(value) {
|
|
348
383
|
const wantOn = value === this.platform.Characteristic.Active.ACTIVE;
|
|
349
384
|
if (wantOn === this.isOn()) {
|
|
@@ -536,7 +571,9 @@ class FanAccessory {
|
|
|
536
571
|
void this.applySpeedPercent(percent);
|
|
537
572
|
}
|
|
538
573
|
else if (wantOn) {
|
|
539
|
-
void this.
|
|
574
|
+
void this.setActiveGated(this.platform.Characteristic.Active.ACTIVE).catch((error) => {
|
|
575
|
+
this.platform.log.error(`Failed to turn on "${this.config.name}": ${error.message}`);
|
|
576
|
+
});
|
|
540
577
|
}
|
|
541
578
|
}, this.speedDebounceMs);
|
|
542
579
|
}
|