homebridge-smarthq-client 1.3.0 → 1.3.1

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.
@@ -21,9 +21,9 @@ jobs:
21
21
  persist-credentials: false
22
22
 
23
23
  - name: Setup Node.js
24
- uses: actions/setup-node@v4
24
+ uses: actions/setup-node@v7
25
25
  with:
26
- node-version: 24
26
+ node-version: '24.10'
27
27
  cache: 'npm'
28
28
 
29
29
  - name: Install Dependencies
@@ -33,7 +33,9 @@ jobs:
33
33
  run: npm run build --if-present
34
34
 
35
35
  - name: Run Tests
36
- run: npm test --if-present
36
+ run: npm test --if-present
37
+ env:
38
+ NODE_OPTIONS: --experimental-vm-modules
37
39
 
38
40
  - name: Semantic Release
39
41
  env:
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [1.3.1](https://github.com/ceb400/homebridge-smarthq-client/compare/v1.3.0...v1.3.1) (2026-09-18)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * :bug: dishwasher options initialized wrong ([ee59785](https://github.com/ceb400/homebridge-smarthq-client/commit/ee59785bbdc33a7373a8d0dfe0b766f0101ac1e2))
7
+ * :bug: Fix Dishwasher Cleaning mode error. Replace Cycle Pct Done with new Time remaining ([a1738bb](https://github.com/ceb400/homebridge-smarthq-client/commit/a1738bbe3e36217c0476d89ffcbe1f5682909776))
8
+
1
9
  # Changelog
2
10
 
3
11
  All notable changes to this project will be documented in this file.
@@ -2,9 +2,13 @@ import { CharacteristicValue, PlatformAccessory, Service, Characteristic } from
2
2
  import { DeviceService } from "ge-smarthq";
3
3
  import { SmartHqPlatform } from "../platform.js";
4
4
  /**
5
+
5
6
  * Platform Accessory
7
+
6
8
  * An instance of this class is created for each accessory your platform registers
9
+
7
10
  * Each accessory may expose multiple services of different service types
11
+
8
12
  */
9
13
  export declare class Dishwasher {
10
14
  private readonly platform;
@@ -50,36 +54,48 @@ export declare class Dishwasher {
50
54
  private readonly SANI_AND_BOOST_TEMP;
51
55
  constructor(platform: SmartHqPlatform, accessory: PlatformAccessory, deviceServices: DeviceService[], deviceId: string, groupAccessory: PlatformAccessory[]);
52
56
  /**
57
+
53
58
  * Handle requests to get the current value of the "Active" characteristic
59
+
54
60
  */
55
61
  handleSetDurationGet(): Promise<CharacteristicValue>;
56
62
  /**
63
+
57
64
  * Handle requests to get the current value of the "Active" characteristic
65
+
58
66
  */
59
67
  handleActiveGet(): Promise<CharacteristicValue>;
60
68
  /**
69
+
61
70
  * Handle requests to set the "Active" characteristic
71
+
62
72
  */
63
73
  handleActiveSet(value: CharacteristicValue): Promise<void>;
64
74
  /**
75
+
65
76
  * Handle requests to get the current value of the "In Use" characteristic
77
+
66
78
  */
67
79
  handleInUseGet(): Promise<CharacteristicValue>;
68
80
  /**
81
+
69
82
  * Handle requests to get the current value of the "Name" characteristic
83
+
70
84
  */
71
85
  handleNameGet(): Promise<string | false>;
72
86
  /**
87
+
73
88
  * Handle requests to get the current value of the "mode" value
89
+
74
90
  */
75
91
  handleModeGet(v1mode: string): Promise<CharacteristicValue>;
76
92
  /**
93
+
77
94
  * Handle requests to get the current value of the "Valve Type" characteristic
95
+
78
96
  */
79
97
  handleValveTypeGet(): Promise<number>;
80
98
  handleRemainingTimeGet(): Promise<CharacteristicValue>;
81
- getCyclePct(): Promise<CharacteristicValue>;
82
- setCyclePct(value: CharacteristicValue): Promise<CharacteristicValue>;
83
99
  setMode(): Promise<boolean>;
84
100
  private formatError;
85
101
  startCycle(): Promise<boolean>;
@@ -96,6 +112,7 @@ export declare class Dishwasher {
96
112
  getLastElementAndCapitalize(str: string, delimiter: string): [string, string];
97
113
  setupWebSocket(): Promise<void>;
98
114
  logCurrentOptions(): void;
115
+ updateDurationTimerDisplay(remainingSeconds: number): void;
99
116
  testCases(): Promise<false | undefined>;
100
117
  testSetMode({ mode, temp, dry, zone, bottle, steam, silverware }: {
101
118
  mode?: string;
@@ -1,9 +1,13 @@
1
1
  import { SmartHQClient } from "ge-smarthq";
2
2
  import chalk from "chalk";
3
3
  /**
4
+
4
5
  * Platform Accessory
6
+
5
7
  * An instance of this class is created for each accessory your platform registers
8
+
6
9
  * Each accessory may expose multiple services of different service types
10
+
7
11
  */
8
12
  export class Dishwasher {
9
13
  platform;
@@ -37,7 +41,7 @@ export class Dishwasher {
37
41
  ONE_HOUR_MODE = "cloud.smarthq.domain.dishwasher.timed.hours.1";
38
42
  PLATPLUS_MODE = "cloud.smarthq.domain.dishwasher.brand.cascade.platinumplus";
39
43
  RINSE_MODE = "cloud.smarthq.domain.dishwasher.rinse";
40
- CLEAN_MODE = "cloud.smarthq.domain.dishwasher.clean";
44
+ CLEAN_MODE = "cloud.smarthq.domain.dishwasher.dishwasher.cleaning";
41
45
  LIGHT_MODE = "cloud.smarthq.domain.dishwasher.light";
42
46
  //======== wash zone constants ========
43
47
  BOTH_ZONE = "cloud.smarthq.type.dishwasher.washzone.both";
@@ -81,26 +85,25 @@ export class Dishwasher {
81
85
  }
82
86
  this.setupWebSocket();
83
87
  /*
88
+
84
89
  * Listen for WebSocket messages for this device and update HomeKit characteristics accordingly
90
+
85
91
  */
86
92
  this.client.on("service_update", (message) => {
87
- //this.client.debug(chalk.red('Wash Modes - Service Update:'+ JSON.stringify(message, null, 2)));
88
93
  if (message.domainType === "cloud.smarthq.domain.energy"
89
94
  && message.deviceType === "cloud.smarthq.device.dishwasher") {
90
95
  this.energyMeterValuePerHour += message.state?.meterValueDelta || 0; // sum for the hour until reset
96
+ this.client.debug(chalk.white('Energy Meter Value: ' + this.energyMeterValuePerHour));
91
97
  }
98
+ // Update the time remaining from the WebSocket message if it is a cycle timer update
99
+ // Update the ConfiguredName of the durationTimer service to show the time remaining in the cycle or the current time if no cycle is active
92
100
  if (message.serviceType === "cloud.smarthq.service.cycletimer" &&
93
101
  message.domainType === "cloud.smarthq.domain.cycle") {
94
- // when secondsRemaining is 0 then reset totalSeconds to prevent stale values from previous cycles
95
- if (message.state?.secondsRemaining === 0) {
96
- this.totalSeconds = 0;
97
- }
102
+ this.timeRemainingFromWebSocket =
103
+ message.state?.secondsRemaining || 0;
98
104
  if (this.totalSeconds === 0) {
99
105
  this.totalSeconds = message.state?.secondsRemaining || 0;
100
106
  }
101
- this.timeRemainingFromWebSocket =
102
- message.state?.secondsRemaining || 0;
103
- this.getCyclePct();
104
107
  }
105
108
  if (message.state?.mode != null) {
106
109
  switch (message.state.mode) {
@@ -138,6 +141,10 @@ export class Dishwasher {
138
141
  this.client.debug(chalk.red("Watts/hour value: " + this.energyMeterValuePerHour));
139
142
  this.energyMeterValuePerHour = 0;
140
143
  }, 60 * 60 * 1000);
144
+ // Interval to update configured name of durationTimer service every minute to show time remaining in cycle or current time if no cycle is active
145
+ setInterval(() => {
146
+ this.updateDurationTimerDisplay(this.timeRemainingFromWebSocket);
147
+ }, 60 * 1000);
141
148
  const washTemps = this.getAvailableItemsByType("washTempAvailable");
142
149
  const washZones = this.getAvailableItemsByType("washZoneAvailable");
143
150
  const heatedDrys = this.getAvailableItemsByType("heatedDryAvailable");
@@ -199,21 +206,23 @@ export class Dishwasher {
199
206
  dishwasher
200
207
  .getCharacteristic(this.Characteristic.Name)
201
208
  .onGet(this.handleNameGet.bind(this));
202
- //=====================================================================================
203
- // create a new Lightbulb service for the Cycle Pct Done
204
- //=====================================================================================
205
- displayName = "Cycle Pct Done";
206
- const cyclePct = this.accessory.getService(displayName) ||
207
- this.accessory.addService(this.Service.Lightbulb, displayName, `${this.deviceId}-cycle-done`);
208
- cyclePct.setCharacteristic(this.Characteristic.Name, displayName);
209
- cyclePct.addOptionalCharacteristic(this.Characteristic.ConfiguredName);
210
- cyclePct.setCharacteristic(this.Characteristic.ConfiguredName, displayName);
211
- cyclePct
212
- .getCharacteristic(this.Characteristic.Brightness)
213
- .onGet(this.getCyclePct.bind(this))
214
- .onSet(this.setCyclePct.bind(this));
209
+ // Test dynamically changing configured name to use as a text field
210
+ const customName = "Time Display";
211
+ const durationTimer = this.setupService("Outlet", customName, `${this.deviceId}-optionstimer`);
212
+ durationTimer
213
+ .getCharacteristic(this.Characteristic.On)
214
+ .onGet(() => {
215
+ const currentState = durationTimer.getCharacteristic(this.Characteristic.On)
216
+ .value;
217
+ return currentState; // Return true or false
218
+ })
219
+ .onSet((value) => {
220
+ this.client.debug(chalk.yellow(`Time Display On/Off set to: ${value}`));
221
+ });
215
222
  /**
223
+
216
224
  * create a new steam Outlet service Dummy (used as 'available' option) ------
225
+
217
226
  */
218
227
  displayName = "Steam option";
219
228
  const optionSteam = this.setupService("Outlet", displayName, `${this.deviceId}-optionsteam`);
@@ -229,7 +238,9 @@ export class Dishwasher {
229
238
  this.testSetMode({ steam: value });
230
239
  });
231
240
  /**
241
+
232
242
  * create a new bottlewash Outlet service Dummy (used as 'available' option) for extending presets ------
243
+
233
244
  */
234
245
  displayName = "Bottlewash option";
235
246
  const optionBottlewash = this.setupService("Outlet", displayName, `${this.deviceId}-optionbottlewash`);
@@ -245,7 +256,9 @@ export class Dishwasher {
245
256
  this.testSetMode({ bottle: value });
246
257
  });
247
258
  /**
259
+
248
260
  * create a new silverware Outlet service Dummy (used as 'available' option) for extending presets ------
261
+
249
262
  */
250
263
  displayName = "Silverware option";
251
264
  const optionSilverware = this.setupService("Outlet", displayName, `${this.deviceId}-option-silverware`);
@@ -261,7 +274,9 @@ export class Dishwasher {
261
274
  this.testSetMode({ silverware: this.currentSilverwareWash });
262
275
  });
263
276
  /**
277
+
264
278
  * Grouped services for Wash Temp, Dry Level, Wash Zone, and Preset Modes - only one can be on at a time within each group
279
+
265
280
  */
266
281
  this.client.debug(chalk.green("Wash Temperature options"));
267
282
  const temps = washTemps.map(([item]) => this.setupGroupService("Outlet", item, "modegroup-" + item.replace(/\s+/g, "-").toLowerCase(), this.groupAccessory[0]));
@@ -495,6 +510,7 @@ export class Dishwasher {
495
510
  // NOTE: only for developing a method for testing command combinations.
496
511
  // this.testCases();
497
512
  /*
513
+
498
514
  for (const service of this.deviceServices) {
499
515
  if (
500
516
  service.serviceDeviceType === "cloud.smarthq.device.dishwasher" &&
@@ -516,18 +532,24 @@ export class Dishwasher {
516
532
  }
517
533
 
518
534
  //const originalPresetMode = response?.state.
535
+
519
536
  }
520
537
  }
538
+
521
539
  */
522
540
  }
523
541
  /**
542
+
524
543
  * Handle requests to get the current value of the "Active" characteristic
544
+
525
545
  */
526
546
  async handleSetDurationGet() {
527
547
  return this.totalSeconds;
528
548
  }
529
549
  /**
550
+
530
551
  * Handle requests to get the current value of the "Active" characteristic
552
+
531
553
  */
532
554
  async handleActiveGet() {
533
555
  let isActive = false;
@@ -541,25 +563,11 @@ export class Dishwasher {
541
563
  this.client.debug("No response from dishwasher run status request");
542
564
  return false;
543
565
  }
544
- if (response.state.runStatus === "cloud.smarthq.type.runstatus.off") {
566
+ if (response.state.runStatus === "cloud.smarthq.type.runstatus.endofcycle"
567
+ || response.state.runStatus === "cloud.smarthq.type.runstatus.off") {
545
568
  // change back to 0 seconds remaining when cycle is not active to prevent stale remaining time value in HomeKit
546
569
  this.totalSeconds = 0; // reset total seconds when cycle is complete
547
- this.accessory
548
- .getService("Dishwasher")
549
- ?.getCharacteristic(this.Characteristic.Active)
550
- .updateValue(false);
551
- this.accessory
552
- .getService("Dishwasher")
553
- ?.getCharacteristic(this.Characteristic.SetDuration)
554
- .updateValue(0);
555
- this.accessory
556
- .getService("Dishwasher")
557
- ?.getCharacteristic(this.Characteristic.RemainingDuration)
558
- .updateValue(0);
559
- this.accessory
560
- .getService("Dishwasher")
561
- ?.getCharacteristic(this.Characteristic.InUse)
562
- .updateValue(false);
570
+ this.updateDurationTimerDisplay(this.totalSeconds);
563
571
  }
564
572
  else {
565
573
  isActive = true;
@@ -570,7 +578,7 @@ export class Dishwasher {
570
578
  this.accessory
571
579
  .getService("Dishwasher")
572
580
  ?.getCharacteristic(this.Characteristic.InUse)
573
- .updateValue(isActive);
581
+ .updateValue(this.Characteristic.InUse.IN_USE);
574
582
  }
575
583
  break;
576
584
  }
@@ -583,7 +591,9 @@ export class Dishwasher {
583
591
  return isActive;
584
592
  }
585
593
  /**
594
+
586
595
  * Handle requests to set the "Active" characteristic
596
+
587
597
  */
588
598
  async handleActiveSet(value) {
589
599
  this.client.debug(`Starting dishwasher active state: ${value}`);
@@ -611,7 +621,9 @@ export class Dishwasher {
611
621
  }
612
622
  }
613
623
  /**
624
+
614
625
  * Handle requests to get the current value of the "In Use" characteristic
626
+
615
627
  */
616
628
  async handleInUseGet() {
617
629
  //this.client.debug('Triggered GET InUse');
@@ -620,7 +632,9 @@ export class Dishwasher {
620
632
  return this.handleActiveGet();
621
633
  }
622
634
  /**
635
+
623
636
  * Handle requests to get the current value of the "Name" characteristic
637
+
624
638
  */
625
639
  async handleNameGet() {
626
640
  //this.client.debug('Triggered GET Name');
@@ -646,7 +660,9 @@ export class Dishwasher {
646
660
  return this.accessory.displayName;
647
661
  }
648
662
  /**
663
+
649
664
  * Handle requests to get the current value of the "mode" value
665
+
650
666
  */
651
667
  async handleModeGet(v1mode) {
652
668
  let isOn = false;
@@ -657,10 +673,12 @@ export class Dishwasher {
657
673
  try {
658
674
  const response = await this.client.getServiceDetails(this.deviceId, service.serviceId);
659
675
  /*
676
+
660
677
  this.client.debug(
661
678
  "================ Response from getServiceDetails for mode get: " +
662
679
  JSON.stringify(response, null, 2),
663
680
  );
681
+
664
682
  */
665
683
  if (response?.state?.mode == null) {
666
684
  this.client.debug("No response from dishwasher mode request");
@@ -678,7 +696,9 @@ export class Dishwasher {
678
696
  return isOn;
679
697
  }
680
698
  /**
699
+
681
700
  * Handle requests to get the current value of the "Valve Type" characteristic
701
+
682
702
  */
683
703
  async handleValveTypeGet() {
684
704
  //this.client.debug('Triggered GET ValveType');
@@ -698,34 +718,6 @@ export class Dishwasher {
698
718
  }
699
719
  return this.totalSeconds;
700
720
  }
701
- async getCyclePct() {
702
- // remaining time appears able to change after a cycle has started???
703
- if (this.timeRemainingFromWebSocket > this.totalSeconds) {
704
- this.totalSeconds = this.timeRemainingFromWebSocket;
705
- }
706
- const pctDone = 100 - Math.round((this.timeRemainingFromWebSocket / this.totalSeconds) * 100) || 0;
707
- this.setCyclePct(pctDone);
708
- if (this.timeRemainingFromWebSocket === 0) {
709
- this.accessory
710
- .getService("Cycle Pct Done")
711
- ?.getCharacteristic(this.Characteristic.On)
712
- .updateValue(false);
713
- }
714
- else {
715
- this.accessory
716
- .getService("Cycle Pct Done")
717
- ?.getCharacteristic(this.Characteristic.On)
718
- .updateValue(true);
719
- this.accessory.getService("Cycle Pct Done")?.updateCharacteristic(this.Characteristic.Brightness, pctDone);
720
- }
721
- return pctDone;
722
- }
723
- async setCyclePct(value) {
724
- this.accessory
725
- .getService("Cycle Pct Done")
726
- ?.updateCharacteristic(this.Characteristic.Brightness, value);
727
- return value;
728
- }
729
721
  async setMode() {
730
722
  const baseCommand = {
731
723
  deviceId: this.deviceId,
@@ -776,6 +768,16 @@ export class Dishwasher {
776
768
  },
777
769
  };
778
770
  break;
771
+ case this.CLEAN_MODE:
772
+ cmdBody = {
773
+ ...baseCommand,
774
+ domainType: this.currentPreset,
775
+ command: {
776
+ washTemp: this.currentWashTemp,
777
+ commandType: "cloud.smarthq.command.dishwasher.mode.v1.set",
778
+ },
779
+ };
780
+ break;
779
781
  case this.ONE_HOUR_MODE:
780
782
  cmdBody = {
781
783
  ...baseCommand,
@@ -862,7 +864,7 @@ export class Dishwasher {
862
864
  this.accessory
863
865
  .getService("Dishwasher")
864
866
  ?.getCharacteristic(this.Characteristic.InUse)
865
- .updateValue(true);
867
+ .updateValue(this.Characteristic.InUse.IN_USE);
866
868
  this.accessory
867
869
  .getService("Dishwasher")
868
870
  ?.getCharacteristic(this.Characteristic.SetDuration)
@@ -870,7 +872,7 @@ export class Dishwasher {
870
872
  this.accessory
871
873
  .getService("Dishwasher")
872
874
  ?.getCharacteristic(this.Characteristic.RemainingDuration)
873
- .updateValue(this.totalSeconds);
875
+ .updateValue(this.timeRemainingFromWebSocket);
874
876
  return response.success;
875
877
  }
876
878
  }
@@ -900,7 +902,7 @@ export class Dishwasher {
900
902
  this.accessory
901
903
  .getService("Dishwasher")
902
904
  ?.getCharacteristic(this.Characteristic.InUse)
903
- .updateValue(false);
905
+ .updateValue(this.Characteristic.InUse.NOT_IN_USE);
904
906
  this.totalSeconds = 0; // reset total seconds when cycle is stopped
905
907
  this.accessory
906
908
  .getService("Dishwasher")
@@ -909,7 +911,7 @@ export class Dishwasher {
909
911
  this.accessory
910
912
  .getService("Dishwasher")
911
913
  ?.getCharacteristic(this.Characteristic.RemainingDuration)
912
- .updateValue(this.totalSeconds);
914
+ .updateValue(this.timeRemainingFromWebSocket);
913
915
  return response.success;
914
916
  }
915
917
  }
@@ -988,6 +990,7 @@ export class Dishwasher {
988
990
  accessory.removeService(service);
989
991
  });
990
992
  }
993
+ // Build a list of available dishwasher option names in a display-friendly format.
991
994
  getAvailableItemsByType(availableType) {
992
995
  let itemsAvailable = [];
993
996
  for (const service of this.deviceServices) {
@@ -1077,6 +1080,52 @@ export class Dishwasher {
1077
1080
  this.client.debug(chalk.red("Steam: " + this.currentSteam));
1078
1081
  this.client.debug(chalk.red("Silverware Wash: " + this.currentSilverwareWash));
1079
1082
  }
1083
+ updateDurationTimerDisplay(remainingSeconds) {
1084
+ const now = new Date();
1085
+ const hours = now.getHours();
1086
+ const minutes = now.getMinutes();
1087
+ if (remainingSeconds > 0) {
1088
+ const remainingHours = Math.floor(remainingSeconds / 3600);
1089
+ const remainingMinutes = Math.floor((remainingSeconds % 3600) / 60);
1090
+ const displayString = `Ends in: ${remainingHours.toString().padStart(2, "0")}:${remainingMinutes.toString().padStart(2, "0")}`;
1091
+ this.accessory
1092
+ .getService("Time Display")
1093
+ ?.getCharacteristic(this.Characteristic.ConfiguredName)
1094
+ .updateValue(displayString);
1095
+ this.accessory
1096
+ .getService("Time Display")
1097
+ ?.getCharacteristic(this.Characteristic.On)
1098
+ .updateValue(false);
1099
+ return;
1100
+ }
1101
+ const displayString = `Time: ${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
1102
+ // when secondsRemaining is 0 then reset totalSeconds to prevent stale values from previous cycles
1103
+ this.totalSeconds = 0;
1104
+ this.accessory
1105
+ .getService("Dishwasher")
1106
+ ?.getCharacteristic(this.Characteristic.SetDuration)
1107
+ .updateValue(0);
1108
+ this.accessory
1109
+ .getService("Dishwasher")
1110
+ ?.getCharacteristic(this.Characteristic.RemainingDuration)
1111
+ .updateValue(0);
1112
+ this.accessory
1113
+ .getService("Dishwasher")
1114
+ ?.getCharacteristic(this.Characteristic.Active)
1115
+ .updateValue(this.Characteristic.Active.INACTIVE);
1116
+ this.accessory
1117
+ .getService("Dishwasher")
1118
+ ?.getCharacteristic(this.Characteristic.InUse)
1119
+ .updateValue(this.Characteristic.InUse.NOT_IN_USE);
1120
+ this.accessory
1121
+ .getService("Time Display")
1122
+ ?.getCharacteristic(this.Characteristic.ConfiguredName)
1123
+ .updateValue(displayString);
1124
+ this.accessory
1125
+ .getService("Time Display")
1126
+ ?.getCharacteristic(this.Characteristic.On)
1127
+ .updateValue(false);
1128
+ }
1080
1129
  async testCases() {
1081
1130
  //======================================================
1082
1131
  // Develop test plan for dishwasher