@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.
- package/MATTER.md +304 -0
- package/dist/devices/advantium.js.map +1 -1
- package/dist/devices/airConditioner.js.map +1 -1
- package/dist/devices/beverageCenter.js.map +1 -1
- package/dist/devices/clothesDryer.js.map +1 -1
- package/dist/devices/clothesWasher.js.map +1 -1
- package/dist/devices/coffeeMaker.js.map +1 -1
- package/dist/devices/device.d.ts +83 -21
- package/dist/devices/device.d.ts.map +1 -1
- package/dist/devices/device.js +216 -70
- package/dist/devices/device.js.map +1 -1
- package/dist/devices/dishwasher.js.map +1 -1
- package/dist/devices/hood.js.map +1 -1
- package/dist/devices/microwave.js.map +1 -1
- package/dist/devices/oven.js.map +1 -1
- package/dist/devices/refrigerator.d.ts +37 -0
- package/dist/devices/refrigerator.d.ts.map +1 -1
- package/dist/devices/refrigerator.js +398 -170
- package/dist/devices/refrigerator.js.map +1 -1
- package/dist/devices/waterFilter.js.map +1 -1
- package/dist/devices/waterHeater.js.map +1 -1
- package/dist/devices/waterSoftener.js.map +1 -1
- package/dist/platform.d.ts +15 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +82 -32
- package/dist/platform.js.map +1 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/SmartHQPlatform.html +15 -9
- package/docs/interfaces/DeviceOptions.html +2 -2
- package/docs/interfaces/SmartHQPlatformConfig.html +2 -2
- package/docs/interfaces/SmartHqContext.html +2 -2
- package/docs/interfaces/SmartHqERDResponse.html +2 -2
- package/docs/interfaces/credentials.html +2 -2
- package/docs/interfaces/devicesConfig.html +3 -2
- package/docs/interfaces/options.html +2 -2
- package/docs/variables/API_URL.html +1 -1
- package/docs/variables/ERD_CODES.html +1 -1
- package/docs/variables/ERD_TYPES.html +1 -1
- package/docs/variables/KEEPALIVE_TIMEOUT.html +1 -1
- package/docs/variables/LOGIN_URL.html +1 -1
- package/docs/variables/OAUTH2_CLIENT_ID.html +1 -1
- package/docs/variables/OAUTH2_CLIENT_SECRET.html +1 -1
- package/docs/variables/OAUTH2_REDIRECT_URI.html +1 -1
- package/docs/variables/PLATFORM_NAME.html +1 -1
- package/docs/variables/PLUGIN_NAME.html +1 -1
- package/docs/variables/SECURE_URL.html +1 -1
- package/docs/variables/default.html +1 -1
- 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
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
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;
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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 () =>
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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 () =>
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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 () =>
|
|
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
|
|
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;
|
|
238
|
+
return false;
|
|
101
239
|
}
|
|
102
240
|
try {
|
|
103
|
-
const
|
|
104
|
-
const status = Number.parseInt(
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
|
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
|
|
146
|
-
|
|
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 })
|
|
286
|
+
.setProps({ minValue: 0, maxValue: 7.2, minStep: 0.5 })
|
|
167
287
|
.onGet(async () => {
|
|
168
|
-
const
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
219
|
-
|
|
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 })
|
|
311
|
+
.setProps({ minValue: -21, maxValue: -3.3, minStep: 0.5 })
|
|
240
312
|
.onGet(async () => {
|
|
241
|
-
const
|
|
242
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|