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 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
@@ -1,4 +1,4 @@
1
- # Broadlink RM Blaster 1.8.0
1
+ # Broadlink RM Blaster 1.10.0
2
2
 
3
3
  **This Homebridge plugin has been 100% vibe coded with Claude.**
4
4
 
@@ -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": "Host",
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
- "mqttPort": {
33
- "title": "Port",
34
- "type": "integer",
35
- "default": 1883,
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
- "mqttPort",
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 setOn;
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.setOn(value));
34
- this.mqtt = new mqttLink_1.MqttLink(platform, config.name, config, async (command) => {
35
- if (command.state !== undefined) {
36
- await this.setOn(command.state === 'on');
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
- async setOn(value) {
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.accessory.getService(this.platform.Service.Switch)
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":"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"}
1
+ {"version":3,"file":"advancedAccessory.js","sourceRoot":"","sources":["../../src/accessories/advancedAccessory.ts"],"names":[],"mappings":";;;AAIA,0CAAuC;AACvC,sDAAmD;AAEnD,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,+BAA+B,GAAG,CAAC,CAAC;AAE1C,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,2EAA2E;AAC3E,oEAAoE;AACpE,0EAA0E;AAC1E,iCAAiC;AACjC,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAa,iBAAiB;IAMT;IACA;IACA;IACA;IARF,OAAO,CAAU;IACjB,IAAI,CAAW;IACf,QAAQ,CAAiB;IAE1C,YACmB,QAAoC,EACpC,SAA4B,EAC5B,MAA+B,EAC/B,EAAU;QAHV,aAAQ,GAAR,QAAQ,CAA4B;QACpC,cAAS,GAAT,SAAS,CAAmB;QAC5B,WAAM,GAAN,MAAM,CAAyB;QAC/B,OAAE,GAAF,EAAE,CAAQ;QAE3B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;eAClE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/E,IAAI,CAAC,QAAQ,GAAG,IAAI,+BAAc,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,+BAA+B,CAAC,GAAG,IAAI,CAAC,CAAC;QAElH,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;aACvD,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;aACzB,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;QAElD,IAAI,CAAC,IAAI,GAAG,IAAI,mBAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;YAClE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAChC,OAAO;YACT,CAAC;YACD,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CACjC,IAAI,CAAC,GAAG,EAAE,EACV,OAAO,CAAC,KAAK,KAAK,IAAI,EACtB,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,EAC3B,SAAS,EACT,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,8CAA8C,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CACnI,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,sEAAsE;IACtE,oEAAoE;IAC5D,KAAK;QACX,OAAO,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,0EAA0E;IAC1E,0EAA0E;IAC1E,uEAAuE;IACvE,6CAA6C;IACrC,gBAAgB,CAAC,KAA0B;QACjD,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CACjC,IAAI,CAAC,GAAG,EAAE,EACV,EAAE,EACF,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EACzB,GAAG,EAAE;YACH,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,IAAI,yCAAyC,CAAC,CAAC;YACzH,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;QACrH,CAAC,EACD,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,yCAAyC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAC9H,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,EAAW;QAClC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YAC1B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC;YACjC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACzE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACxE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA4B,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YACtG,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAC5D,MAAM,IAAI,cAAc,sDAAyC,CAAC;QACpE,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACzB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACzB,+DAA+D;YAC/D,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAC1E,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC3E,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAC1E,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA4B,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YACtG,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAC5D,MAAM,IAAI,cAAc,sDAAyC,CAAC;QACpE,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,uBAAuB,CAAC,GAAG,IAAI,CAAC;QACjF,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAEhC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACvE,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,KAAK,CAAC,SAAS,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;IACH,CAAC;IAEO,iBAAiB;QACvB,UAAU,CAAC,GAAG,EAAE;YACd,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC5E,CAAC,EAAE,cAAc,CAAC,CAAC;IACrB,CAAC;CACF;AA3HD,8CA2HC"}
@@ -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 setOn;
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.setOn(value));
40
- this.mqtt = new mqttLink_1.MqttLink(platform, config.name, config, async (command) => {
41
- if (command.state !== undefined) {
42
- await this.setOn(command.state === 'on');
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
- async setOn(value) {
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":"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"}
1
+ {"version":3,"file":"basicAccessory.js","sourceRoot":"","sources":["../../src/accessories/basicAccessory.ts"],"names":[],"mappings":";;;AAaA,0CAEC;AAKD,0CAQC;AAxBD,0CAAuC;AACvC,sDAAmD;AAEnD,MAAM,+BAA+B,GAAG,CAAC,CAAC;AAE1C,yEAAyE;AACzE,sDAAsD;AACtD,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,SAAgB,eAAe,CAAC,MAAkE,EAAE,EAAW;IAC7G,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,WAAW,CAAC,CAAC;AAC/E,CAAC;AAED,2EAA2E;AAC3E,yEAAyE;AACzE,gEAAgE;AAChE,SAAgB,eAAe,CAC7B,MAAkE,EAClE,EAAW;IAEX,IAAI,EAAE,EAAE,CAAC;QACP,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,+DAA+D,CAAC;AAC7G,CAAC;AAED,MAAa,cAAc;IAMN;IACA;IACA;IACA;IARF,OAAO,CAAU;IACjB,IAAI,CAAW;IACf,QAAQ,CAAiB;IAE1C,YACmB,QAAoC,EACpC,SAA4B,EAC5B,MAA4B,EAC5B,EAAU;QAHV,aAAQ,GAAR,QAAQ,CAA4B;QACpC,cAAS,GAAT,SAAS,CAAmB;QAC5B,WAAM,GAAN,MAAM,CAAsB;QAC5B,OAAE,GAAF,EAAE,CAAQ;QAE3B,MAAM,OAAO,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC1C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/E,IAAI,CAAC,QAAQ,GAAG,IAAI,+BAAc,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,+BAA+B,CAAC,GAAG,IAAI,CAAC,CAAC;QAElH,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;YAC3C,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC5E,CAAC;QAED,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;aACvD,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;aACzB,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;QAElD,IAAI,CAAC,IAAI,GAAG,IAAI,mBAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;YAClE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAChC,OAAO;YACT,CAAC;YACD,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CACjC,IAAI,CAAC,GAAG,EAAE,EACV,OAAO,CAAC,KAAK,KAAK,IAAI,EACtB,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,EAC3B,SAAS,EACT,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,8CAA8C,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CACnI,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,kBAAkB;QACxB,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAClC,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;uBACzD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC/D,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;uBACzD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC/D,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC;uBACtD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC5D,KAAK,OAAO,CAAC;YACb;gBACE,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC;uBAC5D,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,kFAAkF;IAC1E,KAAK;QACX,OAAO,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,oEAAoE;IACpE,+DAA+D;IAC/D,qEAAqE;IACrE,sEAAsE;IACtE,sEAAsE;IACtE,sEAAsE;IACtE,iEAAiE;IACzD,gBAAgB,CAAC,KAA0B;QACjD,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,EAAE,KAAK,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CACjC,IAAI,CAAC,GAAG,EAAE,EACV,EAAE,EACF,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EACzB,GAAG,EAAE;YACH,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,IAAI,yCAAyC,CAAC,CAAC;YACzH,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;QAC5H,CAAC,EACD,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,yCAAyC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAC9H,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,EAAW;QAClC,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAE9C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAC5D,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAC3B,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YACvE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5F,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA4B,IAAI,CAAC,MAAM,CAAC,IAAI,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YACtG,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAC5D,MAAM,IAAI,cAAc,sDAAyC,CAAC;QACpE,CAAC;IACH,CAAC;CACF;AArGD,wCAqGC"}
@@ -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.setActive(this.platform.Characteristic.Active.INACTIVE);
143
+ await this.applyMqttActive(false);
138
144
  return;
139
145
  }
140
146
  if (command.state === 'on') {
141
- await this.setActive(this.platform.Characteristic.Active.ACTIVE);
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.setActive(value);
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.setActive(this.platform.Characteristic.Active.ACTIVE);
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
  }