@homebridge-plugins/homebridge-tuya 3.0.0-beta.1 → 3.0.0-beta.2
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/README.md +5 -1
- package/dist/cloud/api/TuyaOpenAPI.js +10 -0
- package/dist/cloud/api/TuyaOpenAPI.js.map +1 -1
- package/dist/cloud/device/TuyaDeviceManager.js +4 -0
- package/dist/cloud/device/TuyaDeviceManager.js.map +1 -1
- package/dist/local/LocalDeviceManager.js +155 -7
- package/dist/local/LocalDeviceManager.js.map +1 -1
- package/dist/platform.js +139 -2
- package/dist/platform.js.map +1 -1
- package/dist/shared/accessories/BaseAccessory.js +40 -5
- package/dist/shared/accessories/BaseAccessory.js.map +1 -1
- package/package.json +6 -6
- package/test/localDeviceManager.test.ts +59 -0
- package/test/platform.test.ts +795 -0
package/test/platform.test.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { TuyaPlatform } from '../src/platform';
|
|
|
6
6
|
const mockHAP = {
|
|
7
7
|
Service: {},
|
|
8
8
|
Characteristic: {},
|
|
9
|
+
uuid: {
|
|
10
|
+
generate: (name: string) => `${name}-uuid`,
|
|
11
|
+
},
|
|
9
12
|
};
|
|
10
13
|
|
|
11
14
|
const mockAPI = {
|
|
@@ -388,6 +391,181 @@ describe('TuyaPlatform', () => {
|
|
|
388
391
|
expect(bothPlatform.options).toBeDefined();
|
|
389
392
|
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
390
393
|
});
|
|
394
|
+
|
|
395
|
+
test('enriches local config with cloud device details', async () => {
|
|
396
|
+
const bothConfig = {
|
|
397
|
+
platform: 'TuyaPlatform',
|
|
398
|
+
name: 'Tuya',
|
|
399
|
+
mode: 'both',
|
|
400
|
+
options: {
|
|
401
|
+
projectType: '2',
|
|
402
|
+
accessId: 'id',
|
|
403
|
+
accessKey: 'key',
|
|
404
|
+
countryCode: 1,
|
|
405
|
+
username: 'user',
|
|
406
|
+
password: 'pass',
|
|
407
|
+
appSchema: 'tuyaSmart',
|
|
408
|
+
generateWeatherAccessory: false,
|
|
409
|
+
weatherAPI: '',
|
|
410
|
+
},
|
|
411
|
+
local: {
|
|
412
|
+
devices: [],
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
417
|
+
|
|
418
|
+
// Mock cloud device
|
|
419
|
+
const mockCloudDevice = {
|
|
420
|
+
id: 'test-device-123',
|
|
421
|
+
uuid: 'test-device-123',
|
|
422
|
+
name: 'Test Device',
|
|
423
|
+
product_id: 'product-123',
|
|
424
|
+
} as any;
|
|
425
|
+
|
|
426
|
+
// Mock the getDeviceDetails response
|
|
427
|
+
const mockDeviceDetails = {
|
|
428
|
+
success: true,
|
|
429
|
+
result: {
|
|
430
|
+
id: 'test-device-123',
|
|
431
|
+
local_key: 'test-local-key-abcdef',
|
|
432
|
+
ip: '192.168.1.100',
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
bothPlatform.deviceManager = {
|
|
437
|
+
getDeviceDetails: async () => mockDeviceDetails,
|
|
438
|
+
} as any;
|
|
439
|
+
|
|
440
|
+
// Enrich local config
|
|
441
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
442
|
+
|
|
443
|
+
// Verify local config was enriched
|
|
444
|
+
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
445
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
446
|
+
expect(bothPlatform.platformConfig.local.devices.length).toBeGreaterThan(0);
|
|
447
|
+
|
|
448
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
449
|
+
expect(enrichedDevice.tuyaDeviceId).toBe('test-device-123');
|
|
450
|
+
expect(enrichedDevice.tuyaKey).toBe('test-local-key-abcdef');
|
|
451
|
+
expect(enrichedDevice.ip).toBe('192.168.1.100');
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('preserves manual local config when enriching', async () => {
|
|
456
|
+
const bothConfig = {
|
|
457
|
+
platform: 'TuyaPlatform',
|
|
458
|
+
name: 'Tuya',
|
|
459
|
+
mode: 'both',
|
|
460
|
+
options: {
|
|
461
|
+
projectType: '2',
|
|
462
|
+
accessId: 'id',
|
|
463
|
+
accessKey: 'key',
|
|
464
|
+
countryCode: 1,
|
|
465
|
+
username: 'user',
|
|
466
|
+
password: 'pass',
|
|
467
|
+
appSchema: 'tuyaSmart',
|
|
468
|
+
generateWeatherAccessory: false,
|
|
469
|
+
weatherAPI: '',
|
|
470
|
+
},
|
|
471
|
+
local: {
|
|
472
|
+
devices: [
|
|
473
|
+
{
|
|
474
|
+
tuyaDeviceId: 'manual-device-456',
|
|
475
|
+
tuyaKey: 'manual-key-xyz',
|
|
476
|
+
name: 'Manual Device',
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
483
|
+
|
|
484
|
+
// Mock cloud device (different from manual one)
|
|
485
|
+
const mockCloudDevice = {
|
|
486
|
+
id: 'test-device-123',
|
|
487
|
+
uuid: 'test-device-123',
|
|
488
|
+
name: 'Test Device',
|
|
489
|
+
product_id: 'product-123',
|
|
490
|
+
} as any;
|
|
491
|
+
|
|
492
|
+
bothPlatform.deviceManager = {
|
|
493
|
+
getDeviceDetails: async () => ({
|
|
494
|
+
success: true,
|
|
495
|
+
result: {
|
|
496
|
+
id: 'test-device-123',
|
|
497
|
+
local_key: 'cloud-key-123',
|
|
498
|
+
ip: '192.168.1.100',
|
|
499
|
+
},
|
|
500
|
+
}),
|
|
501
|
+
} as any;
|
|
502
|
+
|
|
503
|
+
// Enrich local config
|
|
504
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
505
|
+
|
|
506
|
+
// Verify local config exists and has devices
|
|
507
|
+
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
508
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
509
|
+
// Verify manual device is still there
|
|
510
|
+
const manualDevice = bothPlatform.platformConfig.local.devices.find(
|
|
511
|
+
d => d.tuyaDeviceId === 'manual-device-456'
|
|
512
|
+
);
|
|
513
|
+
expect(manualDevice).toBeDefined();
|
|
514
|
+
expect(manualDevice?.tuyaKey).toBe('manual-key-xyz');
|
|
515
|
+
|
|
516
|
+
// Verify new cloud device was added
|
|
517
|
+
const cloudDevice = bothPlatform.platformConfig.local.devices.find(
|
|
518
|
+
d => d.tuyaDeviceId === 'test-device-123'
|
|
519
|
+
);
|
|
520
|
+
expect(cloudDevice).toBeDefined();
|
|
521
|
+
expect(cloudDevice?.tuyaKey).toBe('cloud-key-123');
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('skips enrichment if cloud device details unavailable', async () => {
|
|
526
|
+
const bothConfig = {
|
|
527
|
+
platform: 'TuyaPlatform',
|
|
528
|
+
name: 'Tuya',
|
|
529
|
+
mode: 'both',
|
|
530
|
+
options: {
|
|
531
|
+
projectType: '2',
|
|
532
|
+
accessId: 'id',
|
|
533
|
+
accessKey: 'key',
|
|
534
|
+
countryCode: 1,
|
|
535
|
+
username: 'user',
|
|
536
|
+
password: 'pass',
|
|
537
|
+
appSchema: 'tuyaSmart',
|
|
538
|
+
generateWeatherAccessory: false,
|
|
539
|
+
weatherAPI: '',
|
|
540
|
+
},
|
|
541
|
+
local: {
|
|
542
|
+
devices: [],
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
547
|
+
|
|
548
|
+
const mockCloudDevice = {
|
|
549
|
+
id: 'test-device-123',
|
|
550
|
+
name: 'Test Device',
|
|
551
|
+
} as any;
|
|
552
|
+
|
|
553
|
+
bothPlatform.deviceManager = {
|
|
554
|
+
getDeviceDetails: async () => ({
|
|
555
|
+
success: false,
|
|
556
|
+
code: 'NOT_FOUND',
|
|
557
|
+
}),
|
|
558
|
+
} as any;
|
|
559
|
+
|
|
560
|
+
// Enrich local config
|
|
561
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
562
|
+
|
|
563
|
+
// Verify nothing was added (since API failed)
|
|
564
|
+
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
565
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
566
|
+
expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
391
569
|
});
|
|
392
570
|
|
|
393
571
|
describe('homebridge lifecycle', () => {
|
|
@@ -512,4 +690,621 @@ describe('TuyaPlatform', () => {
|
|
|
512
690
|
expect(platform2.cachedAccessories.length).toBe(0);
|
|
513
691
|
});
|
|
514
692
|
});
|
|
693
|
+
|
|
694
|
+
describe('API field variations and error handling', () => {
|
|
695
|
+
test('handles localKey instead of local_key in API response', async () => {
|
|
696
|
+
const bothConfig = {
|
|
697
|
+
platform: 'TuyaPlatform',
|
|
698
|
+
name: 'Tuya',
|
|
699
|
+
mode: 'both',
|
|
700
|
+
options: {
|
|
701
|
+
projectType: '2',
|
|
702
|
+
accessId: 'id',
|
|
703
|
+
accessKey: 'key',
|
|
704
|
+
countryCode: 1,
|
|
705
|
+
username: 'user',
|
|
706
|
+
password: 'pass',
|
|
707
|
+
appSchema: 'tuyaSmart',
|
|
708
|
+
generateWeatherAccessory: false,
|
|
709
|
+
weatherAPI: '',
|
|
710
|
+
},
|
|
711
|
+
local: {
|
|
712
|
+
devices: [],
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
717
|
+
|
|
718
|
+
const mockCloudDevice = {
|
|
719
|
+
id: 'test-device-123',
|
|
720
|
+
uuid: 'test-device-123',
|
|
721
|
+
name: 'Test Device',
|
|
722
|
+
product_id: 'product-123',
|
|
723
|
+
} as any;
|
|
724
|
+
|
|
725
|
+
// Response with localKey (camelCase) instead of local_key (snake_case)
|
|
726
|
+
bothPlatform.deviceManager = {
|
|
727
|
+
getDeviceDetails: async () => ({
|
|
728
|
+
success: true,
|
|
729
|
+
result: {
|
|
730
|
+
id: 'test-device-123',
|
|
731
|
+
localKey: 'alternate-format-key', // camelCase variant
|
|
732
|
+
ip: '192.168.1.100',
|
|
733
|
+
},
|
|
734
|
+
}),
|
|
735
|
+
} as any;
|
|
736
|
+
|
|
737
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
738
|
+
|
|
739
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
740
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
741
|
+
expect(enrichedDevice.tuyaKey).toBe('alternate-format-key');
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test('handles address field instead of ip in API response', async () => {
|
|
746
|
+
const bothConfig = {
|
|
747
|
+
platform: 'TuyaPlatform',
|
|
748
|
+
name: 'Tuya',
|
|
749
|
+
mode: 'both',
|
|
750
|
+
options: {
|
|
751
|
+
projectType: '2',
|
|
752
|
+
accessId: 'id',
|
|
753
|
+
accessKey: 'key',
|
|
754
|
+
countryCode: 1,
|
|
755
|
+
username: 'user',
|
|
756
|
+
password: 'pass',
|
|
757
|
+
appSchema: 'tuyaSmart',
|
|
758
|
+
generateWeatherAccessory: false,
|
|
759
|
+
weatherAPI: '',
|
|
760
|
+
},
|
|
761
|
+
local: {
|
|
762
|
+
devices: [],
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
767
|
+
|
|
768
|
+
const mockCloudDevice = {
|
|
769
|
+
id: 'test-device-456',
|
|
770
|
+
name: 'Another Device',
|
|
771
|
+
} as any;
|
|
772
|
+
|
|
773
|
+
bothPlatform.deviceManager = {
|
|
774
|
+
getDeviceDetails: async () => ({
|
|
775
|
+
success: true,
|
|
776
|
+
result: {
|
|
777
|
+
id: 'test-device-456',
|
|
778
|
+
local_key: 'test-key-456',
|
|
779
|
+
address: '10.0.0.50', // Uses 'address' instead of 'ip'
|
|
780
|
+
},
|
|
781
|
+
}),
|
|
782
|
+
} as any;
|
|
783
|
+
|
|
784
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
785
|
+
|
|
786
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
787
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
788
|
+
expect(enrichedDevice.ip).toBe('10.0.0.50');
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test('uses uuid fallback when cloud device id is missing', async () => {
|
|
793
|
+
const bothConfig = {
|
|
794
|
+
platform: 'TuyaPlatform',
|
|
795
|
+
name: 'Tuya',
|
|
796
|
+
mode: 'both',
|
|
797
|
+
options: {
|
|
798
|
+
projectType: '2',
|
|
799
|
+
accessId: 'id',
|
|
800
|
+
accessKey: 'key',
|
|
801
|
+
countryCode: 1,
|
|
802
|
+
username: 'user',
|
|
803
|
+
password: 'pass',
|
|
804
|
+
appSchema: 'tuyaSmart',
|
|
805
|
+
generateWeatherAccessory: false,
|
|
806
|
+
weatherAPI: '',
|
|
807
|
+
},
|
|
808
|
+
local: {
|
|
809
|
+
devices: [],
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
814
|
+
|
|
815
|
+
const mockCloudDevice = {
|
|
816
|
+
uuid: 'fallback-device-123',
|
|
817
|
+
name: 'Fallback Device',
|
|
818
|
+
} as any;
|
|
819
|
+
|
|
820
|
+
bothPlatform.deviceManager = {
|
|
821
|
+
getDeviceDetails: async () => ({
|
|
822
|
+
success: true,
|
|
823
|
+
result: {
|
|
824
|
+
uuid: 'fallback-device-123',
|
|
825
|
+
local_key: 'fallback-key-123',
|
|
826
|
+
ip: '192.168.1.101',
|
|
827
|
+
},
|
|
828
|
+
}),
|
|
829
|
+
} as any;
|
|
830
|
+
|
|
831
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
832
|
+
|
|
833
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
834
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
835
|
+
expect(enrichedDevice.tuyaDeviceId).toBe('fallback-device-123');
|
|
836
|
+
expect(enrichedDevice.tuyaKey).toBe('fallback-key-123');
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test('propagates cloud category into local config', async () => {
|
|
841
|
+
const bothConfig = {
|
|
842
|
+
platform: 'TuyaPlatform',
|
|
843
|
+
name: 'Tuya',
|
|
844
|
+
mode: 'both',
|
|
845
|
+
options: {
|
|
846
|
+
projectType: '2',
|
|
847
|
+
accessId: 'id',
|
|
848
|
+
accessKey: 'key',
|
|
849
|
+
countryCode: 1,
|
|
850
|
+
username: 'user',
|
|
851
|
+
password: 'pass',
|
|
852
|
+
appSchema: 'tuyaSmart',
|
|
853
|
+
generateWeatherAccessory: false,
|
|
854
|
+
weatherAPI: '',
|
|
855
|
+
},
|
|
856
|
+
local: {
|
|
857
|
+
devices: [],
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
862
|
+
|
|
863
|
+
const mockCloudDevice = {
|
|
864
|
+
id: 'category-device-123',
|
|
865
|
+
name: 'Category Device',
|
|
866
|
+
category: 'kg',
|
|
867
|
+
product_id: 'abc123',
|
|
868
|
+
} as any;
|
|
869
|
+
|
|
870
|
+
bothPlatform.deviceManager = {
|
|
871
|
+
getDeviceDetails: async () => ({
|
|
872
|
+
success: true,
|
|
873
|
+
result: {
|
|
874
|
+
id: 'category-device-123',
|
|
875
|
+
local_key: 'category-key-123',
|
|
876
|
+
ip: '192.168.1.102',
|
|
877
|
+
},
|
|
878
|
+
}),
|
|
879
|
+
} as any;
|
|
880
|
+
|
|
881
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
882
|
+
|
|
883
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
884
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
885
|
+
expect(enrichedDevice.category).toBe('kg');
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('handles missing local_key gracefully', async () => {
|
|
890
|
+
const bothConfig = {
|
|
891
|
+
platform: 'TuyaPlatform',
|
|
892
|
+
name: 'Tuya',
|
|
893
|
+
mode: 'both',
|
|
894
|
+
options: {
|
|
895
|
+
projectType: '2',
|
|
896
|
+
accessId: 'id',
|
|
897
|
+
accessKey: 'key',
|
|
898
|
+
countryCode: 1,
|
|
899
|
+
username: 'user',
|
|
900
|
+
password: 'pass',
|
|
901
|
+
appSchema: 'tuyaSmart',
|
|
902
|
+
generateWeatherAccessory: false,
|
|
903
|
+
weatherAPI: '',
|
|
904
|
+
},
|
|
905
|
+
local: {
|
|
906
|
+
devices: [],
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
911
|
+
|
|
912
|
+
const mockCloudDevice = {
|
|
913
|
+
id: 'test-device-no-key',
|
|
914
|
+
name: 'Device Without Local Key',
|
|
915
|
+
} as any;
|
|
916
|
+
|
|
917
|
+
bothPlatform.deviceManager = {
|
|
918
|
+
getDeviceDetails: async () => ({
|
|
919
|
+
success: true,
|
|
920
|
+
result: {
|
|
921
|
+
id: 'test-device-no-key',
|
|
922
|
+
// No local_key or localKey provided
|
|
923
|
+
ip: '192.168.1.200',
|
|
924
|
+
},
|
|
925
|
+
}),
|
|
926
|
+
} as any;
|
|
927
|
+
|
|
928
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
929
|
+
|
|
930
|
+
// Device should not be added to local config without local_key
|
|
931
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
932
|
+
expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test('handles API timeout with grace', async () => {
|
|
937
|
+
const bothConfig = {
|
|
938
|
+
platform: 'TuyaPlatform',
|
|
939
|
+
name: 'Tuya',
|
|
940
|
+
mode: 'both',
|
|
941
|
+
options: {
|
|
942
|
+
projectType: '2',
|
|
943
|
+
accessId: 'id',
|
|
944
|
+
accessKey: 'key',
|
|
945
|
+
countryCode: 1,
|
|
946
|
+
username: 'user',
|
|
947
|
+
password: 'pass',
|
|
948
|
+
appSchema: 'tuyaSmart',
|
|
949
|
+
generateWeatherAccessory: false,
|
|
950
|
+
weatherAPI: '',
|
|
951
|
+
},
|
|
952
|
+
local: {
|
|
953
|
+
devices: [],
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
958
|
+
|
|
959
|
+
const mockCloudDevice = {
|
|
960
|
+
id: 'test-device-timeout',
|
|
961
|
+
name: 'Timeout Device',
|
|
962
|
+
} as any;
|
|
963
|
+
|
|
964
|
+
bothPlatform.deviceManager = {
|
|
965
|
+
getDeviceDetails: async () => {
|
|
966
|
+
throw new Error('Request timeout');
|
|
967
|
+
},
|
|
968
|
+
} as any;
|
|
969
|
+
|
|
970
|
+
// Should not throw, but handle gracefully
|
|
971
|
+
await expect(bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice])).resolves.toBeUndefined();
|
|
972
|
+
|
|
973
|
+
// Local devices should remain empty (timeout treated as error)
|
|
974
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
975
|
+
expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
describe('mixed config scenarios', () => {
|
|
981
|
+
test('updates manual local config with cloud-provided local_key', async () => {
|
|
982
|
+
const bothConfig = {
|
|
983
|
+
platform: 'TuyaPlatform',
|
|
984
|
+
name: 'Tuya',
|
|
985
|
+
mode: 'both',
|
|
986
|
+
options: {
|
|
987
|
+
projectType: '2',
|
|
988
|
+
accessId: 'id',
|
|
989
|
+
accessKey: 'key',
|
|
990
|
+
countryCode: 1,
|
|
991
|
+
username: 'user',
|
|
992
|
+
password: 'pass',
|
|
993
|
+
appSchema: 'tuyaSmart',
|
|
994
|
+
generateWeatherAccessory: false,
|
|
995
|
+
weatherAPI: '',
|
|
996
|
+
},
|
|
997
|
+
local: {
|
|
998
|
+
devices: [
|
|
999
|
+
{
|
|
1000
|
+
tuyaDeviceId: 'manual-device-xyz',
|
|
1001
|
+
// User provided device ID but no key
|
|
1002
|
+
name: 'Manual Device No Key',
|
|
1003
|
+
},
|
|
1004
|
+
],
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1009
|
+
|
|
1010
|
+
const mockCloudDevice = {
|
|
1011
|
+
id: 'manual-device-xyz',
|
|
1012
|
+
name: 'Manual Device No Key',
|
|
1013
|
+
} as any;
|
|
1014
|
+
|
|
1015
|
+
bothPlatform.deviceManager = {
|
|
1016
|
+
getDeviceDetails: async () => ({
|
|
1017
|
+
success: true,
|
|
1018
|
+
result: {
|
|
1019
|
+
id: 'manual-device-xyz',
|
|
1020
|
+
local_key: 'cloud-filled-key',
|
|
1021
|
+
ip: '192.168.1.99',
|
|
1022
|
+
},
|
|
1023
|
+
}),
|
|
1024
|
+
} as any;
|
|
1025
|
+
|
|
1026
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
1027
|
+
|
|
1028
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
1029
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
1030
|
+
expect(enrichedDevice.tuyaDeviceId).toBe('manual-device-xyz');
|
|
1031
|
+
expect(enrichedDevice.tuyaKey).toBe('cloud-filled-key');
|
|
1032
|
+
expect(enrichedDevice.ip).toBe('192.168.1.99');
|
|
1033
|
+
// Original name should be preserved
|
|
1034
|
+
expect(enrichedDevice.name).toBe('Manual Device No Key');
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test('does not overwrite existing manual local_key', async () => {
|
|
1039
|
+
const bothConfig = {
|
|
1040
|
+
platform: 'TuyaPlatform',
|
|
1041
|
+
name: 'Tuya',
|
|
1042
|
+
mode: 'both',
|
|
1043
|
+
options: {
|
|
1044
|
+
projectType: '2',
|
|
1045
|
+
accessId: 'id',
|
|
1046
|
+
accessKey: 'key',
|
|
1047
|
+
countryCode: 1,
|
|
1048
|
+
username: 'user',
|
|
1049
|
+
password: 'pass',
|
|
1050
|
+
appSchema: 'tuyaSmart',
|
|
1051
|
+
generateWeatherAccessory: false,
|
|
1052
|
+
weatherAPI: '',
|
|
1053
|
+
},
|
|
1054
|
+
local: {
|
|
1055
|
+
devices: [
|
|
1056
|
+
{
|
|
1057
|
+
tuyaDeviceId: 'device-with-manual-key',
|
|
1058
|
+
tuyaKey: 'user-provided-key-123', // User explicitly set this
|
|
1059
|
+
name: 'Device With Manual Key',
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1066
|
+
|
|
1067
|
+
const mockCloudDevice = {
|
|
1068
|
+
id: 'device-with-manual-key',
|
|
1069
|
+
name: 'Device With Manual Key',
|
|
1070
|
+
} as any;
|
|
1071
|
+
|
|
1072
|
+
bothPlatform.deviceManager = {
|
|
1073
|
+
getDeviceDetails: async () => ({
|
|
1074
|
+
success: true,
|
|
1075
|
+
result: {
|
|
1076
|
+
id: 'device-with-manual-key',
|
|
1077
|
+
local_key: 'different-cloud-key',
|
|
1078
|
+
ip: '192.168.1.88',
|
|
1079
|
+
},
|
|
1080
|
+
}),
|
|
1081
|
+
} as any;
|
|
1082
|
+
|
|
1083
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
1084
|
+
|
|
1085
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
1086
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
1087
|
+
// Manual key should be skipped (device has tuyaKey already)
|
|
1088
|
+
expect(enrichedDevice.tuyaKey).toBe('user-provided-key-123');
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
describe('multi-device enrichment', () => {
|
|
1094
|
+
test('enriches multiple cloud devices in parallel', async () => {
|
|
1095
|
+
const bothConfig = {
|
|
1096
|
+
platform: 'TuyaPlatform',
|
|
1097
|
+
name: 'Tuya',
|
|
1098
|
+
mode: 'both',
|
|
1099
|
+
options: {
|
|
1100
|
+
projectType: '2',
|
|
1101
|
+
accessId: 'id',
|
|
1102
|
+
accessKey: 'key',
|
|
1103
|
+
countryCode: 1,
|
|
1104
|
+
username: 'user',
|
|
1105
|
+
password: 'pass',
|
|
1106
|
+
appSchema: 'tuyaSmart',
|
|
1107
|
+
generateWeatherAccessory: false,
|
|
1108
|
+
weatherAPI: '',
|
|
1109
|
+
},
|
|
1110
|
+
local: {
|
|
1111
|
+
devices: [],
|
|
1112
|
+
},
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1116
|
+
|
|
1117
|
+
const mockCloudDevices = [
|
|
1118
|
+
{ id: 'device-1', uuid: 'device-1', name: 'Device 1', product_id: 'prod-1' },
|
|
1119
|
+
{ id: 'device-2', uuid: 'device-2', name: 'Device 2', product_id: 'prod-2' },
|
|
1120
|
+
{ id: 'device-3', uuid: 'device-3', name: 'Device 3', product_id: 'prod-3' },
|
|
1121
|
+
] as any;
|
|
1122
|
+
|
|
1123
|
+
const callCount: Record<string, number> = {};
|
|
1124
|
+
bothPlatform.deviceManager = {
|
|
1125
|
+
getDeviceDetails: async (deviceId: string) => {
|
|
1126
|
+
callCount[deviceId] = (callCount[deviceId] || 0) + 1;
|
|
1127
|
+
return {
|
|
1128
|
+
success: true,
|
|
1129
|
+
result: {
|
|
1130
|
+
id: deviceId,
|
|
1131
|
+
local_key: `key-${deviceId}`,
|
|
1132
|
+
ip: `192.168.1.${Math.floor(Math.random() * 254) + 1}`,
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1135
|
+
},
|
|
1136
|
+
} as any;
|
|
1137
|
+
|
|
1138
|
+
await bothPlatform.enrichLocalConfigFromCloud(mockCloudDevices);
|
|
1139
|
+
|
|
1140
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
1141
|
+
expect(bothPlatform.platformConfig.local.devices.length).toBe(3);
|
|
1142
|
+
|
|
1143
|
+
const device1 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-1');
|
|
1144
|
+
const device2 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-2');
|
|
1145
|
+
const device3 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-3');
|
|
1146
|
+
|
|
1147
|
+
expect(device1?.tuyaKey).toBe('key-device-1');
|
|
1148
|
+
expect(device2?.tuyaKey).toBe('key-device-2');
|
|
1149
|
+
expect(device3?.tuyaKey).toBe('key-device-3');
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test('continues enrichment if single device fails', async () => {
|
|
1154
|
+
const bothConfig = {
|
|
1155
|
+
platform: 'TuyaPlatform',
|
|
1156
|
+
name: 'Tuya',
|
|
1157
|
+
mode: 'both',
|
|
1158
|
+
options: {
|
|
1159
|
+
projectType: '2',
|
|
1160
|
+
accessId: 'id',
|
|
1161
|
+
accessKey: 'key',
|
|
1162
|
+
countryCode: 1,
|
|
1163
|
+
username: 'user',
|
|
1164
|
+
password: 'pass',
|
|
1165
|
+
appSchema: 'tuyaSmart',
|
|
1166
|
+
generateWeatherAccessory: false,
|
|
1167
|
+
weatherAPI: '',
|
|
1168
|
+
},
|
|
1169
|
+
local: {
|
|
1170
|
+
devices: [],
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1175
|
+
|
|
1176
|
+
const mockCloudDevices = [
|
|
1177
|
+
{ id: 'device-ok-1', uuid: 'device-ok-1', name: 'OK Device 1' },
|
|
1178
|
+
{ id: 'device-fail', uuid: 'device-fail', name: 'Failing Device' },
|
|
1179
|
+
{ id: 'device-ok-2', uuid: 'device-ok-2', name: 'OK Device 2' },
|
|
1180
|
+
] as any;
|
|
1181
|
+
|
|
1182
|
+
bothPlatform.deviceManager = {
|
|
1183
|
+
getDeviceDetails: async (deviceId: string) => {
|
|
1184
|
+
if (deviceId === 'device-fail') {
|
|
1185
|
+
return { success: false, code: 'ERROR' };
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
success: true,
|
|
1189
|
+
result: {
|
|
1190
|
+
id: deviceId,
|
|
1191
|
+
local_key: `key-${deviceId}`,
|
|
1192
|
+
ip: '192.168.1.100',
|
|
1193
|
+
},
|
|
1194
|
+
};
|
|
1195
|
+
},
|
|
1196
|
+
} as any;
|
|
1197
|
+
|
|
1198
|
+
await bothPlatform.enrichLocalConfigFromCloud(mockCloudDevices);
|
|
1199
|
+
|
|
1200
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
1201
|
+
// Should have 2 devices (one failed, skipped)
|
|
1202
|
+
expect(bothPlatform.platformConfig.local.devices.length).toBe(2);
|
|
1203
|
+
|
|
1204
|
+
const hasOk1 = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-ok-1');
|
|
1205
|
+
const hasOk2 = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-ok-2');
|
|
1206
|
+
const hasFailed = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-fail');
|
|
1207
|
+
|
|
1208
|
+
expect(hasOk1).toBe(true);
|
|
1209
|
+
expect(hasOk2).toBe(true);
|
|
1210
|
+
expect(hasFailed).toBe(false);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
describe('device ID consistency and deduplication', () => {
|
|
1216
|
+
test('uses consistent UUID for same device across cloud and local', () => {
|
|
1217
|
+
const bothConfig = {
|
|
1218
|
+
platform: 'TuyaPlatform',
|
|
1219
|
+
name: 'Tuya',
|
|
1220
|
+
mode: 'both',
|
|
1221
|
+
options: {
|
|
1222
|
+
projectType: '2',
|
|
1223
|
+
accessId: 'id',
|
|
1224
|
+
accessKey: 'key',
|
|
1225
|
+
countryCode: 1,
|
|
1226
|
+
username: 'user',
|
|
1227
|
+
password: 'pass',
|
|
1228
|
+
appSchema: 'tuyaSmart',
|
|
1229
|
+
generateWeatherAccessory: false,
|
|
1230
|
+
weatherAPI: '',
|
|
1231
|
+
},
|
|
1232
|
+
local: {
|
|
1233
|
+
devices: [],
|
|
1234
|
+
},
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1238
|
+
|
|
1239
|
+
// Create mock cloud and local devices with same ID
|
|
1240
|
+
const mockCloudDevice = {
|
|
1241
|
+
id: 'same-device-id',
|
|
1242
|
+
uuid: 'same-device-id',
|
|
1243
|
+
name: 'Test Device (Cloud)',
|
|
1244
|
+
} as any;
|
|
1245
|
+
|
|
1246
|
+
const mockLocalDevice = {
|
|
1247
|
+
id: 'same-device-id',
|
|
1248
|
+
uuid: 'same-device-id',
|
|
1249
|
+
name: 'Test Device (Local)',
|
|
1250
|
+
} as any;
|
|
1251
|
+
|
|
1252
|
+
// Both should map to same UUID
|
|
1253
|
+
const cloudUUID = bothPlatform.api.hap.uuid.generate(mockCloudDevice.id);
|
|
1254
|
+
const localUUID = bothPlatform.api.hap.uuid.generate(mockLocalDevice.id);
|
|
1255
|
+
|
|
1256
|
+
expect(cloudUUID).toBe(localUUID);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
test('enrichment preserves device ID for accessory mapping', async () => {
|
|
1260
|
+
const bothConfig = {
|
|
1261
|
+
platform: 'TuyaPlatform',
|
|
1262
|
+
name: 'Tuya',
|
|
1263
|
+
mode: 'both',
|
|
1264
|
+
options: {
|
|
1265
|
+
projectType: '2',
|
|
1266
|
+
accessId: 'id',
|
|
1267
|
+
accessKey: 'key',
|
|
1268
|
+
countryCode: 1,
|
|
1269
|
+
username: 'user',
|
|
1270
|
+
password: 'pass',
|
|
1271
|
+
appSchema: 'tuyaSmart',
|
|
1272
|
+
generateWeatherAccessory: false,
|
|
1273
|
+
weatherAPI: '',
|
|
1274
|
+
},
|
|
1275
|
+
local: {
|
|
1276
|
+
devices: [],
|
|
1277
|
+
},
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1281
|
+
|
|
1282
|
+
const mockCloudDevice = {
|
|
1283
|
+
id: 'unique-device-id',
|
|
1284
|
+
uuid: 'unique-device-id',
|
|
1285
|
+
name: 'Device',
|
|
1286
|
+
product_id: 'product-123',
|
|
1287
|
+
} as any;
|
|
1288
|
+
|
|
1289
|
+
bothPlatform.deviceManager = {
|
|
1290
|
+
getDeviceDetails: async () => ({
|
|
1291
|
+
success: true,
|
|
1292
|
+
result: {
|
|
1293
|
+
id: 'unique-device-id',
|
|
1294
|
+
local_key: 'test-key',
|
|
1295
|
+
ip: '192.168.1.1',
|
|
1296
|
+
},
|
|
1297
|
+
}),
|
|
1298
|
+
} as any;
|
|
1299
|
+
|
|
1300
|
+
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
1301
|
+
|
|
1302
|
+
if (bothPlatform.platformConfig.local?.devices) {
|
|
1303
|
+
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
1304
|
+
// Device ID should be preserved for accessory mapping
|
|
1305
|
+
expect(enrichedDevice.tuyaDeviceId).toBe('unique-device-id');
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
515
1309
|
});
|
|
1310
|
+
|