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 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.7.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
@@ -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 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":"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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 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":"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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.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
  }