homebridge-broadlink-rm-blaster 1.7.1 → 1.9.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 +20 -0
- package/README.md +7 -1
- package/config.schema.json +111 -2
- 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/temperatureHumiditySensorAccessory.d.ts +3 -1
- package/dist/accessories/temperatureHumiditySensorAccessory.js +4 -2
- package/dist/accessories/temperatureHumiditySensorAccessory.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/configTypes.d.ts +7 -0
- package/dist/mqttClient.d.ts +5 -2
- package/dist/mqttClient.js +16 -4
- package/dist/mqttClient.js.map +1 -1
- package/dist/mqttLastSeen.d.ts +3 -0
- package/dist/mqttLastSeen.js +34 -0
- package/dist/mqttLastSeen.js.map +1 -0
- package/dist/platform.js +3 -3
- 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,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## [1.9.0] - 2026-08-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Simple on/off accessories, advanced accessories, TVs and fans: a "Minimum Time
|
|
13
|
+
Between Switching On and Off" option (default 1 second). A signal that arrives
|
|
14
|
+
before that much time has passed since the last accepted one is not sent straight
|
|
15
|
+
away, and the Home app tile snaps back to reflect the real, unchanged state. It
|
|
16
|
+
isn't lost either - the first such signal is held and applied once the window
|
|
17
|
+
clears. Any further signal during that same window is dropped, so a burst of rapid
|
|
18
|
+
taps or MQTT commands settles as one signal, then one more. Both HomeKit and MQTT
|
|
19
|
+
behave the same way.
|
|
20
|
+
|
|
21
|
+
## [1.8.0] - 2026-08-19
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- MQTT: a `last_seen` field on every publish (sensor readings and accessory state),
|
|
25
|
+
similar to zigbee2mqtt. Format is set platform-wide (ISO 8601 UTC by default, ISO
|
|
26
|
+
8601 local time, epoch milliseconds, or disabled), with a per-RM-device override
|
|
27
|
+
for that device's own sensor readings.
|
|
28
|
+
|
|
9
29
|
## [1.7.1] - 2026-08-16
|
|
10
30
|
|
|
11
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Broadlink RM Blaster 1.
|
|
1
|
+
# Broadlink RM Blaster 1.9.0
|
|
2
2
|
|
|
3
3
|
**This Homebridge plugin has been 100% vibe coded with Claude.**
|
|
4
4
|
|
|
@@ -75,6 +75,12 @@ you untick "Retain state messages" on that accessory, so a subscriber
|
|
|
75
75
|
connecting later immediately gets the last known state. Commands are JSON,
|
|
76
76
|
and anything left out is not changed.
|
|
77
77
|
|
|
78
|
+
Every publish - state messages and sensor readings alike - also carries a
|
|
79
|
+
`last_seen` field, similar to zigbee2mqtt. Set the format ("last_seen
|
|
80
|
+
Format" in the MQTT settings) to ISO 8601 UTC (default), ISO 8601 local
|
|
81
|
+
time, epoch milliseconds, or disable it entirely. Each RM device can
|
|
82
|
+
override the format for its own sensor readings.
|
|
83
|
+
|
|
78
84
|
### Simple on/off accessories, advanced accessories and TVs
|
|
79
85
|
|
|
80
86
|
```json
|
package/config.schema.json
CHANGED
|
@@ -65,6 +65,42 @@
|
|
|
65
65
|
"functionBody": "return model.enableMqtt === true;"
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
|
+
"mqttLastSeenFormat": {
|
|
69
|
+
"title": "last_seen Format",
|
|
70
|
+
"type": "string",
|
|
71
|
+
"required": false,
|
|
72
|
+
"default": "iso8601",
|
|
73
|
+
"description": "Adds a last_seen field to every MQTT publish (sensor readings and accessory state), same idea as zigbee2mqtt. Can be overridden per RM device below.",
|
|
74
|
+
"oneOf": [
|
|
75
|
+
{
|
|
76
|
+
"title": "ISO 8601 (UTC)",
|
|
77
|
+
"enum": [
|
|
78
|
+
"iso8601"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"title": "ISO 8601 (local time)",
|
|
83
|
+
"enum": [
|
|
84
|
+
"iso8601local"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"title": "Epoch (ms)",
|
|
89
|
+
"enum": [
|
|
90
|
+
"epoch"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"title": "Disabled",
|
|
95
|
+
"enum": [
|
|
96
|
+
"disabled"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"condition": {
|
|
101
|
+
"functionBody": "return model.enableMqtt === true;"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
68
104
|
"rmDevices": {
|
|
69
105
|
"title": "Broadlink RM Devices",
|
|
70
106
|
"type": "array",
|
|
@@ -108,6 +144,41 @@
|
|
|
108
144
|
"condition": {
|
|
109
145
|
"functionBody": "return model.enableMqtt === true && model.rmDevices && model.rmDevices[arrayIndices] && model.rmDevices[arrayIndices].enableMqttPublish === true;"
|
|
110
146
|
}
|
|
147
|
+
},
|
|
148
|
+
"mqttLastSeenFormat": {
|
|
149
|
+
"title": "last_seen Format Override",
|
|
150
|
+
"type": "string",
|
|
151
|
+
"required": false,
|
|
152
|
+
"description": "Overrides the platform-wide last_seen format above, for this device's readings only. Leave unset to use the platform setting.",
|
|
153
|
+
"oneOf": [
|
|
154
|
+
{
|
|
155
|
+
"title": "ISO 8601 (UTC)",
|
|
156
|
+
"enum": [
|
|
157
|
+
"iso8601"
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"title": "ISO 8601 (local time)",
|
|
162
|
+
"enum": [
|
|
163
|
+
"iso8601local"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"title": "Epoch (ms)",
|
|
168
|
+
"enum": [
|
|
169
|
+
"epoch"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"title": "Disabled",
|
|
174
|
+
"enum": [
|
|
175
|
+
"disabled"
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
"condition": {
|
|
180
|
+
"functionBody": "return model.enableMqtt === true && model.rmDevices && model.rmDevices[arrayIndices] && model.rmDevices[arrayIndices].enableMqttPublish === true;"
|
|
181
|
+
}
|
|
111
182
|
}
|
|
112
183
|
}
|
|
113
184
|
}
|
|
@@ -171,6 +242,14 @@
|
|
|
171
242
|
"type": "string",
|
|
172
243
|
"required": false
|
|
173
244
|
},
|
|
245
|
+
"switchCooldownSeconds": {
|
|
246
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
247
|
+
"type": "number",
|
|
248
|
+
"minimum": 0,
|
|
249
|
+
"default": 1,
|
|
250
|
+
"required": false,
|
|
251
|
+
"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."
|
|
252
|
+
},
|
|
174
253
|
"mqttSubscribe": {
|
|
175
254
|
"title": "Control via MQTT",
|
|
176
255
|
"type": "boolean",
|
|
@@ -225,6 +304,14 @@
|
|
|
225
304
|
"type": "string",
|
|
226
305
|
"required": false
|
|
227
306
|
},
|
|
307
|
+
"switchCooldownSeconds": {
|
|
308
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
309
|
+
"type": "number",
|
|
310
|
+
"minimum": 0,
|
|
311
|
+
"default": 1,
|
|
312
|
+
"required": false,
|
|
313
|
+
"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."
|
|
314
|
+
},
|
|
228
315
|
"timeoutSeconds": {
|
|
229
316
|
"title": "Timeout Between Signals (seconds)",
|
|
230
317
|
"type": "number",
|
|
@@ -383,6 +470,14 @@
|
|
|
383
470
|
"type": "string",
|
|
384
471
|
"required": false
|
|
385
472
|
},
|
|
473
|
+
"switchCooldownSeconds": {
|
|
474
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
475
|
+
"type": "number",
|
|
476
|
+
"minimum": 0,
|
|
477
|
+
"default": 1,
|
|
478
|
+
"required": false,
|
|
479
|
+
"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."
|
|
480
|
+
},
|
|
386
481
|
"onFollowUpName": {
|
|
387
482
|
"title": "Signal After Power On - Name",
|
|
388
483
|
"type": "string",
|
|
@@ -679,6 +774,14 @@
|
|
|
679
774
|
"type": "string",
|
|
680
775
|
"required": false
|
|
681
776
|
},
|
|
777
|
+
"switchCooldownSeconds": {
|
|
778
|
+
"title": "Minimum Time Between Switching On and Off (seconds)",
|
|
779
|
+
"type": "number",
|
|
780
|
+
"minimum": 0,
|
|
781
|
+
"default": 1,
|
|
782
|
+
"required": false,
|
|
783
|
+
"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."
|
|
784
|
+
},
|
|
682
785
|
"volumeUpCode": {
|
|
683
786
|
"title": "Volume Up Signal",
|
|
684
787
|
"type": "string",
|
|
@@ -778,7 +881,8 @@
|
|
|
778
881
|
"rmDevices[].ip",
|
|
779
882
|
"rmDevices[].enableTemperatureHumidity",
|
|
780
883
|
"rmDevices[].enableMqttPublish",
|
|
781
|
-
"rmDevices[].mqttRetain"
|
|
884
|
+
"rmDevices[].mqttRetain",
|
|
885
|
+
"rmDevices[].mqttLastSeenFormat"
|
|
782
886
|
]
|
|
783
887
|
},
|
|
784
888
|
{
|
|
@@ -799,7 +903,8 @@
|
|
|
799
903
|
"key": "mqttPassword",
|
|
800
904
|
"type": "password"
|
|
801
905
|
},
|
|
802
|
-
"mqttBaseTopic"
|
|
906
|
+
"mqttBaseTopic",
|
|
907
|
+
"mqttLastSeenFormat"
|
|
803
908
|
]
|
|
804
909
|
},
|
|
805
910
|
{
|
|
@@ -818,6 +923,7 @@
|
|
|
818
923
|
"accessories[].accessoryType",
|
|
819
924
|
"accessories[].powerOnCode",
|
|
820
925
|
"accessories[].powerOffCode",
|
|
926
|
+
"accessories[].switchCooldownSeconds",
|
|
821
927
|
"accessories[].mqttSubscribe",
|
|
822
928
|
"accessories[].mqttTopic",
|
|
823
929
|
"accessories[].mqttRetain"
|
|
@@ -837,6 +943,7 @@
|
|
|
837
943
|
"advancedAccessories[].name",
|
|
838
944
|
"advancedAccessories[].rmDevice",
|
|
839
945
|
"advancedAccessories[].offCode",
|
|
946
|
+
"advancedAccessories[].switchCooldownSeconds",
|
|
840
947
|
"advancedAccessories[].timeoutSeconds",
|
|
841
948
|
"advancedAccessories[].mqttSubscribe",
|
|
842
949
|
"advancedAccessories[].mqttTopic",
|
|
@@ -882,6 +989,7 @@
|
|
|
882
989
|
"fans[].powerToggleCode",
|
|
883
990
|
"fans[].powerOnCode",
|
|
884
991
|
"fans[].powerOffCode",
|
|
992
|
+
"fans[].switchCooldownSeconds",
|
|
885
993
|
"fans[].onFollowUpName",
|
|
886
994
|
"fans[].onFollowUpCode",
|
|
887
995
|
"fans[].onFollowUpPressCount",
|
|
@@ -962,6 +1070,7 @@
|
|
|
962
1070
|
"tvs[].rmDevice",
|
|
963
1071
|
"tvs[].powerOnCode",
|
|
964
1072
|
"tvs[].powerOffCode",
|
|
1073
|
+
"tvs[].switchCooldownSeconds",
|
|
965
1074
|
"tvs[].volumeUpCode",
|
|
966
1075
|
"tvs[].volumeDownCode",
|
|
967
1076
|
"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
|
}
|