@homebridge-plugins/homebridge-smarthq 0.5.0-beta.21 → 0.5.0-beta.23

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 (51) hide show
  1. package/MATTER.md +304 -0
  2. package/dist/devices/advantium.js.map +1 -1
  3. package/dist/devices/airConditioner.js.map +1 -1
  4. package/dist/devices/beverageCenter.js.map +1 -1
  5. package/dist/devices/clothesDryer.js.map +1 -1
  6. package/dist/devices/clothesWasher.js.map +1 -1
  7. package/dist/devices/coffeeMaker.js.map +1 -1
  8. package/dist/devices/device.d.ts +83 -21
  9. package/dist/devices/device.d.ts.map +1 -1
  10. package/dist/devices/device.js +216 -70
  11. package/dist/devices/device.js.map +1 -1
  12. package/dist/devices/dishwasher.js.map +1 -1
  13. package/dist/devices/hood.js.map +1 -1
  14. package/dist/devices/microwave.js.map +1 -1
  15. package/dist/devices/oven.js.map +1 -1
  16. package/dist/devices/refrigerator.d.ts +37 -0
  17. package/dist/devices/refrigerator.d.ts.map +1 -1
  18. package/dist/devices/refrigerator.js +398 -170
  19. package/dist/devices/refrigerator.js.map +1 -1
  20. package/dist/devices/waterFilter.js.map +1 -1
  21. package/dist/devices/waterHeater.js.map +1 -1
  22. package/dist/devices/waterSoftener.js.map +1 -1
  23. package/dist/platform.d.ts +15 -1
  24. package/dist/platform.d.ts.map +1 -1
  25. package/dist/platform.js +82 -32
  26. package/dist/platform.js.map +1 -1
  27. package/dist/settings.d.ts +1 -0
  28. package/dist/settings.d.ts.map +1 -1
  29. package/dist/settings.js.map +1 -1
  30. package/docs/assets/search.js +1 -1
  31. package/docs/classes/SmartHQPlatform.html +15 -9
  32. package/docs/interfaces/DeviceOptions.html +2 -2
  33. package/docs/interfaces/SmartHQPlatformConfig.html +2 -2
  34. package/docs/interfaces/SmartHqContext.html +2 -2
  35. package/docs/interfaces/SmartHqERDResponse.html +2 -2
  36. package/docs/interfaces/credentials.html +2 -2
  37. package/docs/interfaces/devicesConfig.html +3 -2
  38. package/docs/interfaces/options.html +2 -2
  39. package/docs/variables/API_URL.html +1 -1
  40. package/docs/variables/ERD_CODES.html +1 -1
  41. package/docs/variables/ERD_TYPES.html +1 -1
  42. package/docs/variables/KEEPALIVE_TIMEOUT.html +1 -1
  43. package/docs/variables/LOGIN_URL.html +1 -1
  44. package/docs/variables/OAUTH2_CLIENT_ID.html +1 -1
  45. package/docs/variables/OAUTH2_CLIENT_SECRET.html +1 -1
  46. package/docs/variables/OAUTH2_REDIRECT_URI.html +1 -1
  47. package/docs/variables/PLATFORM_NAME.html +1 -1
  48. package/docs/variables/PLUGIN_NAME.html +1 -1
  49. package/docs/variables/SECURE_URL.html +1 -1
  50. package/docs/variables/default.html +1 -1
  51. package/package.json +1 -1
@@ -1,38 +1,172 @@
1
1
  import { interval, skipWhile } from 'rxjs';
2
2
  import { ERD_TYPES } from '../settings.js';
3
3
  import { deviceBase } from './device.js';
4
+ /**
5
+ * SmartHQ Refrigerator - Unified HAP/Matter Implementation
6
+ * Supports both HomeKit Accessory Protocol and Matter protocol
7
+ */
4
8
  export class SmartHQRefrigerator extends deviceBase {
5
9
  platform;
6
10
  device;
7
11
  // Updates
8
12
  SensorUpdateInProgress;
9
13
  deviceStatus;
14
+ // Matter support override flag
15
+ useMatterOverride = false;
10
16
  constructor(platform, accessory, device) {
11
17
  super(platform, accessory, device);
12
18
  this.platform = platform;
13
19
  this.device = device;
20
+ // Check if we should use Matter protocol
21
+ this.useMatterOverride = device.useMatter ?? false;
14
22
  this.debugLog(`Refrigerator Features: ${JSON.stringify(accessory.context.device.features)}`);
15
- // DOOR_STATUS ERD returns hex string where each byte represents a door compartment
16
- // Format: "XXYYZZ..." where XX=byte0, YY=byte1, etc.
17
- // 0xFF = not available/unused, 0x00 = closed, 0x01 = open
18
- // Byte mapping for this fridge model:
19
- // byte0 (pos 0-1) = Fridge Right Door
20
- // byte1 (pos 2-3) = Fridge Left Door
21
- // byte2 (pos 4-5) = Freezer Door
22
- // byte3 (pos 6-7) = Unused (0xFF)
23
- // Check which door bytes are available (not 0xFF)
23
+ this.debugLog(`Using protocol: ${this.useMatterOverride ? 'Matter' : 'HAP'}`);
24
+ // Initialize the appropriate protocol
25
+ if (this.useMatterOverride) {
26
+ this.initializeMatter();
27
+ }
28
+ else {
29
+ this.initializeHAP();
30
+ }
31
+ // Start periodic refresh
32
+ this.SensorUpdateInProgress = false;
33
+ interval(this.deviceRefreshRate * 1000)
34
+ .pipe(skipWhile(() => this.SensorUpdateInProgress))
35
+ .subscribe(async () => {
36
+ await this.refreshDeviceStatus();
37
+ });
38
+ // Initial refresh after 2 seconds
39
+ setTimeout(() => this.refreshDeviceStatus(), 2000);
40
+ }
41
+ /**
42
+ * Initialize Matter protocol
43
+ */
44
+ initializeMatter() {
45
+ const matterAPI = this.api.matter;
46
+ if (!matterAPI) {
47
+ this.errorLog('Matter API not available');
48
+ return;
49
+ }
50
+ const serialNumber = this.device.applianceId || 'unknown';
51
+ this.matterUuid = matterAPI.uuid.generate(serialNumber);
52
+ // Create Matter accessory configuration with refrigerator-specific clusters
53
+ const matterAccessory = {
54
+ UUID: this.matterUuid,
55
+ displayName: this.device.nickname || 'SmartHQ Refrigerator',
56
+ serialNumber,
57
+ manufacturer: this.device.brand && this.device.brand !== 'Unknown' ? this.device.brand : 'GE Appliances',
58
+ model: this.device.model || 'SmartHQ',
59
+ firmwareRevision: this.deviceFirmwareVersion,
60
+ hardwareRevision: this.deviceFirmwareVersion,
61
+ deviceType: matterAPI.deviceTypes.RefrigeratorFreezer,
62
+ clusters: {
63
+ // Temperature control for main compartment
64
+ thermostat: {
65
+ localTemperature: 400, // 4°C in 0.01°C units
66
+ occupiedCoolingSetpoint: 400,
67
+ systemMode: 3, // COOL
68
+ thermostatRunningMode: 3,
69
+ controlSequenceOfOperation: 2, // cooling only
70
+ },
71
+ temperatureMeasurement: {
72
+ measuredValue: 400, // 4°C
73
+ minMeasuredValue: 0, // 0°C
74
+ maxMeasuredValue: 720, // 7.2°C
75
+ },
76
+ // Refrigerator and Temperature Controlled Cabinet Mode Cluster (0x0052)
77
+ refrigeratorAndTemperatureControlledCabinetMode: {
78
+ mode: 0, // 0=Normal, 1=RapidCool, 2=RapidFreeze
79
+ },
80
+ // Refrigerator Alarm Cluster (0x0057)
81
+ refrigeratorAlarm: {
82
+ mask: 0, // Bitmap: bit 0=door open, bit 1=temp high, bit 2=temp low
83
+ state: 0, // Current alarm state
84
+ supported: 7, // Support door, temp high, temp low alarms
85
+ },
86
+ },
87
+ // Multi-endpoint structure for fridge and freezer compartments
88
+ parts: [
89
+ // Fridge compartment endpoint
90
+ {
91
+ UUID: matterAPI.uuid.generate(`${serialNumber}-fridge`),
92
+ displayName: 'Fridge Compartment',
93
+ serialNumber: `${serialNumber}-fridge`,
94
+ manufacturer: this.device.brand || 'GE Appliances',
95
+ model: `${this.device.model || 'SmartHQ'} Fridge`,
96
+ deviceType: matterAPI.deviceTypes.TemperatureSensor,
97
+ clusters: {
98
+ temperatureMeasurement: {
99
+ measuredValue: 400, // 4°C
100
+ minMeasuredValue: 0,
101
+ maxMeasuredValue: 720,
102
+ },
103
+ // Boolean state for ice maker
104
+ booleanState: {
105
+ stateValue: false, // Ice maker state
106
+ },
107
+ // Resource monitoring for water filter
108
+ resourceMonitoring: {
109
+ condition: 100, // 100% = OK, 0% = needs replacement
110
+ degradationDirection: 1, // 1 = down (degrades over time)
111
+ changeIndication: 0, // 0=OK, 1=Warning, 2=Critical
112
+ productIdentifierType: 0, // 0=UPC, 1=GTIN8, 2=EAN, 3=GTIN14
113
+ productIdentifierValue: 'FILTER',
114
+ },
115
+ },
116
+ },
117
+ // Freezer compartment endpoint
118
+ {
119
+ UUID: matterAPI.uuid.generate(`${serialNumber}-freezer`),
120
+ displayName: 'Freezer Compartment',
121
+ serialNumber: `${serialNumber}-freezer`,
122
+ manufacturer: this.device.brand || 'GE Appliances',
123
+ model: `${this.device.model || 'SmartHQ'} Freezer`,
124
+ deviceType: matterAPI.deviceTypes.TemperatureSensor,
125
+ clusters: {
126
+ temperatureMeasurement: {
127
+ measuredValue: -1800, // -18°C
128
+ minMeasuredValue: -2100,
129
+ maxMeasuredValue: -330,
130
+ },
131
+ // Boolean state for turbo freeze
132
+ booleanState: {
133
+ stateValue: false, // Turbo freeze state
134
+ },
135
+ },
136
+ },
137
+ ],
138
+ handlers: {
139
+ thermostat: {
140
+ setpointRaiseLower: async (request) => {
141
+ await this.handleMatterSetpointChange(request);
142
+ },
143
+ },
144
+ refrigeratorAndTemperatureControlledCabinetMode: {
145
+ changeToMode: async (request) => {
146
+ await this.handleMatterModeChange(request);
147
+ },
148
+ },
149
+ },
150
+ };
151
+ // Register Matter accessory
152
+ matterAPI.registerAccessory(matterAccessory);
153
+ this.infoLog('Created Matter Refrigerator with advanced clusters and multi-endpoint structure');
154
+ }
155
+ /**
156
+ * Initialize HAP (HomeKit) protocol
157
+ */
158
+ initializeHAP() {
159
+ // Helper functions for parsing ERD values
24
160
  const checkDoorAvailability = async () => {
25
161
  const r = await this.readErd(ERD_TYPES.DOOR_STATUS);
26
162
  if (!r || r.length < 2) {
27
163
  return { byte0: false, byte1: false, byte2: false };
28
164
  }
29
165
  try {
30
- // Parse hex bytes - each byte is 2 characters
31
166
  const byte0 = r.substring(0, 2);
32
167
  const byte1 = r.substring(2, 4);
33
168
  const byte2 = r.substring(4, 6);
34
169
  this.debugLog(`Door status bytes: byte0=${byte0}, byte1=${byte1}, byte2=${byte2}`);
35
- // A byte is available if it exists and isn't 0xFF (unused)
36
170
  return {
37
171
  byte0: byte0 !== 'FF' && byte0 !== '',
38
172
  byte1: byte1 !== 'FF' && byte1 !== '' && byte1 !== undefined,
@@ -44,7 +178,6 @@ export class SmartHQRefrigerator extends deviceBase {
44
178
  return { byte0: false, byte1: false, byte2: false };
45
179
  }
46
180
  };
47
- // Helper to parse individual door byte
48
181
  const parseDoorByte = async (byteIndex) => {
49
182
  const r = await this.readErd(ERD_TYPES.DOOR_STATUS);
50
183
  if (!r) {
@@ -54,7 +187,7 @@ export class SmartHQRefrigerator extends deviceBase {
54
187
  const byteValue = r.substring(byteIndex * 2, byteIndex * 2 + 2);
55
188
  const state = Number.parseInt(byteValue, 16);
56
189
  this.debugLog(`Door byte${byteIndex} value: ${byteValue} = ${state}`);
57
- return state !== 0; // 0 = closed, anything else = open
190
+ return state !== 0;
58
191
  }
59
192
  catch (parseError) {
60
193
  this.debugLog(`Door byte${byteIndex} parse error: ${parseError}`);
@@ -66,48 +199,52 @@ export class SmartHQRefrigerator extends deviceBase {
66
199
  const availableDoors = await checkDoorAvailability();
67
200
  // Byte 0: Fridge Right Door
68
201
  if (availableDoors.byte0) {
69
- const doorService = this.accessory.getService('Fridge Right Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Right Door', 'FridgeRightDoor');
70
- doorService.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Right Door');
71
- doorService
202
+ const fridgeRightDoor = this.accessory.getService('Fridge Right Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Right Door', 'FridgeRightDoor');
203
+ fridgeRightDoor.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Right Door');
204
+ fridgeRightDoor
72
205
  .getCharacteristic(this.platform.Characteristic.ContactSensorState)
73
- .onGet(async () => parseDoorByte(0));
206
+ .onGet(async () => {
207
+ const isOpen = await parseDoorByte(0);
208
+ return isOpen ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
209
+ });
74
210
  }
75
211
  // Byte 1: Fridge Left Door
76
212
  if (availableDoors.byte1) {
77
- const door2Service = this.accessory.getService('Fridge Left Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Left Door', 'FridgeLeftDoor');
78
- door2Service.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Left Door');
79
- door2Service
213
+ const fridgeLeftDoor = this.accessory.getService('Fridge Left Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Fridge Left Door', 'FridgeLeftDoor');
214
+ fridgeLeftDoor.setCharacteristic(this.platform.Characteristic.Name, 'Fridge Left Door');
215
+ fridgeLeftDoor
80
216
  .getCharacteristic(this.platform.Characteristic.ContactSensorState)
81
- .onGet(async () => parseDoorByte(1));
217
+ .onGet(async () => {
218
+ const isOpen = await parseDoorByte(1);
219
+ return isOpen ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
220
+ });
82
221
  }
83
222
  // Byte 2: Freezer Door
84
223
  if (availableDoors.byte2) {
85
- const door3Service = this.accessory.getService('Freezer Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Freezer Door', 'FreezerDoor');
86
- door3Service.setCharacteristic(this.platform.Characteristic.Name, 'Freezer Door');
87
- door3Service
224
+ const freezerDoor = this.accessory.getService('Freezer Door') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Freezer Door', 'FreezerDoor');
225
+ freezerDoor.setCharacteristic(this.platform.Characteristic.Name, 'Freezer Door');
226
+ freezerDoor
88
227
  .getCharacteristic(this.platform.Characteristic.ContactSensorState)
89
- .onGet(async () => parseDoorByte(2));
228
+ .onGet(async () => {
229
+ const isOpen = await parseDoorByte(2);
230
+ return isOpen ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
231
+ });
90
232
  }
91
233
  })();
92
- // Ice Bucket Status (Contact Sensors - "open" when full)
93
- // Format: "XY" where X=fridge ice maker, Y=freezer ice maker
94
- // Each nibble: 0=empty, 1=not full, 2=full, 3=full
95
- // "Open" state (CONTACT_NOT_DETECTED) = bucket is full
96
- // Helper to parse ice bucket nibble (half-byte)
234
+ // Ice Bucket Status helpers
97
235
  const parseIceBucketNibble = async (nibbleIndex) => {
98
236
  const r = await this.readErd(ERD_TYPES.ICE_MAKER_BUCKET_STATUS);
99
237
  if (!r || r.length < 2) {
100
- return false; // Not full
238
+ return false;
101
239
  }
102
240
  try {
103
- const nibble = r.charAt(nibbleIndex);
104
- const status = Number.parseInt(nibble, 16);
105
- const isFull = status >= 2;
106
- this.debugLog(`Ice Bucket ${nibbleIndex === 0 ? 'Fridge' : 'Freezer'} status: ${nibble} (${status}) - ${isFull ? 'Full' : 'Not Full'}`);
107
- return isFull;
241
+ const nibbleValue = nibbleIndex === 0 ? r.charAt(0) : r.charAt(1);
242
+ const status = Number.parseInt(nibbleValue, 16);
243
+ this.debugLog(`Ice bucket nibble${nibbleIndex} value: ${nibbleValue} = ${status}`);
244
+ return status >= 2;
108
245
  }
109
246
  catch (parseError) {
110
- this.debugLog(`Ice Bucket ${nibbleIndex} parse error: ${parseError}`);
247
+ this.debugLog(`Ice bucket nibble${nibbleIndex} parse error: ${parseError}`);
111
248
  return false;
112
249
  }
113
250
  };
@@ -118,9 +255,7 @@ export class SmartHQRefrigerator extends deviceBase {
118
255
  .getCharacteristic(this.platform.Characteristic.ContactSensorState)
119
256
  .onGet(async () => {
120
257
  const isFull = await parseIceBucketNibble(0);
121
- return isFull
122
- ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED // Open = full
123
- : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED; // Closed = not full
258
+ return isFull ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
124
259
  });
125
260
  // Freezer Ice Bucket (nibble 1)
126
261
  const freezerIceBucket = this.accessory.getService('Freezer Ice Bucket') ?? this.accessory.addService(this.platform.Service.ContactSensor, 'Freezer Ice Bucket', 'FreezerIceBucket');
@@ -129,155 +264,57 @@ export class SmartHQRefrigerator extends deviceBase {
129
264
  .getCharacteristic(this.platform.Characteristic.ContactSensorState)
130
265
  .onGet(async () => {
131
266
  const isFull = await parseIceBucketNibble(1);
132
- return isFull
133
- ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED // Open = full
134
- : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED; // Closed = not full
267
+ return isFull ? this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
135
268
  });
136
- // Fridge Thermostat (with temperature sensor and target temp control)
269
+ // Fridge Thermostat
137
270
  const fridgeThermostat = this.accessory.getService('Fridge') ?? this.accessory.addService(this.platform.Service.Thermostat, 'Fridge', 'FridgeThermostat');
138
271
  fridgeThermostat.setCharacteristic(this.platform.Characteristic.Name, 'Fridge');
139
272
  fridgeThermostat.setCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits, this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT);
140
273
  fridgeThermostat.setCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, this.platform.Characteristic.CurrentHeatingCoolingState.COOL);
141
274
  fridgeThermostat.setCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, this.platform.Characteristic.TargetHeatingCoolingState.COOL);
275
+ fridgeThermostat
276
+ .getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
277
+ .setProps({ validValues: [this.platform.Characteristic.TargetHeatingCoolingState.COOL] });
142
278
  fridgeThermostat
143
279
  .getCharacteristic(this.platform.Characteristic.CurrentTemperature)
144
280
  .onGet(async () => {
145
- const r = await this.readErd(ERD_TYPES.CURRENT_TEMPERATURE);
146
- this.debugLog(`Raw CURRENT_TEMPERATURE ERD response: ${r}`);
147
- if (!r || r === 'undefined') {
148
- return 2.8; // Default to ~37°F in Celsius
149
- }
150
- try {
151
- const temps = JSON.parse(r);
152
- const fridgeTempF = temps.fridge || temps.Fridge;
153
- this.debugLog(`Fridge current temp: ${fridgeTempF}°F`);
154
- if (!fridgeTempF || Number.isNaN(Number(fridgeTempF))) {
155
- return 2.8;
156
- }
157
- return (Number(fridgeTempF) - 32) * 5 / 9;
158
- }
159
- catch (parseError) {
160
- this.debugLog(`Fridge Temperature: JSON parse error, using default`);
161
- return 2.8;
162
- }
281
+ const temp = await this.parseTemperature('fridge');
282
+ return temp ?? 4.0;
163
283
  });
164
284
  fridgeThermostat
165
285
  .getCharacteristic(this.platform.Characteristic.TargetTemperature)
166
- .setProps({ minValue: 0, maxValue: 7.2, minStep: 0.5 }) // 32°F to 45°F range
286
+ .setProps({ minValue: 0, maxValue: 7.2, minStep: 0.5 })
167
287
  .onGet(async () => {
168
- const r = await this.readErd(ERD_TYPES.TEMPERATURE_SETTING);
169
- if (!r || r === 'undefined') {
170
- return 3.3; // Default to ~38°F
171
- }
172
- try {
173
- const setpoints = JSON.parse(r);
174
- const fridgeTargetF = setpoints.fridge || setpoints.Fridge;
175
- this.debugLog(`Fridge target temp: ${fridgeTargetF}°F`);
176
- if (!fridgeTargetF || Number.isNaN(Number(fridgeTargetF))) {
177
- return 3.3;
178
- }
179
- return (Number(fridgeTargetF) - 32) * 5 / 9;
180
- }
181
- catch (parseError) {
182
- this.debugLog(`Fridge Target Temperature: JSON parse error, using default`);
183
- return 3.3;
184
- }
288
+ const temp = await this.parseSetpoint('fridge');
289
+ return temp ?? 4.0;
185
290
  })
186
291
  .onSet(async (value) => {
187
- try {
188
- // Convert Celsius to Fahrenheit
189
- const targetF = Math.round(value * 9 / 5 + 32);
190
- this.infoLog(`Setting Fridge target temperature to ${targetF}°F`);
191
- // Get current freezer setting to preserve it
192
- const currentSettings = await this.readErd(ERD_TYPES.TEMPERATURE_SETTING);
193
- let freezerTargetF = 0; // Default
194
- if (currentSettings && currentSettings !== 'undefined') {
195
- try {
196
- const setpoints = JSON.parse(currentSettings);
197
- freezerTargetF = setpoints.freezer || setpoints.Freezer || 0;
198
- }
199
- catch { }
200
- }
201
- // Write both fridge and freezer settings
202
- const newSettings = JSON.stringify({ fridge: targetF, freezer: freezerTargetF });
203
- await this.writeErd(ERD_TYPES.TEMPERATURE_SETTING, newSettings);
204
- }
205
- catch (error) {
206
- this.warnLog?.(`Fridge set target temp error: ${error?.message ?? error}`);
207
- }
292
+ await this.writeSetpoint('fridge', value);
208
293
  });
209
- // Freezer Thermostat (with temperature sensor and target temp control)
294
+ // Freezer Thermostat
210
295
  const freezerThermostat = this.accessory.getService('Freezer') ?? this.accessory.addService(this.platform.Service.Thermostat, 'Freezer', 'FreezerThermostat');
211
296
  freezerThermostat.setCharacteristic(this.platform.Characteristic.Name, 'Freezer');
212
297
  freezerThermostat.setCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits, this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT);
213
298
  freezerThermostat.setCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, this.platform.Characteristic.CurrentHeatingCoolingState.COOL);
214
299
  freezerThermostat.setCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, this.platform.Characteristic.TargetHeatingCoolingState.COOL);
300
+ freezerThermostat
301
+ .getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
302
+ .setProps({ validValues: [this.platform.Characteristic.TargetHeatingCoolingState.COOL] });
215
303
  freezerThermostat
216
304
  .getCharacteristic(this.platform.Characteristic.CurrentTemperature)
217
305
  .onGet(async () => {
218
- const r = await this.readErd(ERD_TYPES.CURRENT_TEMPERATURE);
219
- this.debugLog(`Raw CURRENT_TEMPERATURE ERD response: ${r}`);
220
- if (!r || r === 'undefined') {
221
- return -17.8; // Default to ~0°F in Celsius
222
- }
223
- try {
224
- const temps = JSON.parse(r);
225
- const freezerTempF = temps.freezer || temps.Freezer;
226
- this.debugLog(`Freezer current temp: ${freezerTempF}°F`);
227
- if (!freezerTempF || Number.isNaN(Number(freezerTempF))) {
228
- return -17.8;
229
- }
230
- return (Number(freezerTempF) - 32) * 5 / 9;
231
- }
232
- catch (parseError) {
233
- this.debugLog(`Freezer Temperature: JSON parse error, using default`);
234
- return -17.8;
235
- }
306
+ const temp = await this.parseTemperature('freezer');
307
+ return temp ?? -18.0;
236
308
  });
237
309
  freezerThermostat
238
310
  .getCharacteristic(this.platform.Characteristic.TargetTemperature)
239
- .setProps({ minValue: -21, maxValue: -3.3, minStep: 0.5 }) // -6°F to 26°F range
311
+ .setProps({ minValue: -21, maxValue: -3.3, minStep: 0.5 })
240
312
  .onGet(async () => {
241
- const r = await this.readErd(ERD_TYPES.TEMPERATURE_SETTING);
242
- if (!r || r === 'undefined') {
243
- return -17.8; // Default to ~0°F
244
- }
245
- try {
246
- const setpoints = JSON.parse(r);
247
- const freezerTargetF = setpoints.freezer || setpoints.Freezer;
248
- this.debugLog(`Freezer target temp: ${freezerTargetF}°F`);
249
- if (!freezerTargetF || Number.isNaN(Number(freezerTargetF))) {
250
- return -17.8;
251
- }
252
- return (Number(freezerTargetF) - 32) * 5 / 9;
253
- }
254
- catch (parseError) {
255
- this.debugLog(`Freezer Target Temperature: JSON parse error, using default`);
256
- return -17.8;
257
- }
313
+ const temp = await this.parseSetpoint('freezer');
314
+ return temp ?? -18.0;
258
315
  })
259
316
  .onSet(async (value) => {
260
- try {
261
- // Convert Celsius to Fahrenheit
262
- const targetF = Math.round(value * 9 / 5 + 32);
263
- this.infoLog(`Setting Freezer target temperature to ${targetF}°F`);
264
- // Get current fridge setting to preserve it
265
- const currentSettings = await this.readErd(ERD_TYPES.TEMPERATURE_SETTING);
266
- let fridgeTargetF = 37; // Default
267
- if (currentSettings && currentSettings !== 'undefined') {
268
- try {
269
- const setpoints = JSON.parse(currentSettings);
270
- fridgeTargetF = setpoints.fridge || setpoints.Fridge || 37;
271
- }
272
- catch { }
273
- }
274
- // Write both fridge and freezer settings
275
- const newSettings = JSON.stringify({ fridge: fridgeTargetF, freezer: targetF });
276
- await this.writeErd(ERD_TYPES.TEMPERATURE_SETTING, newSettings);
277
- }
278
- catch (error) {
279
- this.warnLog?.(`Freezer set target temp error: ${error?.message ?? error}`);
280
- }
317
+ await this.writeSetpoint('freezer', value);
281
318
  });
282
319
  // Air Filter Maintenance
283
320
  const filterService = this.accessory.getService('Air Filter') ?? this.accessory.addService(this.platform.Service.FilterMaintenance, 'Air Filter', 'AirFilter');
@@ -293,7 +330,7 @@ export class SmartHQRefrigerator extends deviceBase {
293
330
  ? this.platform.Characteristic.FilterChangeIndication.CHANGE_FILTER
294
331
  : this.platform.Characteristic.FilterChangeIndication.FILTER_OK;
295
332
  });
296
- // Ice Maker Control (Switch)
333
+ // Ice Maker Control
297
334
  const iceMakerService = this.accessory.getService('Ice Maker') ?? this.accessory.addService(this.platform.Service.Switch, 'Ice Maker', 'IceMaker');
298
335
  iceMakerService.setCharacteristic(this.platform.Characteristic.Name, 'Ice Maker');
299
336
  iceMakerService
@@ -329,16 +366,207 @@ export class SmartHQRefrigerator extends deviceBase {
329
366
  .onSet(async (value) => {
330
367
  await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, value);
331
368
  });
332
- // this is subject we use to track when we need to POST changes to the SmartHQ API
333
- this.SensorUpdateInProgress = false;
334
- // Retrieve initial values and updateHomekit
335
- // this.refreshStatus()
336
- // Start an update interval
337
- interval(this.deviceRefreshRate * 10000)
338
- .pipe(skipWhile(() => this.SensorUpdateInProgress))
339
- .subscribe(async () => {
340
- // await this.refreshStatus()
341
- });
369
+ }
370
+ /**
371
+ * Shared helper: Parse temperature from ERD (works for both HAP and Matter)
372
+ */
373
+ async parseTemperature(compartment) {
374
+ const r = await this.readErd(ERD_TYPES.CURRENT_TEMPERATURE);
375
+ this.debugLog(`Raw CURRENT_TEMPERATURE ERD response: ${r}`);
376
+ if (!r || r === 'undefined') {
377
+ return undefined;
378
+ }
379
+ try {
380
+ const parsed = JSON.parse(r);
381
+ if (parsed[compartment] !== undefined) {
382
+ const tempCelsius = Number(parsed[compartment]);
383
+ this.debugLog(`${compartment} temperature: ${tempCelsius}°C`);
384
+ return tempCelsius;
385
+ }
386
+ }
387
+ catch (parseError) {
388
+ this.debugLog(`Temperature parse error: ${parseError}`);
389
+ }
390
+ return undefined;
391
+ }
392
+ /**
393
+ * Shared helper: Parse setpoint from ERD
394
+ */
395
+ async parseSetpoint(compartment) {
396
+ const r = await this.readErd(ERD_TYPES.TEMPERATURE_SETTING);
397
+ if (!r || r === 'undefined') {
398
+ return undefined;
399
+ }
400
+ try {
401
+ const parsed = JSON.parse(r);
402
+ if (parsed[compartment] !== undefined) {
403
+ const tempCelsius = Number(parsed[compartment]);
404
+ this.debugLog(`${compartment} setpoint: ${tempCelsius}°C`);
405
+ return tempCelsius;
406
+ }
407
+ }
408
+ catch (parseError) {
409
+ this.debugLog(`Setpoint parse error: ${parseError}`);
410
+ }
411
+ return undefined;
412
+ }
413
+ /**
414
+ * Shared helper: Write setpoint to ERD
415
+ */
416
+ async writeSetpoint(compartment, temperature) {
417
+ try {
418
+ const value = Math.round(temperature).toString();
419
+ const erdData = JSON.stringify({ [compartment]: value });
420
+ await this.writeErd(ERD_TYPES.TEMPERATURE_SETTING, erdData);
421
+ await this.successLog(`Set ${compartment} temperature to ${value}°C`);
422
+ }
423
+ catch (error) {
424
+ await this.errorLog(`Failed to write ${compartment} setpoint: ${error?.message ?? error}`);
425
+ }
426
+ }
427
+ /**
428
+ * Handle Matter setpoint change
429
+ */
430
+ async handleMatterSetpointChange(request) {
431
+ try {
432
+ const amount = request.amount;
433
+ const currentSetpoint = await this.parseSetpoint('fridge') ?? 4;
434
+ const newSetpoint = currentSetpoint + (amount / 10);
435
+ await this.infoLog(`Matter setpoint adjust: ${amount * 0.1}°C (${currentSetpoint}°C -> ${newSetpoint}°C)`);
436
+ await this.writeSetpoint('fridge', newSetpoint);
437
+ }
438
+ catch (error) {
439
+ await this.errorLog(`Failed to adjust Matter setpoint: ${error?.message ?? error}`);
440
+ }
441
+ }
442
+ /**
443
+ * Handle Matter mode change (Normal, Rapid Cool, Rapid Freeze)
444
+ */
445
+ async handleMatterModeChange(request) {
446
+ try {
447
+ const newMode = request.newMode;
448
+ await this.infoLog(`Matter mode change to: ${newMode === 0 ? 'Normal' : newMode === 1 ? 'Rapid Cool' : 'Rapid Freeze'}`);
449
+ // Map Matter modes to SmartHQ ERD codes
450
+ if (newMode === 1) {
451
+ // Rapid Cool - enable Turbo Cool
452
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, true);
453
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, false);
454
+ }
455
+ else if (newMode === 2) {
456
+ // Rapid Freeze - enable Turbo Freeze
457
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, false);
458
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, true);
459
+ }
460
+ else {
461
+ // Normal - disable both
462
+ await this.writeErd(ERD_TYPES.TURBO_COOL_STATUS, false);
463
+ await this.writeErd(ERD_TYPES.TURBO_FREEZE_STATUS, false);
464
+ }
465
+ // Update the mode cluster
466
+ if (this.matterUuid) {
467
+ const matterAPI = this.api.matter;
468
+ await matterAPI.updateAccessoryState(this.matterUuid, 'refrigeratorAndTemperatureControlledCabinetMode', { mode: newMode });
469
+ }
470
+ }
471
+ catch (error) {
472
+ await this.errorLog(`Failed to change Matter mode: ${error?.message ?? error}`);
473
+ }
474
+ }
475
+ /**
476
+ * Refresh device status - update both HAP and Matter states
477
+ */
478
+ async refreshDeviceStatus() {
479
+ try {
480
+ this.SensorUpdateInProgress = true;
481
+ // Get temperatures
482
+ const fridgeTemp = await this.parseTemperature('fridge');
483
+ const freezerTemp = await this.parseTemperature('freezer');
484
+ // Get setpoints
485
+ const fridgeSetpoint = await this.parseSetpoint('fridge');
486
+ const freezerSetpoint = await this.parseSetpoint('freezer');
487
+ // Get door status for alarm cluster
488
+ const doorStatus = await this.readErd(ERD_TYPES.DOOR_STATUS);
489
+ // Get ice maker status
490
+ const iceMakerStatus = await this.readErd(ERD_TYPES.ICE_MAKER_CONTROL);
491
+ // Get turbo cool/freeze status for mode cluster
492
+ const turboCoolStatus = await this.readErd(ERD_TYPES.TURBO_COOL_STATUS);
493
+ const turboFreezeStatus = await this.readErd(ERD_TYPES.TURBO_FREEZE_STATUS);
494
+ // Get filter status for resource monitoring
495
+ const filterStatus = await this.readErd(ERD_TYPES.AIR_FILTER_STATUS);
496
+ // Update Matter state if using Matter
497
+ if (this.useMatterOverride && this.matterUuid) {
498
+ const matterAPI = this.api.matter;
499
+ // Update fridge values
500
+ if (fridgeTemp !== undefined) {
501
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { localTemperature: Math.round(fridgeTemp * 100) });
502
+ await matterAPI.updateAccessoryState(this.matterUuid, 'temperatureMeasurement', { measuredValue: Math.round(fridgeTemp * 100) });
503
+ }
504
+ // Update fridge setpoint
505
+ if (fridgeSetpoint !== undefined) {
506
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { occupiedCoolingSetpoint: Math.round(fridgeSetpoint * 100) });
507
+ }
508
+ // Update freezer values
509
+ if (freezerTemp !== undefined) {
510
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { localTemperature: Math.round(freezerTemp * 100) });
511
+ }
512
+ // Update freezer setpoint
513
+ if (freezerSetpoint !== undefined) {
514
+ await matterAPI.updateAccessoryState(this.matterUuid, matterAPI.clusterNames.Thermostat, { occupiedCoolingSetpoint: Math.round(freezerSetpoint * 100) });
515
+ }
516
+ // Update refrigerator mode cluster based on turbo status
517
+ let currentMode = 0; // Normal
518
+ if (turboCoolStatus && Number.parseInt(turboCoolStatus) !== 0) {
519
+ currentMode = 1; // Rapid Cool
520
+ }
521
+ else if (turboFreezeStatus && Number.parseInt(turboFreezeStatus) !== 0) {
522
+ currentMode = 2; // Rapid Freeze
523
+ }
524
+ await matterAPI.updateAccessoryState(this.matterUuid, 'refrigeratorAndTemperatureControlledCabinetMode', { mode: currentMode });
525
+ // Update refrigerator alarm cluster
526
+ let alarmState = 0;
527
+ if (doorStatus) {
528
+ // Check if any door is open (non-zero value)
529
+ const byte0 = doorStatus.substring(0, 2);
530
+ const byte1 = doorStatus.substring(2, 4);
531
+ const byte2 = doorStatus.substring(4, 6);
532
+ if (byte0 !== '00' || byte1 !== '00' || byte2 !== '00') {
533
+ alarmState |= 1; // Set door open alarm bit
534
+ }
535
+ }
536
+ await matterAPI.updateAccessoryState(this.matterUuid, 'refrigeratorAlarm', { state: alarmState });
537
+ // Update fridge compartment endpoint
538
+ const fridgeEndpointUuid = matterAPI.uuid.generate(`${this.device.applianceId}-fridge`);
539
+ if (fridgeTemp !== undefined) {
540
+ await matterAPI.updateAccessoryState(fridgeEndpointUuid, 'temperatureMeasurement', { measuredValue: Math.round(fridgeTemp * 100) });
541
+ }
542
+ // Update fridge ice maker boolean state
543
+ if (iceMakerStatus) {
544
+ await matterAPI.updateAccessoryState(fridgeEndpointUuid, 'booleanState', { stateValue: Number.parseInt(iceMakerStatus) !== 0 });
545
+ }
546
+ // Update fridge filter resource monitoring
547
+ if (filterStatus) {
548
+ const needsReplacement = Number.parseInt(filterStatus) === 1;
549
+ await matterAPI.updateAccessoryState(fridgeEndpointUuid, 'resourceMonitoring', {
550
+ condition: needsReplacement ? 0 : 100,
551
+ changeIndication: needsReplacement ? 2 : 0, // 2=Critical, 0=OK
552
+ });
553
+ }
554
+ // Update freezer compartment endpoint
555
+ const freezerEndpointUuid = matterAPI.uuid.generate(`${this.device.applianceId}-freezer`);
556
+ if (freezerTemp !== undefined) {
557
+ await matterAPI.updateAccessoryState(freezerEndpointUuid, 'temperatureMeasurement', { measuredValue: Math.round(freezerTemp * 100) });
558
+ }
559
+ // Update freezer turbo freeze boolean state
560
+ if (turboFreezeStatus) {
561
+ await matterAPI.updateAccessoryState(freezerEndpointUuid, 'booleanState', { stateValue: Number.parseInt(turboFreezeStatus) !== 0 });
562
+ }
563
+ }
564
+ this.SensorUpdateInProgress = false;
565
+ }
566
+ catch (error) {
567
+ this.SensorUpdateInProgress = false;
568
+ await this.errorLog(`Failed to refresh device status: ${error?.message ?? error}`);
569
+ }
342
570
  }
343
571
  }
344
572
  //# sourceMappingURL=refrigerator.js.map