mclimate-payload-helper 1.2.1 → 1.2.3
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/dist/decoders/commandsReadingHelper.d.ts.map +1 -1
- package/dist/decoders/commandsReadingHelper.js +214 -121
- package/dist/decoders/commandsReadingHelper.js.map +1 -1
- package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.d.ts.map +1 -1
- package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.js +12 -12
- package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.js.map +1 -1
- package/dist/encoders/Co2DisplayCommonCommands.d.ts +4 -0
- package/dist/encoders/Co2DisplayCommonCommands.d.ts.map +1 -1
- package/dist/encoders/Co2DisplayCommonCommands.js +38 -0
- package/dist/encoders/Co2DisplayCommonCommands.js.map +1 -1
- package/dist/encoders/CommandBuilder.d.ts.map +1 -1
- package/dist/encoders/CommandBuilder.js +1 -0
- package/dist/encoders/CommandBuilder.js.map +1 -1
- package/dist/encoders/MelissaCommands.d.ts +6 -0
- package/dist/encoders/MelissaCommands.d.ts.map +1 -0
- package/dist/encoders/MelissaCommands.js +35 -0
- package/dist/encoders/MelissaCommands.js.map +1 -0
- package/dist/encoders/index.d.ts +1 -0
- package/dist/encoders/index.d.ts.map +1 -1
- package/dist/encoders/index.js +1 -0
- package/dist/encoders/index.js.map +1 -1
- package/dist/encoders/types/schemas.d.ts +105 -0
- package/dist/encoders/types/schemas.d.ts.map +1 -1
- package/dist/encoders/types/schemas.js +5 -0
- package/dist/encoders/types/schemas.js.map +1 -1
- package/dist/test/payloadDecoders.test.js +28 -10
- package/dist/test/payloadDecoders.test.js.map +1 -1
- package/dist/test/payloadEncoders.test.js +6 -0
- package/dist/test/payloadEncoders.test.js.map +1 -1
- package/package.json +1 -1
- package/src/decoders/commandsReadingHelper.ts +204 -122
- package/src/decoders/payloadParsers/melissaLorawanPayloadParser.ts +19 -16
- package/src/encoders/CommandBuilder.ts +2 -0
- package/src/encoders/MelissaCommands.ts +33 -0
- package/src/encoders/index.ts +1 -0
- package/src/encoders/types/schemas.ts +17 -0
- package/src/test/payloadDecoders.test.ts +30 -11
- package/src/test/payloadEncoders.test.ts +8 -0
- package/dist/encoders/FanCoilThermostat.d.ts +0 -61
- package/dist/encoders/FanCoilThermostat.d.ts.map +0 -1
- package/dist/encoders/FanCoilThermostat.js +0 -719
- package/dist/encoders/FanCoilThermostat.js.map +0 -1
- package/dist/encoders/SetAqiLed.d.ts +0 -6
- package/dist/encoders/SetAqiLed.d.ts.map +0 -1
- package/dist/encoders/SetAqiLed.js +0 -46
- package/dist/encoders/SetAqiLed.js.map +0 -1
- package/dist/encoders/types/enumValidation.d.ts +0 -6
- package/dist/encoders/types/enumValidation.d.ts.map +0 -1
- package/dist/encoders/types/enumValidation.js +0 -50
- package/dist/encoders/types/enumValidation.js.map +0 -1
- package/dist/encoders/types/schemaValidatedEnums.d.ts +0 -40
- package/dist/encoders/types/schemaValidatedEnums.d.ts.map +0 -1
- package/dist/encoders/types/schemaValidatedEnums.js +0 -181
- package/dist/encoders/types/schemaValidatedEnums.js.map +0 -1
- package/dist/types/deviceTypes.d.ts +0 -19
- package/dist/types/deviceTypes.d.ts.map +0 -1
- package/dist/types/deviceTypes.js +0 -23
- package/dist/types/deviceTypes.js.map +0 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -18
- package/dist/types/index.js.map +0 -1
- package/dist/types/types.d.ts +0 -13
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/types.js +0 -9
- package/dist/types/types.js.map +0 -1
- package/dist/validation.d.ts +0 -3
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -8
- package/dist/validation.js.map +0 -1
|
@@ -198,6 +198,68 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
break
|
|
201
|
+
case '0a':
|
|
202
|
+
{
|
|
203
|
+
try {
|
|
204
|
+
let data;
|
|
205
|
+
if (deviceType === DeviceType.MelissaLorawan) {
|
|
206
|
+
command_len = 4;
|
|
207
|
+
let recordedIrCodeSize = (parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16)
|
|
208
|
+
let bytesSent = `${commands[i + 3]}${commands[i + 4]}`
|
|
209
|
+
data = { recordedIrInfo: { recordedIrCodeSize, bytesSent } }
|
|
210
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
throw new CustomError({
|
|
214
|
+
message: `Failed to process command '0a'`,
|
|
215
|
+
hexData,
|
|
216
|
+
command,
|
|
217
|
+
deviceType,
|
|
218
|
+
originalError: e as Error,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case '0b':
|
|
224
|
+
{
|
|
225
|
+
try {
|
|
226
|
+
let data;
|
|
227
|
+
if (deviceType === DeviceType.MelissaLorawan) {
|
|
228
|
+
// First byte after command indicates how many bytes follow
|
|
229
|
+
const bytesCount = parseInt(commands[i + 1], 16);
|
|
230
|
+
command_len = 1 + bytesCount; // 1 for bytesCount + the actual bytes
|
|
231
|
+
|
|
232
|
+
// Next two bytes are buffer offset
|
|
233
|
+
const bufferOffsetMSB = parseInt(commands[i + 2], 16);
|
|
234
|
+
const bufferOffsetLSB = parseInt(commands[i + 3], 16);
|
|
235
|
+
const address = (bufferOffsetMSB << 8) | bufferOffsetLSB;
|
|
236
|
+
|
|
237
|
+
// The rest is IR code data
|
|
238
|
+
let irCodeDataString = '';
|
|
239
|
+
for (let j = 4; j < 4 + bytesCount - 2; j++) { // -2 to account for the offset bytes
|
|
240
|
+
irCodeDataString += commands[i + j];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
data = {
|
|
244
|
+
irCodeData: {
|
|
245
|
+
bytesCount: bytesCount - 2,
|
|
246
|
+
address,
|
|
247
|
+
data: irCodeDataString
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data });
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
throw new CustomError({
|
|
254
|
+
message: `Failed to process command '0b'`,
|
|
255
|
+
hexData,
|
|
256
|
+
command,
|
|
257
|
+
deviceType,
|
|
258
|
+
originalError: e as Error,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
201
263
|
case '0e':
|
|
202
264
|
{
|
|
203
265
|
try {
|
|
@@ -1588,10 +1650,24 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
1588
1650
|
{
|
|
1589
1651
|
try {
|
|
1590
1652
|
let data
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1653
|
+
if (deviceType === DeviceType.Vicki) {
|
|
1654
|
+
command_len = 2;
|
|
1655
|
+
let notificationByte = parseInt(commands[i + 1], 16);
|
|
1656
|
+
// Extract notification flags from bits
|
|
1657
|
+
let temperatureRestoredAfterManualBoost = !!(notificationByte & 0x01); // Bit 0
|
|
1658
|
+
let temperatureChangedByHeatingSchedule = !!(notificationByte & 0x02); // Bit 1
|
|
1659
|
+
data = {
|
|
1660
|
+
notifications: {
|
|
1661
|
+
temperatureRestoredAfterManualBoost: temperatureRestoredAfterManualBoost,
|
|
1662
|
+
temperatureChangedByHeatingSchedule: temperatureChangedByHeatingSchedule
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
}else{
|
|
1666
|
+
command_len = 3
|
|
1667
|
+
let state = parseInt(commands[i + 1], 16)
|
|
1668
|
+
let time = (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16)
|
|
1669
|
+
data = { relayTimerInSeconds: { state, time } }
|
|
1670
|
+
}
|
|
1595
1671
|
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
1596
1672
|
} catch (e) {
|
|
1597
1673
|
throw new CustomError({
|
|
@@ -1656,6 +1732,9 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
1656
1732
|
if (deviceType === DeviceType.FanCoilThermostat) {
|
|
1657
1733
|
command_len = 1
|
|
1658
1734
|
data = { fanSpeedNotOccupied: parseInt(commands[i + 1], 16) }
|
|
1735
|
+
}else{
|
|
1736
|
+
command_len = 1
|
|
1737
|
+
data = { manualChangeRelayState: parseInt(commands[i + 1], 16) }
|
|
1659
1738
|
}
|
|
1660
1739
|
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
1661
1740
|
} catch (e) {
|
|
@@ -1701,6 +1780,11 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
1701
1780
|
data = {
|
|
1702
1781
|
overheatingEvents: { events: parseInt(commands[i + 1], 16), temperature: parseInt(commands[i + 2], 16) },
|
|
1703
1782
|
}
|
|
1783
|
+
} else if (deviceType === DeviceType.Vicki) {
|
|
1784
|
+
command_len = 1;
|
|
1785
|
+
let offsetByte = parseInt(commands[i + 1], 16);
|
|
1786
|
+
let offsetHours = (offsetByte & 0x80) ? (offsetByte - 256) : offsetByte;
|
|
1787
|
+
data = { deviceTimeZone: offsetHours };
|
|
1704
1788
|
}
|
|
1705
1789
|
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
1706
1790
|
} catch (e) {
|
|
@@ -1754,6 +1838,10 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
1754
1838
|
current: (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16),
|
|
1755
1839
|
},
|
|
1756
1840
|
}
|
|
1841
|
+
}else{
|
|
1842
|
+
command_len = 1;
|
|
1843
|
+
let timeValue = parseInt(commands[i + 1], 16);
|
|
1844
|
+
data = { autoSetpointRestoreStatus: timeValue === 0 ? 0 : timeValue * 10 };
|
|
1757
1845
|
}
|
|
1758
1846
|
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
1759
1847
|
} catch (e) {
|
|
@@ -1893,6 +1981,9 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
1893
1981
|
if (deviceType === DeviceType.FanCoilThermostat) {
|
|
1894
1982
|
command_len = 1
|
|
1895
1983
|
data = { frostProtectionStatus: parseInt(commands[i + 1], 16) }
|
|
1984
|
+
}else{
|
|
1985
|
+
command_len = 1;
|
|
1986
|
+
data = { timeRequestByMACcommand: parseInt(commands[i + 1], 16) };
|
|
1896
1987
|
}
|
|
1897
1988
|
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
1898
1989
|
} catch (e) {
|
|
@@ -2307,23 +2398,6 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
2307
2398
|
}
|
|
2308
2399
|
}
|
|
2309
2400
|
break
|
|
2310
|
-
case '58': {
|
|
2311
|
-
command_len = 1;
|
|
2312
|
-
let notificationByte = parseInt(commands[i + 1], 16);
|
|
2313
|
-
|
|
2314
|
-
// Extract notification flags from bits
|
|
2315
|
-
let temperatureRestoredAfterManualBoost = !!(notificationByte & 0x01); // Bit 0
|
|
2316
|
-
let temperatureChangedByHeatingSchedule = !!(notificationByte & 0x02); // Bit 1
|
|
2317
|
-
|
|
2318
|
-
let data = {
|
|
2319
|
-
notifications: {
|
|
2320
|
-
temperatureRestoredAfterManualBoost: temperatureRestoredAfterManualBoost,
|
|
2321
|
-
temperatureChangedByHeatingSchedule: temperatureChangedByHeatingSchedule
|
|
2322
|
-
}
|
|
2323
|
-
};
|
|
2324
|
-
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2325
|
-
}
|
|
2326
|
-
break;
|
|
2327
2401
|
case '5a':
|
|
2328
2402
|
{
|
|
2329
2403
|
try {
|
|
@@ -2441,24 +2515,6 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
2441
2515
|
}
|
|
2442
2516
|
}
|
|
2443
2517
|
break
|
|
2444
|
-
case '5d':
|
|
2445
|
-
{
|
|
2446
|
-
try {
|
|
2447
|
-
command_len = 1
|
|
2448
|
-
let data = { manualChangeRelayState: parseInt(commands[i + 1], 16) }
|
|
2449
|
-
|
|
2450
|
-
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2451
|
-
} catch (e) {
|
|
2452
|
-
throw new CustomError({
|
|
2453
|
-
message: `Failed to process command '5d'`,
|
|
2454
|
-
hexData,
|
|
2455
|
-
command,
|
|
2456
|
-
deviceType,
|
|
2457
|
-
originalError: e as Error,
|
|
2458
|
-
})
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
break
|
|
2462
2518
|
case '5e':
|
|
2463
2519
|
{
|
|
2464
2520
|
try {
|
|
@@ -2487,116 +2543,142 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
|
|
|
2487
2543
|
})
|
|
2488
2544
|
}
|
|
2489
2545
|
}
|
|
2490
|
-
break
|
|
2491
|
-
case '60': {
|
|
2492
|
-
command_len = 1;
|
|
2493
|
-
let offsetByte = parseInt(commands[i + 1], 16);
|
|
2494
|
-
let offsetHours = (offsetByte & 0x80) ? (offsetByte - 256) : offsetByte;
|
|
2495
|
-
let data = { deviceTimeZone: offsetHours };
|
|
2496
|
-
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2497
|
-
}
|
|
2498
2546
|
break;
|
|
2499
|
-
case '62': {
|
|
2500
|
-
command_len = 1;
|
|
2501
|
-
let timeValue = parseInt(commands[i + 1], 16);
|
|
2502
|
-
let data = { autoSetpointRestoreStatus: timeValue === 0 ? 0 : timeValue * 10 };
|
|
2503
|
-
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2504
|
-
}
|
|
2505
|
-
break;
|
|
2506
|
-
|
|
2507
2547
|
case '64': {
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2548
|
+
try {
|
|
2549
|
+
command_len = 1;
|
|
2550
|
+
let ledDurationValue = parseInt(commands[i + 1], 16);
|
|
2551
|
+
let durationInSeconds = ledDurationValue / 2; // As per the docs, value is divided by 2 to get seconds
|
|
2511
2552
|
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2553
|
+
let data = {
|
|
2554
|
+
ledIndicationDuration: durationInSeconds
|
|
2555
|
+
};
|
|
2556
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2557
|
+
} catch (e) {
|
|
2558
|
+
throw new CustomError({
|
|
2559
|
+
message: `Failed to process command '64'`,
|
|
2560
|
+
hexData,
|
|
2561
|
+
command,
|
|
2562
|
+
deviceType,
|
|
2563
|
+
originalError: e as Error,
|
|
2564
|
+
})
|
|
2565
|
+
}
|
|
2516
2566
|
}
|
|
2517
2567
|
break;
|
|
2518
|
-
|
|
2519
2568
|
case '66': {
|
|
2520
|
-
|
|
2569
|
+
try {
|
|
2570
|
+
command_len = 2;
|
|
2521
2571
|
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2572
|
+
let tempHigh = parseInt(commands[i + 1], 16);
|
|
2573
|
+
let tempLow = parseInt(commands[i + 2], 16);
|
|
2574
|
+
let targetTemp = (tempHigh << 8) | tempLow;
|
|
2575
|
+
let data = {
|
|
2576
|
+
offlineTargetTemperature: targetTemp === 0 ? 0 : targetTemp / 10
|
|
2577
|
+
};
|
|
2578
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2579
|
+
} catch (e) {
|
|
2580
|
+
throw new CustomError({
|
|
2581
|
+
message: `Failed to process command '66'`,
|
|
2582
|
+
hexData,
|
|
2583
|
+
command,
|
|
2584
|
+
deviceType,
|
|
2585
|
+
originalError: e as Error,
|
|
2586
|
+
})
|
|
2587
|
+
}
|
|
2529
2588
|
}
|
|
2530
2589
|
break;
|
|
2531
2590
|
case '68': {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2591
|
+
try {
|
|
2592
|
+
command_len = 1;
|
|
2593
|
+
let data = { internalAlgoTemporaryState: parseInt(commands[i + 1], 16) === 0 ? true : false };
|
|
2594
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2595
|
+
} catch (e) {
|
|
2596
|
+
throw new CustomError({
|
|
2597
|
+
message: `Failed to process command '68'`,
|
|
2598
|
+
hexData,
|
|
2599
|
+
command,
|
|
2600
|
+
deviceType,
|
|
2601
|
+
originalError: e as Error,
|
|
2602
|
+
})
|
|
2603
|
+
}
|
|
2535
2604
|
}
|
|
2536
2605
|
break;
|
|
2537
2606
|
case '6a': {
|
|
2538
|
-
|
|
2539
|
-
|
|
2607
|
+
try {
|
|
2608
|
+
command_len = 12;
|
|
2609
|
+
let temperatureLevels: { [key: string]: number } = {};
|
|
2540
2610
|
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2611
|
+
// Process 6 scale levels (0-5), each with a 2-byte temperature value
|
|
2612
|
+
for (let level = 0; level < 6; level++) {
|
|
2613
|
+
let tempHighByte = parseInt(commands[i + 1 + (level * 2)], 16);
|
|
2614
|
+
let tempLowByte = parseInt(commands[i + 2 + (level * 2)], 16);
|
|
2615
|
+
let tempValue = (tempHighByte << 8) | tempLowByte;
|
|
2546
2616
|
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2617
|
+
// The temperature values are pre-multiplied by 10
|
|
2618
|
+
temperatureLevels['level' + level] = tempValue / 10;
|
|
2619
|
+
}
|
|
2550
2620
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2621
|
+
let data = {
|
|
2622
|
+
temperatureLevels: temperatureLevels
|
|
2623
|
+
};
|
|
2554
2624
|
|
|
2555
|
-
|
|
2625
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data })
|
|
2626
|
+
} catch (e) {
|
|
2627
|
+
throw new CustomError({
|
|
2628
|
+
message: `Failed to process command '6a'`,
|
|
2629
|
+
hexData,
|
|
2630
|
+
command,
|
|
2631
|
+
deviceType,
|
|
2632
|
+
originalError: e as Error,
|
|
2633
|
+
})
|
|
2634
|
+
}
|
|
2556
2635
|
}
|
|
2557
2636
|
break;
|
|
2558
2637
|
case '6c': {
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2638
|
+
try {
|
|
2639
|
+
command_len = 4;
|
|
2640
|
+
let eventsHighByte = parseInt(commands[i + 2], 16); // Events 19-16
|
|
2641
|
+
let eventsMidByte = parseInt(commands[i + 3], 16); // Events 15-8
|
|
2642
|
+
let eventsLowByte = parseInt(commands[i + 4], 16); // Events 7-0
|
|
2563
2643
|
|
|
2564
|
-
|
|
2565
|
-
|
|
2644
|
+
// Create a more structured and readable format for heating events
|
|
2645
|
+
let heatingEventStates: { [key: string]: boolean } = {};
|
|
2566
2646
|
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2647
|
+
// Process all 20 events (0-19) in a single loop
|
|
2648
|
+
for (let eventIdx = 0; eventIdx < 20; eventIdx++) {
|
|
2649
|
+
// Calculate which bit to check
|
|
2650
|
+
let bitPosition;
|
|
2651
|
+
if (eventIdx >= 16) {
|
|
2652
|
+
// Events 16-19 in high byte
|
|
2653
|
+
bitPosition = eventIdx - 16;
|
|
2654
|
+
heatingEventStates[eventIdx] = !!(eventsHighByte & (1 << bitPosition));
|
|
2655
|
+
} else if (eventIdx >= 8) {
|
|
2656
|
+
// Events 8-15 in mid byte
|
|
2657
|
+
bitPosition = eventIdx - 8;
|
|
2658
|
+
heatingEventStates[eventIdx] = !!(eventsMidByte & (1 << bitPosition));
|
|
2659
|
+
} else {
|
|
2660
|
+
// Events 0-7 in low byte
|
|
2661
|
+
bitPosition = eventIdx;
|
|
2662
|
+
heatingEventStates[eventIdx] = !!(eventsLowByte & (1 << bitPosition));
|
|
2663
|
+
}
|
|
2583
2664
|
}
|
|
2584
|
-
}
|
|
2585
2665
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2666
|
+
let data = {
|
|
2667
|
+
heatingEventStates: heatingEventStates
|
|
2668
|
+
};
|
|
2589
2669
|
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2670
|
+
Object.assign(resultToPass, { ...resultToPass }, { ...data });
|
|
2671
|
+
} catch (e) {
|
|
2672
|
+
throw new CustomError({
|
|
2673
|
+
message: `Failed to process command '6c'`,
|
|
2674
|
+
hexData,
|
|
2675
|
+
command,
|
|
2676
|
+
deviceType,
|
|
2677
|
+
originalError: e as Error,
|
|
2678
|
+
})
|
|
2679
|
+
}
|
|
2597
2680
|
break;
|
|
2598
2681
|
}
|
|
2599
|
-
|
|
2600
2682
|
case '9b':
|
|
2601
2683
|
{
|
|
2602
2684
|
try {
|
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
import { commandsReadingHelper } from '@/decoders/commandsReadingHelper'
|
|
2
|
-
import { DeviceType
|
|
2
|
+
import { DeviceType } from '@/decoders/payloadParsers/types'
|
|
3
3
|
import { byteArrayParser } from '@/helpers'
|
|
4
4
|
import { CustomError } from '@/utils'
|
|
5
5
|
|
|
6
|
+
interface MelissaKeepAliveData {
|
|
7
|
+
sensorTemperature?: number
|
|
8
|
+
relativeHumidity?: number
|
|
9
|
+
isIrCodeRecordingRequested?: boolean
|
|
10
|
+
}
|
|
6
11
|
export const melissaLorawanPayloadParser = (hexData: string) => {
|
|
7
12
|
let deviceData = {}
|
|
8
13
|
|
|
9
14
|
try {
|
|
10
15
|
const handleKeepAliveData = (bytes: number[]) => {
|
|
11
|
-
let keepaliveData:
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
keepaliveData.internalTemperature = isNegative ? -temperature : temperature;
|
|
17
|
-
|
|
18
|
-
// Energy data
|
|
19
|
-
keepaliveData.energy = ((bytes[2] << 24) | (bytes[3] << 16) | (bytes[4] << 8) | bytes[5]) / 1000
|
|
16
|
+
let keepaliveData: MelissaKeepAliveData = {}
|
|
17
|
+
// Internal temperature sensor data from bytes 1-2 (16 bits)
|
|
18
|
+
// According to the screenshot: t[°C] = (T[15:0] - 400) / 10
|
|
19
|
+
const rawTemperature = (bytes[1] << 8) | bytes[2];
|
|
20
|
+
keepaliveData.sensorTemperature = Number(((rawTemperature - 400) / 10).toFixed(2));
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
-
|
|
22
|
+
// Relative Humidity from byte 3
|
|
23
|
+
// According to the screenshot: RH[%] = (XX * 100) / 256
|
|
24
|
+
keepaliveData.relativeHumidity = Number(((bytes[3] * 100) / 256).toFixed(2));
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
+
// Device status information (byte 4)
|
|
27
|
+
// Bit 7 indicates if IR code recording is requested (1) or not (0)
|
|
28
|
+
keepaliveData.isIrCodeRecordingRequested = ((bytes[4] & 0x80) !== 0);
|
|
26
29
|
|
|
27
30
|
Object.assign(deviceData, { ...deviceData }, { ...keepaliveData })
|
|
28
31
|
}
|
|
@@ -35,7 +38,7 @@ export const melissaLorawanPayloadParser = (hexData: string) => {
|
|
|
35
38
|
handleKeepAliveData(byteArray)
|
|
36
39
|
} else {
|
|
37
40
|
// parse command answers
|
|
38
|
-
let data = commandsReadingHelper(hexData,
|
|
41
|
+
let data = commandsReadingHelper(hexData, 10, DeviceType.MelissaLorawan)
|
|
39
42
|
if (!data) return
|
|
40
43
|
const shouldKeepAlive = data.hasOwnProperty('decodeKeepalive') ? true : false
|
|
41
44
|
if ('decodeKeepalive' in data) {
|
|
@@ -46,7 +49,7 @@ export const melissaLorawanPayloadParser = (hexData: string) => {
|
|
|
46
49
|
|
|
47
50
|
// get only keepalive from device response
|
|
48
51
|
if (shouldKeepAlive) {
|
|
49
|
-
let keepaliveData = hexData.slice(-
|
|
52
|
+
let keepaliveData = hexData.slice(-10)
|
|
50
53
|
let dataToPass = byteArrayParser(keepaliveData)
|
|
51
54
|
if (!dataToPass) return
|
|
52
55
|
handleKeepAliveData(dataToPass)
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
Relay16DryCommands,
|
|
20
20
|
HTPirLiteCommands,
|
|
21
21
|
Co2PirLiteCommands,
|
|
22
|
+
MelissaCommands,
|
|
22
23
|
} from '@/encoders'
|
|
23
24
|
|
|
24
25
|
import { CustomError, toCamelCase } from '@/utils'
|
|
@@ -48,6 +49,7 @@ export class CommandBuilder {
|
|
|
48
49
|
relay_16_dry: Relay16DryCommands,
|
|
49
50
|
ht_pir_lite: HTPirLiteCommands,
|
|
50
51
|
co2_pir_lite: Co2PirLiteCommands,
|
|
52
|
+
melissa_lorawan: MelissaCommands,
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { GeneralCommands, BaseCommand } from '@/encoders'
|
|
2
|
+
import { applyMixins, decToHex, CustomError } from '@/utils'
|
|
3
|
+
import { DeviceCommandSchemas, MelissaCommandTypes } from './types'
|
|
4
|
+
import { ZodError, } from 'zod'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class MelissaCommands extends GeneralCommands {
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
static setIrCodeRecording(params: MelissaCommandTypes.SetIrCodeRecordingParams) {
|
|
11
|
+
try {
|
|
12
|
+
DeviceCommandSchemas.MelissaCommandSchemas.setIrCodeRecording.parse(params)
|
|
13
|
+
const { time } = params
|
|
14
|
+
return new BaseCommand('SetIrCodeRecording', 0x09, decToHex(time))
|
|
15
|
+
} catch (e) {
|
|
16
|
+
if (e instanceof ZodError) {
|
|
17
|
+
throw new CustomError({
|
|
18
|
+
message: 'Zod validation error during SetIrCodeRecording execution',
|
|
19
|
+
command: 'SetIrCodeRecording',
|
|
20
|
+
originalError: e,
|
|
21
|
+
})
|
|
22
|
+
} else {
|
|
23
|
+
throw new CustomError({
|
|
24
|
+
message: 'Error during SetIrCodeRecording execution',
|
|
25
|
+
command: 'SetIrCodeRecording',
|
|
26
|
+
originalError: e as Error,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
applyMixins(MelissaCommands, [
|
|
33
|
+
])
|
package/src/encoders/index.ts
CHANGED
|
@@ -1377,6 +1377,22 @@ export namespace Co2PirLiteCommandTypes {
|
|
|
1377
1377
|
>
|
|
1378
1378
|
export type RestartDeviceParams = z.infer<typeof Co2PirLiteCommandSchemas.restartDevice>
|
|
1379
1379
|
}
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
/* --------------------------------------- Melissa --------------------------------------- */
|
|
1384
|
+
const MelissaCommandSchemas = {
|
|
1385
|
+
...GeneralCommandSchemas,
|
|
1386
|
+
setIrCodeRecording: z.object({
|
|
1387
|
+
time: z.number().min(0).max(255),
|
|
1388
|
+
}),
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
export namespace MelissaCommandTypes {
|
|
1392
|
+
export type SetIrCodeRecordingParams = z.infer<
|
|
1393
|
+
typeof MelissaCommandSchemas.setIrCodeRecording
|
|
1394
|
+
>
|
|
1395
|
+
}
|
|
1380
1396
|
/* --------------------------------------- EXPORT ALL SCHEMA GROUPS --------------------------------------- */
|
|
1381
1397
|
export const DeviceCommandSchemas = {
|
|
1382
1398
|
GeneralCommandSchemas,
|
|
@@ -1402,4 +1418,5 @@ export const DeviceCommandSchemas = {
|
|
|
1402
1418
|
Relay16DryCommandSchemas,
|
|
1403
1419
|
HTPirLiteCommandSchemas,
|
|
1404
1420
|
Co2PirLiteCommandSchemas,
|
|
1421
|
+
MelissaCommandSchemas,
|
|
1405
1422
|
}
|
|
@@ -824,23 +824,42 @@ describe('ADS payload decoder', () => {
|
|
|
824
824
|
})
|
|
825
825
|
describe('Melissa payload decoder', () => {
|
|
826
826
|
test('simple keepalive', () => {
|
|
827
|
-
expect(uplinkPayloadParser('
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
})
|
|
827
|
+
expect(uplinkPayloadParser('01027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
|
|
828
|
+
sensorTemperature: 23.8,
|
|
829
|
+
relativeHumidity: 27.34,
|
|
830
|
+
isIrCodeRecordingRequested: false
|
|
831
|
+
})
|
|
833
832
|
|
|
834
833
|
})
|
|
835
834
|
test('keepalive with response of preloaded code', () => {
|
|
836
|
-
expect(uplinkPayloadParser('
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
acVoltage: 164,
|
|
835
|
+
expect(uplinkPayloadParser('06004201027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
|
|
836
|
+
sensorTemperature: 23.8,
|
|
837
|
+
relativeHumidity: 27.34,
|
|
838
|
+
isIrCodeRecordingRequested: false,
|
|
841
839
|
preloadedCode: { codeAddress: 66, codeAddressRaw: '0042' }
|
|
842
840
|
})
|
|
843
841
|
})
|
|
842
|
+
test('keepalive with response of recorded code', () => {
|
|
843
|
+
expect(uplinkPayloadParser('0a0020002001027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
|
|
844
|
+
sensorTemperature: 23.8,
|
|
845
|
+
relativeHumidity: 27.34,
|
|
846
|
+
isIrCodeRecordingRequested: false,
|
|
847
|
+
recordedIrInfo: { recordedIrCodeSize: 32, bytesSent: '0020' }
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
test('keepalive with response of transfered code', () => {
|
|
851
|
+
expect(uplinkPayloadParser('0b2200000063320612062e062e000606000508120004040008080005180008081100050c01027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
|
|
852
|
+
sensorTemperature: 23.8,
|
|
853
|
+
relativeHumidity: 27.34,
|
|
854
|
+
isIrCodeRecordingRequested: false,
|
|
855
|
+
irCodeData: {
|
|
856
|
+
bytesCount: 32,
|
|
857
|
+
address: 0,
|
|
858
|
+
data: "0063320612062e062e000606000508120004040008080005180008081100050c"
|
|
859
|
+
}
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
|
|
844
863
|
})
|
|
845
864
|
describe('CO2PirLite payload decoder', () => {
|
|
846
865
|
test('simple keepalive', () => {
|
|
@@ -816,3 +816,11 @@ describe('HT PIR Lite Commands payload encoder', () => {
|
|
|
816
816
|
expect(commandBuilder.build('RestartDevice')).toStrictEqual(new BaseCommand('RestartDevice', 0xa5))
|
|
817
817
|
})
|
|
818
818
|
})
|
|
819
|
+
describe('Melissa Commands payload encoder', () => {
|
|
820
|
+
const commandBuilder = new CommandBuilder('melissa_lorawan')
|
|
821
|
+
test('Set IR Code Recording ', () => {
|
|
822
|
+
expect(commandBuilder.build('SetIrCodeRecording', { time: 15 })).toStrictEqual(
|
|
823
|
+
new BaseCommand('SetIrCodeRecording', 0x09, '0F'),
|
|
824
|
+
)
|
|
825
|
+
})
|
|
826
|
+
})
|