@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.
@@ -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
+