@homebridge-plugins/homebridge-smarthq 0.5.0-beta.4 → 0.5.0-beta.40

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.
Files changed (145) hide show
  1. package/.github/ISSUE_TEMPLATE/config.yml +1 -1
  2. package/.github/copilot-instructions.md +361 -0
  3. package/.github/scripts/branch-helper.sh +41 -0
  4. package/.github/workflows/beta-release.yml +1 -1
  5. package/.github/workflows/release.yml +53 -17
  6. package/CHANGELOG.md +563 -18
  7. package/MATTER.md +304 -0
  8. package/README.md +15 -0
  9. package/config.schema.json +24 -5
  10. package/dist/devices/OpalIceMaker/Managers/OpalDescaleSvcManager.d.ts.map +1 -1
  11. package/dist/devices/OpalIceMaker/Managers/OpalDescaleSvcManager.js +3 -1
  12. package/dist/devices/OpalIceMaker/Managers/OpalDescaleSvcManager.js.map +1 -1
  13. package/dist/devices/OpalIceMaker/Managers/OpalFilterMaintenanceSvcManager.d.ts.map +1 -1
  14. package/dist/devices/OpalIceMaker/Managers/OpalFilterMaintenanceSvcManager.js +3 -1
  15. package/dist/devices/OpalIceMaker/Managers/OpalFilterMaintenanceSvcManager.js.map +1 -1
  16. package/dist/devices/OpalIceMaker/Managers/OpalPowerSvcManager.js +1 -1
  17. package/dist/devices/OpalIceMaker/Managers/OpalPowerSvcManager.js.map +1 -1
  18. package/dist/devices/OpalIceMaker/Managers/OpalProgressSvcManager.d.ts +1 -1
  19. package/dist/devices/OpalIceMaker/Managers/OpalProgressSvcManager.d.ts.map +1 -1
  20. package/dist/devices/OpalIceMaker/Managers/OpalProgressSvcManager.js +12 -5
  21. package/dist/devices/OpalIceMaker/Managers/OpalProgressSvcManager.js.map +1 -1
  22. package/dist/devices/OpalIceMaker/Managers/StatusManagers/OpalStatusBase.d.ts.map +1 -1
  23. package/dist/devices/OpalIceMaker/Managers/StatusManagers/OpalStatusBase.js +4 -2
  24. package/dist/devices/OpalIceMaker/Managers/StatusManagers/OpalStatusBase.js.map +1 -1
  25. package/dist/devices/OpalIceMaker/OpalDeviceBase.d.ts +3 -4
  26. package/dist/devices/OpalIceMaker/OpalDeviceBase.d.ts.map +1 -1
  27. package/dist/devices/OpalIceMaker/OpalDeviceBase.js +3 -18
  28. package/dist/devices/OpalIceMaker/OpalDeviceBase.js.map +1 -1
  29. package/dist/devices/advantium.d.ts +10 -0
  30. package/dist/devices/advantium.d.ts.map +1 -0
  31. package/dist/devices/advantium.js +75 -0
  32. package/dist/devices/advantium.js.map +1 -0
  33. package/dist/devices/airConditioner.d.ts +11 -2
  34. package/dist/devices/airConditioner.d.ts.map +1 -1
  35. package/dist/devices/airConditioner.js +111 -17
  36. package/dist/devices/airConditioner.js.map +1 -1
  37. package/dist/devices/beverageCenter.d.ts +10 -0
  38. package/dist/devices/beverageCenter.d.ts.map +1 -0
  39. package/dist/devices/beverageCenter.js +125 -0
  40. package/dist/devices/beverageCenter.js.map +1 -0
  41. package/dist/devices/clothesDryer.d.ts +21 -0
  42. package/dist/devices/clothesDryer.d.ts.map +1 -0
  43. package/dist/devices/clothesDryer.js +273 -0
  44. package/dist/devices/clothesDryer.js.map +1 -0
  45. package/dist/devices/clothesWasher.d.ts +21 -0
  46. package/dist/devices/clothesWasher.d.ts.map +1 -0
  47. package/dist/devices/clothesWasher.js +284 -0
  48. package/dist/devices/clothesWasher.js.map +1 -0
  49. package/dist/devices/coffeeMaker.d.ts +10 -0
  50. package/dist/devices/coffeeMaker.d.ts.map +1 -0
  51. package/dist/devices/coffeeMaker.js +79 -0
  52. package/dist/devices/coffeeMaker.js.map +1 -0
  53. package/dist/devices/device.d.ts +108 -13
  54. package/dist/devices/device.d.ts.map +1 -1
  55. package/dist/devices/device.js +381 -60
  56. package/dist/devices/device.js.map +1 -1
  57. package/dist/devices/dishwasher.d.ts +9 -3
  58. package/dist/devices/dishwasher.d.ts.map +1 -1
  59. package/dist/devices/dishwasher.js +164 -29
  60. package/dist/devices/dishwasher.js.map +1 -1
  61. package/dist/devices/hood.d.ts +41 -0
  62. package/dist/devices/hood.d.ts.map +1 -0
  63. package/dist/devices/hood.js +336 -0
  64. package/dist/devices/hood.js.map +1 -0
  65. package/dist/devices/microwave.d.ts +19 -0
  66. package/dist/devices/microwave.d.ts.map +1 -0
  67. package/dist/devices/microwave.js +147 -0
  68. package/dist/devices/microwave.js.map +1 -0
  69. package/dist/devices/oven.d.ts +9 -2
  70. package/dist/devices/oven.d.ts.map +1 -1
  71. package/dist/devices/oven.js +267 -43
  72. package/dist/devices/oven.js.map +1 -1
  73. package/dist/devices/refrigerator.d.ts +37 -2
  74. package/dist/devices/refrigerator.d.ts.map +1 -1
  75. package/dist/devices/refrigerator.js +583 -34
  76. package/dist/devices/refrigerator.js.map +1 -1
  77. package/dist/devices/waterFilter.d.ts +10 -0
  78. package/dist/devices/waterFilter.d.ts.map +1 -0
  79. package/dist/devices/waterFilter.js +67 -0
  80. package/dist/devices/waterFilter.js.map +1 -0
  81. package/dist/devices/waterHeater.d.ts +19 -0
  82. package/dist/devices/waterHeater.d.ts.map +1 -0
  83. package/dist/devices/waterHeater.js +161 -0
  84. package/dist/devices/waterHeater.js.map +1 -0
  85. package/dist/devices/waterSoftener.d.ts +10 -0
  86. package/dist/devices/waterSoftener.d.ts.map +1 -0
  87. package/dist/devices/waterSoftener.js +67 -0
  88. package/dist/devices/waterSoftener.js.map +1 -0
  89. package/dist/getAccessToken.d.ts.map +1 -1
  90. package/dist/getAccessToken.js +190 -2
  91. package/dist/getAccessToken.js.map +1 -1
  92. package/dist/platform.d.ts +32 -1
  93. package/dist/platform.d.ts.map +1 -1
  94. package/dist/platform.js +853 -151
  95. package/dist/platform.js.map +1 -1
  96. package/dist/platform.test.d.ts +2 -0
  97. package/dist/platform.test.d.ts.map +1 -0
  98. package/dist/platform.test.js +80 -0
  99. package/dist/platform.test.js.map +1 -0
  100. package/dist/settings.d.ts +24 -0
  101. package/dist/settings.d.ts.map +1 -1
  102. package/dist/settings.js +13 -0
  103. package/dist/settings.js.map +1 -1
  104. package/docs/assets/highlight.css +7 -0
  105. package/docs/assets/main.js +5 -5
  106. package/docs/assets/navigation.js +1 -0
  107. package/docs/assets/search.js +1 -0
  108. package/docs/assets/style.css +248 -226
  109. package/docs/classes/SmartHQPlatform.html +54 -56
  110. package/docs/hierarchy.html +1 -0
  111. package/docs/index.html +22 -12
  112. package/docs/interfaces/DeviceOptions.html +2 -3
  113. package/docs/interfaces/SmartHQPlatformConfig.html +6 -17
  114. package/docs/interfaces/SmartHqContext.html +6 -4
  115. package/docs/interfaces/SmartHqERDResponse.html +7 -8
  116. package/docs/interfaces/credentials.html +3 -4
  117. package/docs/interfaces/devicesConfig.html +9 -8
  118. package/docs/interfaces/options.html +7 -8
  119. package/docs/media/copilot-instructions.md +361 -0
  120. package/docs/modules.html +1 -2
  121. package/docs/variables/API_URL.html +1 -2
  122. package/docs/variables/ERD_CODES.html +1 -2
  123. package/docs/variables/ERD_TYPES.html +1 -2
  124. package/docs/variables/KEEPALIVE_TIMEOUT.html +1 -2
  125. package/docs/variables/LOGIN_URL.html +2 -3
  126. package/docs/variables/OAUTH2_CLIENT_ID.html +1 -2
  127. package/docs/variables/OAUTH2_CLIENT_SECRET.html +1 -2
  128. package/docs/variables/OAUTH2_REDIRECT_URI.html +1 -2
  129. package/docs/variables/PLATFORM_NAME.html +2 -3
  130. package/docs/variables/PLUGIN_NAME.html +2 -3
  131. package/docs/variables/SECURE_URL.html +2 -3
  132. package/docs/variables/default.html +1 -0
  133. package/package.json +22 -33
  134. package/typedoc.json +0 -4
  135. package/vitest.config.ts +6 -5
  136. package/.github/workflows/build.yml +0 -18
  137. package/.github/workflows/changerelease.yml +0 -11
  138. package/.github/workflows/labeler.yml +0 -9
  139. package/.github/workflows/release-drafter.yml +0 -14
  140. package/docs/assets/dmt/dmt-component-data.js +0 -1
  141. package/docs/assets/dmt/dmt-components.css +0 -20
  142. package/docs/assets/dmt/dmt-components.js +0 -67
  143. package/docs/assets/dmt/dmt-search.cmp +0 -0
  144. package/docs/assets/dmt/dmt-theme.css +0 -1
  145. package/docs/functions/default.html +0 -2
@@ -1,59 +1,608 @@
1
- import axios from 'axios';
2
1
  import { interval, skipWhile } from 'rxjs';
3
2
  import { ERD_TYPES } from '../settings.js';
4
3
  import { deviceBase } from './device.js';
4
+ /**
5
+ * SmartHQ Refrigerator - Unified HAP/Matter Implementation
6
+ * Supports both HomeKit Accessory Protocol and Matter protocol
7
+ */
5
8
  export class SmartHQRefrigerator extends deviceBase {
6
9
  platform;
7
10
  device;
8
11
  // Updates
9
12
  SensorUpdateInProgress;
10
13
  deviceStatus;
14
+ // Matter support override flag
15
+ useMatterOverride = false;
11
16
  constructor(platform, accessory, device) {
12
17
  super(platform, accessory, device);
13
18
  this.platform = platform;
14
19
  this.device = device;
20
+ // Check if we should use Matter protocol
21
+ this.useMatterOverride = device.useMatter ?? false;
15
22
  this.debugLog(`Refrigerator Features: ${JSON.stringify(accessory.context.device.features)}`);
16
- accessory.context.device.features.forEach((feature) => {
17
- /* [
18
- "DOOR_STATUS"
19
- ] */
20
- switch (feature) {
21
- case 'DOOR_STATUS': {
22
- const refrigerator = this.accessory.getService(accessory.displayName) ?? this.accessory.addService(this.platform.Service.ContactSensor, accessory.displayName, 'Refrigerator');
23
- refrigerator
24
- .getCharacteristic(this.platform.Characteristic.ContactSensorState)
25
- .onGet(() => this.readErd(ERD_TYPES.DOOR_STATUS).then(r => Number.parseInt(r) !== 0))
26
- .onSet(value => this.writeErd(ERD_TYPES.DOOR_STATUS, value));
27
- break;
28
- }
29
- }
30
- });
31
- // this is subject we use to track when we need to POST changes to the SmartHQ API
23
+ this.debugLog(`Using protocol: ${this.useMatterOverride ? 'Matter' : 'HAP'}`);
24
+ // Initialize the appropriate protocol
25
+ if (this.useMatterOverride) {
26
+ this.initializeMatter().catch((error) => {
27
+ this.errorLog(`Failed to initialize Matter: ${error}`);
28
+ });
29
+ }
30
+ else {
31
+ this.initializeHAP();
32
+ }
33
+ // Start periodic refresh
32
34
  this.SensorUpdateInProgress = false;
33
- // Retrieve initial values and updateHomekit
34
- // this.refreshStatus()
35
- // Start an update interval
36
- interval(this.deviceRefreshRate * 10000)
35
+ interval(this.deviceRefreshRate * 1000)
37
36
  .pipe(skipWhile(() => this.SensorUpdateInProgress))
38
37
  .subscribe(async () => {
39
- // await this.refreshStatus()
38
+ await this.refreshDeviceStatus();
40
39
  });
40
+ // Initial refresh - wait longer for Matter registration if using Matter
41
+ const initialRefreshDelay = this.useMatterOverride ? 5000 : 2000;
42
+ setTimeout(() => this.refreshDeviceStatus(), initialRefreshDelay);
41
43
  }
42
- async readErd(erd) {
43
- const d = await axios
44
- .get(`/appliance/${this.accessory.context.device.applianceId}/erd/${erd}`);
45
- return String(d.data.value);
44
+ /**
45
+ * Initialize Matter protocol
46
+ */
47
+ async initializeMatter() {
48
+ const { valid, api: matterAPI } = this.validateMatterAPI();
49
+ if (!valid) {
50
+ if (this.device.matterOnly) {
51
+ this.errorLog('Matter API not available or incomplete - accessory will NOT be published (matterOnly mode enabled)');
52
+ this.errorLog('Reason: Matter API validation failed');
53
+ return;
54
+ }
55
+ this.errorLog('Matter API not available or incomplete - falling back to HAP');
56
+ this.initializeHAP();
57
+ return;
58
+ }
59
+ // Check if RefrigeratorFreezer device type is available
60
+ if (!matterAPI.deviceTypes.RefrigeratorFreezer) {
61
+ if (this.device.matterOnly) {
62
+ this.errorLog('Matter RefrigeratorFreezer device type not available - accessory will NOT be published (matterOnly mode enabled)');
63
+ this.errorLog('Reason: Required Matter device type "RefrigeratorFreezer" is not available in this Homebridge version');
64
+ this.errorLog(`Available Matter device types: ${Object.keys(matterAPI.deviceTypes).join(', ')}`);
65
+ return;
66
+ }
67
+ this.warnLog('Matter RefrigeratorFreezer device type not available in this Homebridge version - falling back to HAP');
68
+ this.warnLog(`Available Matter device types: ${Object.keys(matterAPI.deviceTypes).join(', ')}`);
69
+ this.useMatterOverride = false;
70
+ this.initializeHAP();
71
+ return;
72
+ }
73
+ const serialNumber = this.device.applianceId || 'unknown';
74
+ this.matterUuid = matterAPI.uuid.generate(serialNumber);
75
+ // Create Matter accessory configuration with refrigerator-specific clusters
76
+ const matterAccessory = {
77
+ UUID: this.matterUuid,
78
+ displayName: this.device.nickname || 'SmartHQ Refrigerator',
79
+ serialNumber,
80
+ manufacturer: this.device.brand && this.device.brand !== 'Unknown' ? this.device.brand : 'GE Appliances',
81
+ model: this.device.model || 'SmartHQ',
82
+ firmwareRevision: this.deviceFirmwareVersion,
83
+ hardwareRevision: this.deviceFirmwareVersion,
84
+ deviceType: matterAPI.deviceTypes.RefrigeratorFreezer,
85
+ clusters: {
86
+ // Temperature control for main compartment
87
+ thermostat: {
88
+ localTemperature: 400, // 4°C in 0.01°C units
89
+ occupiedCoolingSetpoint: 400,
90
+ systemMode: 3, // COOL
91
+ thermostatRunningMode: 3,
92
+ controlSequenceOfOperation: 2, // cooling only
93
+ },
94
+ temperatureMeasurement: {
95
+ measuredValue: 400, // 4°C
96
+ minMeasuredValue: 0, // 0°C
97
+ maxMeasuredValue: 720, // 7.2°C
98
+ },
99
+ // Refrigerator and Temperature Controlled Cabinet Mode Cluster (0x0052)
100
+ refrigeratorAndTemperatureControlledCabinetMode: {
101
+ mode: 0, // 0=Normal, 1=RapidCool, 2=RapidFreeze
102
+ },
103
+ // Refrigerator Alarm Cluster (0x0057)
104
+ refrigeratorAlarm: {
105
+ mask: 0, // Bitmap: bit 0=door open, bit 1=temp high, bit 2=temp low
106
+ state: 0, // Current alarm state
107
+ supported: 7, // Support door, temp high, temp low alarms
108
+ },
109
+ },
110
+ // Multi-endpoint structure for fridge and freezer compartments
111
+ parts: [
112
+ // Fridge compartment endpoint
113
+ {
114
+ UUID: matterAPI.uuid.generate(`${serialNumber}-fridge`),
115
+ displayName: 'Fridge Compartment',
116
+ serialNumber: `${serialNumber}-fridge`,
117
+ manufacturer: this.device.brand || 'GE Appliances',
118
+ model: `${this.device.model || 'SmartHQ'} Fridge`,
119
+ deviceType: matterAPI.deviceTypes.TemperatureSensor,
120
+ clusters: {
121
+ temperatureMeasurement: {
122
+ measuredValue: 400, // 4°C
123
+ minMeasuredValue: 0,
124
+ maxMeasuredValue: 720,
125
+ },
126
+ // Boolean state for ice maker
127
+ booleanState: {
128
+ stateValue: false, // Ice maker state
129
+ },
130
+ // Resource monitoring for water filter
131
+ resourceMonitoring: {
132
+ condition: 100, // 100% = OK, 0% = needs replacement
133
+ degradationDirection: 1, // 1 = down (degrades over time)
134
+ changeIndication: 0, // 0=OK, 1=Warning, 2=Critical
135
+ productIdentifierType: 0, // 0=UPC, 1=GTIN8, 2=EAN, 3=GTIN14
136
+ productIdentifierValue: 'FILTER',
137
+ },
138
+ },
139
+ },
140
+ // Freezer compartment endpoint
141
+ {
142
+ UUID: matterAPI.uuid.generate(`${serialNumber}-freezer`),
143
+ displayName: 'Freezer Compartment',
144
+ serialNumber: `${serialNumber}-freezer`,
145
+ manufacturer: this.device.brand || 'GE Appliances',
146
+ model: `${this.device.model || 'SmartHQ'} Freezer`,
147
+ deviceType: matterAPI.deviceTypes.TemperatureSensor,
148
+ clusters: {
149
+ temperatureMeasurement: {
150
+ measuredValue: -1800, // -18°C
151
+ minMeasuredValue: -2100,
152
+ maxMeasuredValue: -330,
153
+ },
154
+ // Boolean state for turbo freeze
155
+ booleanState: {
156
+ stateValue: false, // Turbo freeze state
157
+ },
158
+ },
159
+ },
160
+ ],
161
+ handlers: {
162
+ thermostat: {
163
+ setpointRaiseLower: async (request) => {
164
+ await this.handleMatterSetpointChange(request);
165
+ },
166
+ },
167
+ refrigeratorAndTemperatureControlledCabinetMode: {
168
+ changeToMode: async (request) => {
169
+ await this.handleMatterModeChange(request);
170
+ },
171
+ },
172
+ },
173
+ };
174
+ // Register Matter accessory as external device
175
+ await matterAPI.registerPlatformAccessories('@homebridge-plugins/homebridge-smarthq', 'SmartHQ', [matterAccessory]);
176
+ this.matterRegistered = true;
177
+ this.infoLog('Created Matter Refrigerator with advanced clusters and multi-endpoint structure');
46
178
  }
47
- async writeErd(erd, value) {
48
- await axios
49
- .post(`/appliance/${this.accessory.context.device.applianceId}/erd/${erd}`, {
50
- kind: 'appliance#erdListEntry',
51
- userId: this.accessory.context.userId,
52
- applianceId: this.accessory.context.device.applianceId,
53
- erd,
54
- value: typeof value === 'boolean' ? (value ? '01' : '00') : value,
179
+ /**
180
+ * Initialize HAP (HomeKit) protocol
181
+ */
182
+ initializeHAP() {
183
+ // Helper functions for parsing ERD values
184
+ const checkDoorAvailability = async () => {
185
+ const r = await this.readErd(ERD_TYPES.DOOR_STATUS);
186
+ if (!r || r.length < 2) {
187
+ return { byte0: false, byte1: false, byte2: false };
188
+ }
189
+ try {
190
+ const byte0 = r.substring(0, 2);
191
+ const byte1 = r.substring(2, 4);
192
+ const byte2 = r.substring(4, 6);
193
+ this.debugLog(`Door status bytes: byte0=${byte0}, byte1=${byte1}, byte2=${byte2}`);
194
+ return {
195
+ byte0: byte0 !== 'FF' && byte0 !== '',
196
+ byte1: byte1 !== 'FF' && byte1 !== '' && byte1 !== undefined,
197
+ byte2: byte2 !== 'FF' && byte2 !== '' && byte2 !== undefined,
198
+ };
199
+ }
200
+ catch (parseError) {
201
+ this.debugLog(`Door availability check failed: ${parseError}`);
202
+ return { byte0: false, byte1: false, byte2: false };
203
+ }
204
+ };
205
+ const parseDoorByte = async (byteIndex) => {
206
+ const r = await this.readErd(ERD_TYPES.DOOR_STATUS);
207
+ if (!r) {
208
+ return false;
209
+ }
210
+ try {
211
+ const byteValue = r.substring(byteIndex * 2, byteIndex * 2 + 2);
212
+ const state = Number.parseInt(byteValue, 16);
213
+ this.debugLog(`Door byte${byteIndex} value: ${byteValue} = ${state}`);
214
+ return state !== 0;
215
+ }
216
+ catch (parseError) {
217
+ this.debugLog(`Door byte${byteIndex} parse error: ${parseError}`);
218
+ return false;
219
+ }
220
+ };
221
+ // Check which door sensors to create
222
+ (async () => {
223
+ const availableDoors = await checkDoorAvailability();
224
+ // Byte 0: Fridge Right Door
225
+ if (availableDoors.byte0) {
226
+ const fridgeRightDoor = this.accessory.getService('Fridge Right Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Right Door', 'FridgeRightDoor');
227
+ fridgeRightDoor.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Right Door');
228
+ fridgeRightDoor
229
+ .getCharacteristic(this.platform.Characteristic.ContactSensorState)
230
+ .onGet(async () => {
231
+ const isOpen = await parseDoorByte(0);
232
+ return isOpen ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
233
+ });
234
+ }
235
+ // Byte 1: Fridge Left Door
236
+ if (availableDoors.byte1) {
237
+ const fridgeLeftDoor = this.accessory.getService('Fridge Left Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Left Door', 'FridgeLeftDoor');
238
+ fridgeLeftDoor.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Left Door');
239
+ fridgeLeftDoor
240
+ .getCharacteristic(this.platform.Characteristic.ContactSensorState)
241
+ .onGet(async () => {
242
+ const isOpen = await parseDoorByte(1);
243
+ return isOpen ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
244
+ });
245
+ }
246
+ // Byte 2: Freezer Door
247
+ if (availableDoors.byte2) {
248
+ const freezerDoor = this.accessory.getService('Freezer Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Freezer Door', 'FreezerDoor');
249
+ freezerDoor.setCharacteristic(this.platform.Characteristic.Name, 'Freezer Door');
250
+ freezerDoor
251
+ .getCharacteristic(this.platform.Characteristic.ContactSensorState)
252
+ .onGet(async () => {
253
+ const isOpen = await parseDoorByte(2);
254
+ return isOpen ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
255
+ });
256
+ }
257
+ })();
258
+ // Ice Bucket Status helpers
259
+ const parseIceBucketNibble = async (nibbleIndex) => {
260
+ const r = await this.readErd(ERD_TYPES.ICE_MAKER_BUCKET_STATUS);
261
+ if (!r || r.length < 2) {
262
+ return false;
263
+ }
264
+ try {
265
+ const nibbleValue = nibbleIndex === 0 ? r.charAt(0) : r.charAt(1);
266
+ const status = Number.parseInt(nibbleValue, 16);
267
+ this.debugLog(`Ice bucket nibble${nibbleIndex} value: ${nibbleValue} = ${status}`);
268
+ return status >= 2;
269
+ }
270
+ catch (parseError) {
271
+ this.debugLog(`Ice bucket nibble${nibbleIndex} parse error: ${parseError}`);
272
+ return false;
273
+ }
274
+ };
275
+ // Fridge Ice Bucket (nibble 0)
276
+ const fridgeIceBucket = this.accessory.getService('Fridge Ice Bucket') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Ice Bucket', 'FridgeIceBucket');
277
+ fridgeIceBucket.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Ice Bucket');
278
+ fridgeIceBucket
279
+ .getCharacteristic(this.platform.Characteristic.ContactSensorState)
280
+ .onGet(async () => {
281
+ const isFull = await parseIceBucketNibble(0);
282
+ return isFull ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
283
+ });
284
+ // Freezer Ice Bucket (nibble 1)
285
+ const freezerIceBucket = this.accessory.getService('Freezer Ice Bucket') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Freezer Ice Bucket', 'FreezerIceBucket');
286
+ freezerIceBucket.setCharacteristic(this.platform.Characteristic.Name, 'Freezer Ice Bucket');
287
+ freezerIceBucket
288
+ .getCharacteristic(this.platform.Characteristic.ContactSensorState)
289
+ .onGet(async () => {
290
+ const isFull = await parseIceBucketNibble(1);
291
+ return isFull ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
292
+ });
293
+ // Fridge Thermostat
294
+ const fridgeThermostat = this.accessory.getService('Fridge') ?? this.accessory.addService(this.platform.Service.Thermostat, 'Fridge', 'FridgeThermostat');
295
+ fridgeThermostat.setCharacteristic(this.platform.Characteristic.Name, 'Fridge');
296
+ fridgeThermostat.setCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits, this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT);
297
+ fridgeThermostat.setCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, this.platform.Characteristic.CurrentHeatingCoolingState.COOL);
298
+ fridgeThermostat.setCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, this.platform.Characteristic.TargetHeatingCoolingState.COOL);
299
+ fridgeThermostat
300
+ .getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
301
+ .setProps({ validValues: [this.platform.Characteristic.TargetHeatingCoolingState.COOL] });
302
+ fridgeThermostat
303
+ .getCharacteristic(this.platform.Characteristic.CurrentTemperature)
304
+ .onGet(async () => {
305
+ const temp = await this.parseTemperature('fridge');
306
+ return temp ?? 4.0;
307
+ });
308
+ fridgeThermostat
309
+ .getCharacteristic(this.platform.Characteristic.TargetTemperature)
310
+ .setProps({ minValue: 0, maxValue: 7.2, minStep: 0.5 })
311
+ .onGet(async () => {
312
+ const temp = await this.parseSetpoint('fridge');
313
+ return temp ?? 4.0;
314
+ })
315
+ .onSet(async (value) => {
316
+ await this.writeSetpoint('fridge', value);
317
+ });
318
+ // Freezer Thermostat
319
+ const freezerThermostat = this.accessory.getService('Freezer') ?? this.accessory.addService(this.platform.Service.Thermostat, 'Freezer', 'FreezerThermostat');
320
+ freezerThermostat.setCharacteristic(this.platform.Characteristic.Name, 'Freezer');
321
+ freezerThermostat.setCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits, this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT);
322
+ freezerThermostat.setCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, this.platform.Characteristic.CurrentHeatingCoolingState.COOL);
323
+ freezerThermostat.setCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, this.platform.Characteristic.TargetHeatingCoolingState.COOL);
324
+ freezerThermostat
325
+ .getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
326
+ .setProps({ validValues: [this.platform.Characteristic.TargetHeatingCoolingState.COOL] });
327
+ freezerThermostat
328
+ .getCharacteristic(this.platform.Characteristic.CurrentTemperature)
329
+ .onGet(async () => {
330
+ const temp = await this.parseTemperature('freezer');
331
+ return temp ?? -18.0;
332
+ });
333
+ freezerThermostat
334
+ .getCharacteristic(this.platform.Characteristic.TargetTemperature)
335
+ .setProps({ minValue: -21, maxValue: -3.3, minStep: 0.5 })
336
+ .onGet(async () => {
337
+ const temp = await this.parseSetpoint('freezer');
338
+ return temp ?? -18.0;
339
+ })
340
+ .onSet(async (value) => {
341
+ await this.writeSetpoint('freezer', value);
55
342
  });
343
+ // Air Filter Maintenance
344
+ const filterService = this.accessory.getService('Air Filter') ?? this.accessory.addService(this.platform.Service.FilterMaintenance, 'Air Filter', 'AirFilter');
345
+ filterService.setCharacteristic(this.platform.Characteristic.Name, 'Air Filter');
346
+ filterService
347
+ .getCharacteristic(this.platform.Characteristic.FilterChangeIndication)
348
+ .onGet(async () => {
349
+ const r = await this.readErd(ERD_TYPES.AIR_FILTER_STATUS);
350
+ if (!r) {
351
+ return this.platform.Characteristic.FilterChangeIndication.FILTER_OK;
352
+ }
353
+ return Number.parseInt(r) === 1
354
+ ? this.platform.Characteristic.FilterChangeIndication.CHANGE_FILTER
355
+ : this.platform.Characteristic.FilterChangeIndication.FILTER_OK;
356
+ });
357
+ // Ice Maker Control
358
+ const iceMakerService = this.accessory.getService('Ice Maker') ?? this.accessory.addService(this.platform.Service.Switch, 'Ice Maker', 'IceMaker');
359
+ iceMakerService.setCharacteristic(this.platform.Characteristic.Name, 'Ice Maker');
360
+ iceMakerService
361
+ .getCharacteristic(this.platform.Characteristic.On)
362
+ .onGet(async () => {
363
+ const r = await this.readErd(ERD_TYPES.ICE_MAKER_CONTROL);
364
+ return r ? Number.parseInt(r) !== 0 : false;
365
+ })
366
+ .onSet(async (value) => {
367
+ await this.writeErd(ERD_TYPES.ICE_MAKER_CONTROL, value);
368
+ });
369
+ // Turbo Cool Switch
370
+ const turboCoolService = this.accessory.getService('Turbo Cool') ?? this.accessory.addService(this.platform.Service.Switch, 'Turbo Cool', 'TurboCool');
371
+ turboCoolService.setCharacteristic(this.platform.Characteristic.Name, 'Turbo Cool');
372
+ turboCoolService
373
+ .getCharacteristic(this.platform.Characteristic.On)
374
+ .onGet(async () => {
375
+ const r = await this.readErd(ERD_TYPES.TURBO_COOL_STATUS);
376
+ return r ? Number.parseInt(r) !== 0 : false;
377
+ })
378
+ .onSet(async (value) => {
379
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, value);
380
+ });
381
+ // Turbo Freeze Switch
382
+ const turboFreezeService = this.accessory.getService('Turbo Freeze') ?? this.accessory.addService(this.platform.Service.Switch, 'Turbo Freeze', 'TurboFreeze');
383
+ turboFreezeService.setCharacteristic(this.platform.Characteristic.Name, 'Turbo Freeze');
384
+ turboFreezeService
385
+ .getCharacteristic(this.platform.Characteristic.On)
386
+ .onGet(async () => {
387
+ const r = await this.readErd(ERD_TYPES.TURBO_FREEZE_STATUS);
388
+ return r ? Number.parseInt(r) !== 0 : false;
389
+ })
390
+ .onSet(async (value) => {
391
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, value);
392
+ });
393
+ }
394
+ /**
395
+ * Shared helper: Parse temperature from ERD (works for both HAP and Matter)
396
+ */
397
+ async parseTemperature(compartment) {
398
+ const r = await this.readErd(ERD_TYPES.CURRENT_TEMPERATURE);
399
+ this.debugLog(`Raw CURRENT_TEMPERATURE ERD response: ${r}`);
400
+ if (!r || r === 'undefined') {
401
+ return undefined;
402
+ }
403
+ try {
404
+ const parsed = JSON.parse(r);
405
+ if (parsed[compartment] !== undefined) {
406
+ const tempCelsius = Number(parsed[compartment]);
407
+ this.debugLog(`${compartment} temperature: ${tempCelsius}°C`);
408
+ return tempCelsius;
409
+ }
410
+ }
411
+ catch (parseError) {
412
+ this.debugLog(`Temperature parse error: ${parseError}`);
413
+ }
56
414
  return undefined;
57
415
  }
416
+ /**
417
+ * Shared helper: Parse setpoint from ERD
418
+ */
419
+ async parseSetpoint(compartment) {
420
+ const r = await this.readErd(ERD_TYPES.TEMPERATURE_SETTING);
421
+ if (!r || r === 'undefined') {
422
+ return undefined;
423
+ }
424
+ try {
425
+ const parsed = JSON.parse(r);
426
+ if (parsed[compartment] !== undefined) {
427
+ const tempCelsius = Number(parsed[compartment]);
428
+ this.debugLog(`${compartment} setpoint: ${tempCelsius}°C`);
429
+ return tempCelsius;
430
+ }
431
+ }
432
+ catch (parseError) {
433
+ this.debugLog(`Setpoint parse error: ${parseError}`);
434
+ }
435
+ return undefined;
436
+ }
437
+ /**
438
+ * Shared helper: Write setpoint to ERD
439
+ */
440
+ async writeSetpoint(compartment, temperature) {
441
+ try {
442
+ const value = Math.round(temperature).toString();
443
+ const erdData = JSON.stringify({ [compartment]: value });
444
+ await this.writeErd(ERD_TYPES.TEMPERATURE_SETTING, erdData);
445
+ await this.successLog(`Set ${compartment} temperature to ${value}°C`);
446
+ }
447
+ catch (error) {
448
+ await this.errorLog(`Failed to write ${compartment} setpoint: ${error?.message ?? error}`);
449
+ }
450
+ }
451
+ /**
452
+ * Handle Matter setpoint change
453
+ */
454
+ async handleMatterSetpointChange(request) {
455
+ try {
456
+ const amount = request.amount;
457
+ const currentSetpoint = await this.parseSetpoint('fridge') ?? 4;
458
+ const newSetpoint = currentSetpoint + (amount / 10);
459
+ await this.infoLog(`Matter setpoint adjust: ${amount * 0.1}°C (${currentSetpoint}°C -> ${newSetpoint}°C)`);
460
+ await this.writeSetpoint('fridge', newSetpoint);
461
+ }
462
+ catch (error) {
463
+ await this.errorLog(`Failed to adjust Matter setpoint: ${error?.message ?? error}`);
464
+ }
465
+ }
466
+ /**
467
+ * Handle Matter mode change (Normal, Rapid Cool, Rapid Freeze)
468
+ */
469
+ async handleMatterModeChange(request) {
470
+ try {
471
+ const newMode = request.newMode;
472
+ await this.infoLog(`Matter mode change to: ${newMode === 0 ? 'Normal' : newMode === 1 ? 'Rapid Cool' : 'Rapid Freeze'}`);
473
+ // Map Matter modes to SmartHQ ERD codes
474
+ if (newMode === 1) {
475
+ // Rapid Cool - enable Turbo Cool
476
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, true);
477
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, false);
478
+ }
479
+ else if (newMode === 2) {
480
+ // Rapid Freeze - enable Turbo Freeze
481
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, false);
482
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, true);
483
+ }
484
+ else {
485
+ // Normal - disable both
486
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, false);
487
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, false);
488
+ }
489
+ // Update the mode cluster
490
+ if (this.matterUuid && this.matterRegistered) {
491
+ const matterAPI = this.api.matter;
492
+ await matterAPI.updateAccessoryState(this.matterUuid, 'refrigeratorAndTemperatureControlledCabinetMode', { mode: newMode });
493
+ }
494
+ }
495
+ catch (error) {
496
+ await this.errorLog(`Failed to change Matter mode: ${error?.message ?? error}`);
497
+ }
498
+ }
499
+ /**
500
+ * Refresh device status - update both HAP and Matter states
501
+ */
502
+ async refreshDeviceStatus() {
503
+ try {
504
+ this.SensorUpdateInProgress = true;
505
+ // Get temperatures
506
+ const fridgeTemp = await this.parseTemperature('fridge');
507
+ const freezerTemp = await this.parseTemperature('freezer');
508
+ // Get setpoints
509
+ const fridgeSetpoint = await this.parseSetpoint('fridge');
510
+ const freezerSetpoint = await this.parseSetpoint('freezer');
511
+ // Get door status for alarm cluster
512
+ const doorStatus = await this.readErd(ERD_TYPES.DOOR_STATUS);
513
+ // Get ice maker status
514
+ const iceMakerStatus = await this.readErd(ERD_TYPES.ICE_MAKER_CONTROL);
515
+ // Get turbo cool/freeze status for mode cluster
516
+ const turboCoolStatus = await this.readErd(ERD_TYPES.TURBO_COOL_STATUS);
517
+ const turboFreezeStatus = await this.readErd(ERD_TYPES.TURBO_FREEZE_STATUS);
518
+ // Get filter status for resource monitoring
519
+ const filterStatus = await this.readErd(ERD_TYPES.AIR_FILTER_STATUS);
520
+ // Update Matter state if using Matter
521
+ if (this.useMatterOverride && this.matterUuid && this.matterRegistered) {
522
+ try {
523
+ const matterAPI = this.api.matter;
524
+ // Update fridge values
525
+ if (fridgeTemp !== undefined) {
526
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { localTemperature: Math.round(fridgeTemp * 100) });
527
+ await matterAPI.updateAccessoryState(this.matterUuid, 'temperatureMeasurement', { measuredValue: Math.round(fridgeTemp * 100) });
528
+ }
529
+ // Update fridge setpoint
530
+ if (fridgeSetpoint !== undefined) {
531
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { occupiedCoolingSetpoint: Math.round(fridgeSetpoint * 100) });
532
+ }
533
+ // Update freezer values
534
+ if (freezerTemp !== undefined) {
535
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { localTemperature: Math.round(freezerTemp * 100) });
536
+ }
537
+ // Update freezer setpoint
538
+ if (freezerSetpoint !== undefined) {
539
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { occupiedCoolingSetpoint: Math.round(freezerSetpoint * 100) });
540
+ }
541
+ // Update refrigerator mode cluster based on turbo status
542
+ let currentMode = 0; // Normal
543
+ if (turboCoolStatus && Number.parseInt(turboCoolStatus) !== 0) {
544
+ currentMode = 1; // Rapid Cool
545
+ }
546
+ else if (turboFreezeStatus && Number.parseInt(turboFreezeStatus) !== 0) {
547
+ currentMode = 2; // Rapid Freeze
548
+ }
549
+ await matterAPI.updateAccessoryState(this.matterUuid, 'refrigeratorAndTemperatureControlledCabinetMode', { mode: currentMode });
550
+ // Update refrigerator alarm cluster
551
+ let alarmState = 0;
552
+ if (doorStatus) {
553
+ // Check if any door is open (non-zero value)
554
+ const byte0 = doorStatus.substring(0, 2);
555
+ const byte1 = doorStatus.substring(2, 4);
556
+ const byte2 = doorStatus.substring(4, 6);
557
+ if (byte0 !== '00' || byte1 !== '00' || byte2 !== '00') {
558
+ alarmState |= 1; // Set door open alarm bit
559
+ }
560
+ }
561
+ await matterAPI.updateAccessoryState(this.matterUuid, 'refrigeratorAlarm', { state: alarmState });
562
+ // Update fridge compartment endpoint
563
+ const fridgeEndpointUuid = matterAPI.uuid.generate(`${this.device.applianceId}-fridge`);
564
+ if (fridgeTemp !== undefined) {
565
+ await matterAPI.updateAccessoryState(fridgeEndpointUuid, 'temperatureMeasurement', { measuredValue: Math.round(fridgeTemp * 100) });
566
+ }
567
+ // Update fridge ice maker boolean state
568
+ if (iceMakerStatus) {
569
+ await matterAPI.updateAccessoryState(fridgeEndpointUuid, 'booleanState', { stateValue: Number.parseInt(iceMakerStatus) !== 0 });
570
+ }
571
+ // Update fridge filter resource monitoring
572
+ if (filterStatus) {
573
+ const needsReplacement = Number.parseInt(filterStatus) === 1;
574
+ await matterAPI.updateAccessoryState(fridgeEndpointUuid, 'resourceMonitoring', {
575
+ condition: needsReplacement ? 0 : 100,
576
+ changeIndication: needsReplacement ? 2 : 0, // 2=Critical, 0=OK
577
+ });
578
+ }
579
+ // Update freezer compartment endpoint
580
+ const freezerEndpointUuid = matterAPI.uuid.generate(`${this.device.applianceId}-freezer`);
581
+ if (freezerTemp !== undefined) {
582
+ await matterAPI.updateAccessoryState(freezerEndpointUuid, 'temperatureMeasurement', { measuredValue: Math.round(freezerTemp * 100) });
583
+ }
584
+ // Update freezer turbo freeze boolean state
585
+ if (turboFreezeStatus) {
586
+ await matterAPI.updateAccessoryState(freezerEndpointUuid, 'booleanState', { stateValue: Number.parseInt(turboFreezeStatus) !== 0 });
587
+ }
588
+ }
589
+ catch (error) {
590
+ // Matter updates failed, but this is recoverable - log and continue
591
+ // Suppress "not found or not registered" errors as they're expected during startup
592
+ if (error?.message?.includes('not found or not registered')) {
593
+ await this.debugLog(`Matter accessory not yet registered, will retry on next refresh cycle`);
594
+ }
595
+ else {
596
+ await this.errorLog(`Failed to update Matter accessory state: ${error?.message ?? error}`);
597
+ }
598
+ }
599
+ }
600
+ this.SensorUpdateInProgress = false;
601
+ }
602
+ catch (error) {
603
+ this.SensorUpdateInProgress = false;
604
+ await this.errorLog(`Failed to refresh device status: ${error?.message ?? error}`);
605
+ }
606
+ }
58
607
  }
59
608
  //# sourceMappingURL=refrigerator.js.map