@@ -837,6 +837,155 @@ def check_for_duplicate_cluster_ids(clusters) -> None:
837
837
check_for_duplicate_cluster_ids (ep_data .get (OUTPUT_CLUSTERS , []))
838
838
839
839
840
+ @pytest .mark .parametrize (
841
+ "quirk" ,
842
+ [
843
+ quirk_cls
844
+ for quirk_cls in ALL_QUIRK_CLASSES
845
+ if quirk_cls
846
+ not in (
847
+ # -- Tuya devices --
848
+ # remove duplicated OnOff from input cluster (Tuya remotes):
849
+ zhaquirks .tuya .ts004f .TuyaSmartRemote004F ,
850
+ zhaquirks .tuya .ts004f .TuyaSmartRemote004FROK ,
851
+ zhaquirks .tuya .ts004f .TuyaSmartRemote004FDMS ,
852
+ zhaquirks .tuya .ts004f .TuyaSmartRemote004FSK ,
853
+ zhaquirks .tuya .ts004f .TuyaSmartRemote004FSK_v2 ,
854
+ # swap OnOff from input to output cluster (Tuya remotes):
855
+ zhaquirks .tuya .ts0041 .TuyaSmartRemote0041TOPlusA ,
856
+ zhaquirks .tuya .ts0042 .TuyaSmartRemote0042TOPlusA ,
857
+ zhaquirks .tuya .ts0043 .TuyaSmartRemote0043TOPlusB ,
858
+ zhaquirks .tuya .ts0044 .TuyaSmartRemote0044TOPlusB ,
859
+ zhaquirks .tuya .ts0046 .TuyaSmartRemote0046 ,
860
+ # swap TuyaZBExternalSwitchTypeCluster input to output cluster (Tuya plug):
861
+ zhaquirks .tuya .ts011f_plug .Plug_v6 ,
862
+ #
863
+ # -- Xiaomi/Aqara devices --
864
+ # swap OnOff from input to output cluster (binary sensor):
865
+ zhaquirks .xiaomi .aqara .magnet_aq2 .MagnetAQ2 ,
866
+ # swap OnOff from input to output cluster (Aqara remotes):
867
+ zhaquirks .xiaomi .aqara .sensor_switch_aq3 .SwitchAQ3 ,
868
+ zhaquirks .xiaomi .aqara .switch_aq2 .SwitchAQ2 ,
869
+ # remove MultistateInput output cluster (Xiaomi cube):
870
+ zhaquirks .xiaomi .aqara .cube .Cube ,
871
+ zhaquirks .xiaomi .aqara .cube_aqgl01 .CubeAQGL01 ,
872
+ # also add OTA input cluster (Aqara cube):
873
+ zhaquirks .xiaomi .aqara .cube_aqgl01 .CubeCAGL02 ,
874
+ # remove custom Xiaomi output cluster (E1 curtain driver):
875
+ zhaquirks .xiaomi .aqara .driver_curtain_e1 .DriverE1 ,
876
+ # remove random AnalogInput input cluster (Aqara remote + temp sensor):
877
+ zhaquirks .xiaomi .aqara .remote_b186acn01 .RemoteB186ACN01 ,
878
+ zhaquirks .xiaomi .aqara .remote_b286acn01 .RemoteB286ACN01 ,
879
+ zhaquirks .xiaomi .mija .sensor_ht .Weather ,
880
+ # remove Time input cluster (Aqara switch):
881
+ zhaquirks .xiaomi .aqara .switch_t1 .SwitchT1Alt2 ,
882
+ zhaquirks .xiaomi .aqara .switch_t1 .SwitchT1 ,
883
+ # remove OnOff output cluster (Aqara switch):
884
+ zhaquirks .xiaomi .aqara .switch_t1 .SwitchT1Alt3 ,
885
+ # remove OTA input cluster (Aqara remote + motion sensor):
886
+ zhaquirks .xiaomi .mija .motion .Motion ,
887
+ zhaquirks .xiaomi .mija .sensor_switch .MijaButton ,
888
+ # remove a bunch of incorrect output clusters (LUMI/Keen temp sensor):
889
+ zhaquirks .keenhome .weather .TemperatureHumidtyPressureSensor ,
890
+ # this just exposed all ZCL clusters, remove a lot (Aqara light):
891
+ zhaquirks .xiaomi .aqara .light_aqcn2 .LightAqcn02 ,
892
+ # DoorLock cluster that's actually a MultistateInput cluster
893
+ # removed as output cluster (Aqara vibration sensor):
894
+ zhaquirks .xiaomi .aqara .vibration_aq1 .VibrationAQ1 ,
895
+ #
896
+ # -- IKEA devices --
897
+ # swap PM25 cluster from output to input cluster (IKEA Starkvind):
898
+ zhaquirks .ikea .starkvind .IkeaSTARKVIND ,
899
+ zhaquirks .ikea .starkvind .IkeaSTARKVIND_v2 ,
900
+ # removes Group input cluster (IKEA remote):
901
+ zhaquirks .ikea .twobtnremote .IkeaRodretRemote2BtnNew ,
902
+ # remove WindowCovering input cluster (IKEA remote):
903
+ zhaquirks .ikea .twobtnremote .IkeaTradfriRemote2BtnZLL ,
904
+ #
905
+ # -- other devices --
906
+ # adds DoorLock cluster to output clusters (Yale door locks):
907
+ zhaquirks .yale .realliving .YRD210PBDB220TSLL ,
908
+ zhaquirks .yale .realliving .YRD220240TSDB ,
909
+ # remove LevelControl input cluster (Adurolight remote):
910
+ zhaquirks .aduro .adurolightncc .AdurolightNCC ,
911
+ # add a bunch of output clusters (Zhongxing motion sensor):
912
+ zhaquirks .zhongxing .motion .SN10ZW ,
913
+ # remove Tuya clusters from input and output clusters (ZLinky):
914
+ zhaquirks .lixee .zlinky .ZLinkyTICFWV14 ,
915
+ zhaquirks .lixee .zlinky .ZLinkyTICFWV15 ,
916
+ )
917
+ ],
918
+ )
919
+ def test_suspicious_cluster_moves (quirk : CustomDevice ) -> None :
920
+ """Verify that no quirks do suspicious moves or copy/pastes of clusters."""
921
+ for ep_id , ep_data in quirk .replacement [ENDPOINTS ].items ():
922
+ # Originals
923
+ orig_in_clusters = set (
924
+ quirk .signature .get (ENDPOINTS , {}).get (ep_id , {}).get (INPUT_CLUSTERS , [])
925
+ )
926
+ orig_out_clusters = set (
927
+ quirk .signature .get (ENDPOINTS , {}).get (ep_id , {}).get (OUTPUT_CLUSTERS , [])
928
+ )
929
+
930
+ # New
931
+ new_in_clusters = {
932
+ cluster if isinstance (cluster , int ) else cluster .cluster_id
933
+ for cluster in ep_data .get (INPUT_CLUSTERS , [])
934
+ }
935
+ new_out_clusters = {
936
+ cluster if isinstance (cluster , int ) else cluster .cluster_id
937
+ for cluster in ep_data .get (OUTPUT_CLUSTERS , [])
938
+ }
939
+
940
+ added_in_clusters = set (new_in_clusters ) - set (orig_in_clusters )
941
+ added_out_clusters = set (new_out_clusters ) - set (orig_out_clusters )
942
+
943
+ removed_in_clusters = set (orig_in_clusters ) - set (new_in_clusters )
944
+ removed_out_clusters = set (orig_out_clusters ) - set (new_out_clusters )
945
+
946
+ # Moved clusters
947
+ in_clusters_moved_to_out = added_out_clusters & removed_in_clusters
948
+ out_clusters_moved_to_in = added_in_clusters & removed_out_clusters
949
+
950
+ if in_clusters_moved_to_out :
951
+ pytest .fail (
952
+ f"Quirk { quirk !r} moved input to output cluster on EP { ep_id } : { in_clusters_moved_to_out !r} "
953
+ )
954
+
955
+ if out_clusters_moved_to_in :
956
+ pytest .fail (
957
+ f"Quirk { quirk !r} moved output to input cluster on EP { ep_id } : { out_clusters_moved_to_in !r} "
958
+ )
959
+
960
+ # Mirrored clusters
961
+ out_mirrored_to_in = added_in_clusters & orig_out_clusters
962
+ in_mirrored_to_out = added_out_clusters & orig_in_clusters
963
+
964
+ if out_mirrored_to_in :
965
+ pytest .fail (
966
+ f"Quirk { quirk !r} mirrored output to input cluster on EP { ep_id } : { out_mirrored_to_in !r} "
967
+ )
968
+
969
+ if in_mirrored_to_out :
970
+ pytest .fail (
971
+ f"Quirk { quirk !r} mirrored input to output cluster on EP { ep_id } : { in_mirrored_to_out !r} "
972
+ )
973
+
974
+ # Removed clusters where one exists of the opposite type
975
+ removed_duplicate_in_clusters = removed_in_clusters & orig_out_clusters
976
+ removed_duplicate_out_clusters = removed_out_clusters & orig_in_clusters
977
+
978
+ if removed_duplicate_in_clusters :
979
+ pytest .fail (
980
+ f"Quirk { quirk !r} removed input cluster that has output cluster with same ID on EP { ep_id } : { removed_duplicate_in_clusters !r} "
981
+ )
982
+
983
+ if removed_duplicate_out_clusters :
984
+ pytest .fail (
985
+ f"Quirk { quirk !r} removed output cluster that has input cluster with same ID on EP { ep_id } : { removed_duplicate_out_clusters !r} "
986
+ )
987
+
988
+
840
989
async def test_local_data_cluster (device_mock ) -> None :
841
990
"""Ensure reading attributes from a LocalDataCluster works as expected."""
842
991
registry = DeviceRegistry ()
0 commit comments