Bladeren bron

tests: update for HA 2026.7 and later

In HA 2025.7, pytest was updates, and around then or after some
changes were made to event loop that affect unit tests.

One or the combination of both these changes block the
unittest.IsolatedAsyncIoTestCase class from being used as a base for
our tests.

Adapt the top level tests which are affected by this change.

Disable device specific tests for now, as there is a lot of work in
adapting them to work without IsolatedAsyncIoTestCase.
Jason Rumney 2 dagen geleden
bovenliggende
commit
f211e6aae1
91 gewijzigde bestanden met toevoegingen van 1831 en 2000 verwijderingen
  1. 0 1
      pyproject.toml
  2. 1 0
      requirements-dev.txt
  3. 0 2
      tests/devices/test_ailrinni_fplock.py
  4. 0 2
      tests/devices/test_anko_fan.py
  5. 0 2
      tests/devices/test_arlec_fan.py
  6. 0 2
      tests/devices/test_arlec_fan_light.py
  7. 0 2
      tests/devices/test_aspen_adv200_fan.py
  8. 0 2
      tests/devices/test_avatto_blinds.py
  9. 0 2
      tests/devices/test_avatto_curtain_switch.py
  10. 0 2
      tests/devices/test_bcom_intercom_camera.py
  11. 0 2
      tests/devices/test_beca_bac002_thermostat.py
  12. 0 4
      tests/devices/test_beca_bhp6000_thermostat.py
  13. 0 2
      tests/devices/test_beok_tr9b_thermostat.py
  14. 0 2
      tests/devices/test_betterlife_bl1500_heater.py
  15. 0 2
      tests/devices/test_ble_smartplant.py
  16. 0 2
      tests/devices/test_ble_water_valve.py
  17. 0 2
      tests/devices/test_blitzwolf_bsh2_humidifier.py
  18. 0 2
      tests/devices/test_digoo_dgsp01_dual_nightlight_switch.py
  19. 0 2
      tests/devices/test_digoo_dgsp202.py
  20. 0 2
      tests/devices/test_duux_blizzard.py
  21. 0 2
      tests/devices/test_eanons_humidifier.py
  22. 0 2
      tests/devices/test_eberg_cooly_c35hd.py
  23. 0 2
      tests/devices/test_eberg_qubo_q40hd_heatpump.py
  24. 0 2
      tests/devices/test_elko_cfmtb_thermostat.py
  25. 0 2
      tests/devices/test_energy_monitoring_powerstrip.py
  26. 0 2
      tests/devices/test_essentials_purifier.py
  27. 0 2
      tests/devices/test_eurom_600_heater.py
  28. 0 2
      tests/devices/test_eurom_600v2_heater.py
  29. 0 2
      tests/devices/test_eurom_601_heater.py
  30. 0 2
      tests/devices/test_garage_door_opener.py
  31. 0 2
      tests/devices/test_goldair_fan.py
  32. 0 2
      tests/devices/test_goldair_geco_heater.py
  33. 0 2
      tests/devices/test_goldair_gpph_heater.py
  34. 0 2
      tests/devices/test_goldair_portable_airconditioner.py
  35. 0 2
      tests/devices/test_grid_connect_double_power_point.py
  36. 0 2
      tests/devices/test_gx_aroma_diffuser.py
  37. 0 2
      tests/devices/test_himox_h05_purifier.py
  38. 0 2
      tests/devices/test_himox_h06_purifier.py
  39. 0 2
      tests/devices/test_hydrotherm_dynamicx8.py
  40. 0 2
      tests/devices/test_immax_neo_light_vento.py
  41. 0 2
      tests/devices/test_inkbird_itc308_thermostat.py
  42. 0 2
      tests/devices/test_inkbird_sousvide.py
  43. 0 2
      tests/devices/test_ir_remote_sensors.py
  44. 0 2
      tests/devices/test_kogan_garage_door_opener.py
  45. 0 2
      tests/devices/test_kogan_glass_1_7l_kettle.py
  46. 0 2
      tests/devices/test_kogan_kawfpac09ya_airconditioner.py
  47. 0 2
      tests/devices/test_kyvol_e30_vacuum.py
  48. 0 2
      tests/devices/test_ledvance_light.py
  49. 0 2
      tests/devices/test_lefant_m213_vacuum.py
  50. 0 2
      tests/devices/test_lexy_f501_fan.py
  51. 0 2
      tests/devices/test_logicom_powerstrip.py
  52. 0 2
      tests/devices/test_m027_curtain.py
  53. 0 2
      tests/devices/test_moebot.py
  54. 0 2
      tests/devices/test_moes_bht002_thermostat.py
  55. 0 2
      tests/devices/test_moes_rgb_socket.py
  56. 0 2
      tests/devices/test_motion_sensor_light.py
  57. 0 2
      tests/devices/test_mustool_mt15mt29_airbox.py
  58. 0 2
      tests/devices/test_nashone_mts700wb_thermostat.py
  59. 0 2
      tests/devices/test_nedis_htpl20f_heater.py
  60. 0 2
      tests/devices/test_orion_outdoor_siren.py
  61. 0 2
      tests/devices/test_orion_smartlock.py
  62. 0 2
      tests/devices/test_parkside_plgs2012a1_smart_charger.py
  63. 0 2
      tests/devices/test_poiema_one_purifier.py
  64. 0 2
      tests/devices/test_poolex_qline_heatpump.py
  65. 0 2
      tests/devices/test_qoto_03_sprinkler.py
  66. 0 2
      tests/devices/test_qs_c01_curtain.py
  67. 0 2
      tests/devices/test_renpho_rp_ap001s.py
  68. 0 2
      tests/devices/test_rgbcw_lightbulb.py
  69. 0 2
      tests/devices/test_sd123_hpr01_presence.py
  70. 0 2
      tests/devices/test_simple_blinds.py
  71. 0 2
      tests/devices/test_simple_switch_with_timer.py
  72. 0 2
      tests/devices/test_simple_switch_with_timerv2.py
  73. 0 2
      tests/devices/test_smartplug_encoded.py
  74. 0 2
      tests/devices/test_smartplugv2.py
  75. 0 2
      tests/devices/test_smartplugv2_energy.py
  76. 0 2
      tests/devices/test_starlight_heatpump.py
  77. 0 2
      tests/devices/test_stirling_fs140dc_fan.py
  78. 0 2
      tests/devices/test_thermex_if50v.py
  79. 0 2
      tests/devices/test_tmwf02_fan.py
  80. 0 2
      tests/devices/test_tompd63lw_breaker.py
  81. 0 2
      tests/devices/test_treatlife_ds02f.py
  82. 0 2
      tests/devices/test_vork_vk6067aw_purifier.py
  83. 0 2
      tests/devices/test_weau_pool_heatpumpv2.py
  84. 0 2
      tests/devices/test_wetair_wawh1210lw_humidifier.py
  85. 0 2
      tests/devices/test_wetair_wch750_heater.py
  86. 0 2
      tests/devices/test_woox_r4028_powerstrip.py
  87. 0 2
      tests/devices/test_zemismart_am25_blind.py
  88. 0 2
      tests/devices/test_zx_g30_alarm.py
  89. 724 715
      tests/test_config_flow.py
  90. 637 618
      tests/test_device.py
  91. 469 492
      tests/test_device_config.py

+ 0 - 1
pyproject.toml

@@ -3,4 +3,3 @@ target-version = ["py313"]
 
 
 [tool.pytest.ini_options]
 [tool.pytest.ini_options]
 asyncio_mode = "auto"
 asyncio_mode = "auto"
-asyncio_default_fixture_loop_scope = "function"

+ 1 - 0
requirements-dev.txt

@@ -4,6 +4,7 @@ pytest-homeassistant-custom-component==0.13.316
 pytest
 pytest
 pytest-asyncio
 pytest-asyncio
 pytest-cov
 pytest-cov
+pytest-mock
 ruff
 ruff
 tinytuya~=1.17.6
 tinytuya~=1.17.6
 tuya-device-sharing-sdk~=0.2.1
 tuya-device-sharing-sdk~=0.2.1

+ 0 - 2
tests/devices/test_ailrinni_fplock.py

@@ -24,8 +24,6 @@ UNLOCK_OFFLINE_DP = "67"
 
 
 
 
 class TestAilrinniFingerprintLock(TuyaDeviceTestCase):
 class TestAilrinniFingerprintLock(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "ailrinni_fingerprint_lock.yaml", AILRINNI_FINGERPRINTLOCK_PAYLOAD
             "ailrinni_fingerprint_lock.yaml", AILRINNI_FINGERPRINTLOCK_PAYLOAD

+ 0 - 2
tests/devices/test_anko_fan.py

@@ -16,8 +16,6 @@ TIMER_DPS = "6"
 
 
 
 
 class TestAnkoFan(SwitchableTests, BasicNumberTests, TuyaDeviceTestCase):
 class TestAnkoFan(SwitchableTests, BasicNumberTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("anko_fan.yaml", ANKO_FAN_PAYLOAD)
         self.setUpForConfig("anko_fan.yaml", ANKO_FAN_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_arlec_fan.py

@@ -18,8 +18,6 @@ TIMER_DPS = "103"
 
 
 
 
 class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
 class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("arlec_fan.yaml", ARLEC_FAN_PAYLOAD)
         self.setUpForConfig("arlec_fan.yaml", ARLEC_FAN_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_arlec_fan_light.py

@@ -22,8 +22,6 @@ TIMER_DPS = "103"
 
 
 
 
 class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
 class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("arlec_fan_light.yaml", ARLEC_FAN_LIGHT_PAYLOAD)
         self.setUpForConfig("arlec_fan_light.yaml", ARLEC_FAN_LIGHT_PAYLOAD)
         self.subject = self.entities.get("fan")
         self.subject = self.entities.get("fan")

+ 0 - 2
tests/devices/test_aspen_adv200_fan.py

@@ -26,8 +26,6 @@ LIGHT_DPS = "102"
 class TestAspenASP200Fan(
 class TestAspenASP200Fan(
     DimmableLightTests, SwitchableTests, TargetTemperatureTests, TuyaDeviceTestCase
     DimmableLightTests, SwitchableTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("aspen_asp200_fan.yaml", ASPEN_ASP200_FAN_PAYLOAD)
         self.setUpForConfig("aspen_asp200_fan.yaml", ASPEN_ASP200_FAN_PAYLOAD)
         self.subject = self.entities.get("fan")
         self.subject = self.entities.get("fan")

+ 0 - 2
tests/devices/test_avatto_blinds.py

@@ -21,8 +21,6 @@ TRAVELTIME_DP = "11"
 
 
 
 
 class TestAvattoBlinds(MultiSensorTests, BasicSelectTests, TuyaDeviceTestCase):
 class TestAvattoBlinds(MultiSensorTests, BasicSelectTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("avatto_roller_blinds.yaml", AVATTO_BLINDS_PAYLOAD)
         self.setUpForConfig("avatto_roller_blinds.yaml", AVATTO_BLINDS_PAYLOAD)
         self.subject = self.entities["cover_blind"]
         self.subject = self.entities["cover_blind"]

+ 0 - 2
tests/devices/test_avatto_curtain_switch.py

@@ -12,8 +12,6 @@ BACKLIGHT_DP = "101"
 
 
 
 
 class TestAvattoCurtainSwitch(BasicLightTests, TuyaDeviceTestCase):
 class TestAvattoCurtainSwitch(BasicLightTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "avatto_curtain_switch.yaml",
             "avatto_curtain_switch.yaml",

+ 0 - 2
tests/devices/test_bcom_intercom_camera.py

@@ -22,8 +22,6 @@ LOCK_DPS = "232"
 
 
 
 
 class TestBcomIntercomCamera(TuyaDeviceTestCase):
 class TestBcomIntercomCamera(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("bcom_intercom_camera.yaml", BCOM_CAMERA_PAYLOAD)
         self.setUpForConfig("bcom_intercom_camera.yaml", BCOM_CAMERA_PAYLOAD)
         self.subject = self.entities.get("camera")
         self.subject = self.entities.get("camera")

+ 0 - 2
tests/devices/test_beca_bac002_thermostat.py

@@ -33,8 +33,6 @@ class TestBecaBAC002Thermostat(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("beca_bac002_thermostat_c.yaml", BECA_BAC002_PAYLOAD)
         self.setUpForConfig("beca_bac002_thermostat_c.yaml", BECA_BAC002_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 4
tests/devices/test_beca_bhp6000_thermostat.py

@@ -23,8 +23,6 @@ class TestBecaBHP6000Thermostat(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "beca_bhp6000_thermostat_f.yaml",
             "beca_bhp6000_thermostat_f.yaml",
@@ -144,8 +142,6 @@ class TestBecaBHP6000Thermostat(
 
 
 
 
 class TestBecaBHP6000ThermostatC(TuyaDeviceTestCase):
 class TestBecaBHP6000ThermostatC(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "beca_bhp6000_thermostat_c.yaml",
             "beca_bhp6000_thermostat_c.yaml",

+ 0 - 2
tests/devices/test_beok_tr9b_thermostat.py

@@ -37,8 +37,6 @@ class TestBeokTR9BThermostat(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "beok_tr9b_thermostat.yaml",
             "beok_tr9b_thermostat.yaml",

+ 0 - 2
tests/devices/test_betterlife_bl1500_heater.py

@@ -25,8 +25,6 @@ class TestBetterlifeBL1500Heater(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("betterlife_bl1500_heater.yaml", BETTERLIFE_BL1500_PAYLOAD)
         self.setUpForConfig("betterlife_bl1500_heater.yaml", BETTERLIFE_BL1500_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_ble_smartplant.py

@@ -18,8 +18,6 @@ BATTERY_DP = "15"
 
 
 
 
 class TestBleSmartPlant(TuyaDeviceTestCase):
 class TestBleSmartPlant(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "ble_smart_plant_moisture.yaml",
             "ble_smart_plant_moisture.yaml",

+ 0 - 2
tests/devices/test_ble_water_valve.py

@@ -21,8 +21,6 @@ IRRIGSCHED_DP = "17"
 
 
 
 
 class TestBLEValve(TuyaDeviceTestCase):
 class TestBLEValve(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("ble_water_valve.yaml", BLE_WATERVALVE_PAYLOAD)
         self.setUpForConfig("ble_water_valve.yaml", BLE_WATERVALVE_PAYLOAD)
         self.subject = self.entities["valve_water"]
         self.subject = self.entities["valve_water"]

+ 0 - 2
tests/devices/test_blitzwolf_bsh2_humidifier.py

@@ -12,8 +12,6 @@ TIMER_DP = "19"
 
 
 
 
 class TestBlitzwolfSH2Humidifier(MultiSelectTests, TuyaDeviceTestCase):
 class TestBlitzwolfSH2Humidifier(MultiSelectTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "blitzwolf_bwsh2_humidifier.yaml",
             "blitzwolf_bwsh2_humidifier.yaml",

+ 0 - 2
tests/devices/test_digoo_dgsp01_dual_nightlight_switch.py

@@ -25,8 +25,6 @@ UNKNOWN36_DPS = "36"
 
 
 
 
 class TestDigooNightlightSwitch(BasicSwitchTests, TuyaDeviceTestCase):
 class TestDigooNightlightSwitch(BasicSwitchTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "digoo_dgsp01_dual_nightlight_switch.yaml",
             "digoo_dgsp01_dual_nightlight_switch.yaml",

+ 0 - 2
tests/devices/test_digoo_dgsp202.py

@@ -27,8 +27,6 @@ class TestDigooDGSP202Switch(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("digoo_dgsp202.yaml", DIGOO_DGSP202_SOCKET_PAYLOAD)
         self.setUpForConfig("digoo_dgsp202.yaml", DIGOO_DGSP202_SOCKET_PAYLOAD)
         self.setUpMultiSwitch(
         self.setUpMultiSwitch(

+ 0 - 2
tests/devices/test_duux_blizzard.py

@@ -23,8 +23,6 @@ COUNTDOWN_DP = "15"
 
 
 
 
 class TestDuuxBlizzard(TargetTemperatureTests, TuyaDeviceTestCase):
 class TestDuuxBlizzard(TargetTemperatureTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "duux_blizzard_portable_aircon.yaml",
             "duux_blizzard_portable_aircon.yaml",

+ 0 - 2
tests/devices/test_eanons_humidifier.py

@@ -30,8 +30,6 @@ class TestEanonsHumidifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("eanons_humidifier.yaml", EANONS_HUMIDIFIER_PAYLOAD)
         self.setUpForConfig("eanons_humidifier.yaml", EANONS_HUMIDIFIER_PAYLOAD)
         self.subject = self.entities.get("humidifier_humidifier")
         self.subject = self.entities.get("humidifier_humidifier")

+ 0 - 2
tests/devices/test_eberg_cooly_c35hd.py

@@ -30,8 +30,6 @@ UNKNOWN19_DPS = "19"
 class TestEbergCoolyC35HDHeatpump(
 class TestEbergCoolyC35HDHeatpump(
     BasicSelectTests, TargetTemperatureTests, TuyaDeviceTestCase
     BasicSelectTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "eberg_cooly_c35hd.yaml",
             "eberg_cooly_c35hd.yaml",

+ 0 - 2
tests/devices/test_eberg_qubo_q40hd_heatpump.py

@@ -31,8 +31,6 @@ HVACACTION_DPS = "101"
 class TestEbergQuboQ40HDHeatpump(
 class TestEbergQuboQ40HDHeatpump(
     BasicNumberTests, TargetTemperatureTests, TuyaDeviceTestCase
     BasicNumberTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "eberg_qubo_q40hd_heatpump.yaml",
             "eberg_qubo_q40hd_heatpump.yaml",

+ 0 - 2
tests/devices/test_elko_cfmtb_thermostat.py

@@ -8,8 +8,6 @@ OVERRIDE_END_DPS = "108"
 
 
 
 
 class TestElkoCFMTBThermostat(TuyaDeviceTestCase):
 class TestElkoCFMTBThermostat(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "elko_cfmtb_thermostat.yaml",
             "elko_cfmtb_thermostat.yaml",

+ 0 - 2
tests/devices/test_energy_monitoring_powerstrip.py

@@ -32,8 +32,6 @@ class TestEnergyMonitoringPowerstrip(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "energy_monitoring_powerstrip.yaml", ENERGY_POWERSTRIP_PAYLOAD
             "energy_monitoring_powerstrip.yaml", ENERGY_POWERSTRIP_PAYLOAD

+ 0 - 2
tests/devices/test_essentials_purifier.py

@@ -36,8 +36,6 @@ class TestEssentialsPurifier(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("essentials_purifier.yaml", ESSENTIALS_PURIFIER_PAYLOAD)
         self.setUpForConfig("essentials_purifier.yaml", ESSENTIALS_PURIFIER_PAYLOAD)
         self.setUpBasicButton(
         self.setUpBasicButton(

+ 0 - 2
tests/devices/test_eurom_600_heater.py

@@ -17,8 +17,6 @@ ERROR_DPS = "6"
 class TestEurom600Heater(
 class TestEurom600Heater(
     BasicBinarySensorTests, TargetTemperatureTests, TuyaDeviceTestCase
     BasicBinarySensorTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("eurom_600_heater.yaml", EUROM_600_HEATER_PAYLOAD)
         self.setUpForConfig("eurom_600_heater.yaml", EUROM_600_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_eurom_600v2_heater.py

@@ -17,8 +17,6 @@ ERROR_DPS = "7"
 class TestEurom600v2Heater(
 class TestEurom600v2Heater(
     BasicBinarySensorTests, TargetTemperatureTests, TuyaDeviceTestCase
     BasicBinarySensorTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("eurom_600_heater_v2.yaml", EUROM_600v2_HEATER_PAYLOAD)
         self.setUpForConfig("eurom_600_heater_v2.yaml", EUROM_600v2_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_eurom_601_heater.py

@@ -23,8 +23,6 @@ ERROR_DPS = "13"
 class TestEurom601Heater(
 class TestEurom601Heater(
     BasicBinarySensorTests, TargetTemperatureTests, TuyaDeviceTestCase
     BasicBinarySensorTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("eurom_601_heater.yaml", EUROM_601_HEATER_PAYLOAD)
         self.setUpForConfig("eurom_601_heater.yaml", EUROM_601_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_garage_door_opener.py

@@ -11,8 +11,6 @@ OPEN_DPS = "101"
 
 
 
 
 class TestSimpleGarageOpener(TuyaDeviceTestCase):
 class TestSimpleGarageOpener(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("garage_door_opener.yaml", SIMPLE_GARAGE_DOOR_PAYLOAD)
         self.setUpForConfig("garage_door_opener.yaml", SIMPLE_GARAGE_DOOR_PAYLOAD)
         self.subject = self.entities["cover_garage"]
         self.subject = self.entities["cover_garage"]

+ 0 - 2
tests/devices/test_goldair_fan.py

@@ -15,8 +15,6 @@ LIGHT_DPS = "101"
 
 
 
 
 class TestGoldairFan(BasicLightTests, SwitchableTests, TuyaDeviceTestCase):
 class TestGoldairFan(BasicLightTests, SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("goldair_fan.yaml", FAN_PAYLOAD)
         self.setUpForConfig("goldair_fan.yaml", FAN_PAYLOAD)
         self.subject = self.entities.get("fan")
         self.subject = self.entities.get("fan")

+ 0 - 2
tests/devices/test_goldair_geco_heater.py

@@ -26,8 +26,6 @@ class TestGoldairGECOHeater(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("goldair_geco_heater.yaml", GECO_HEATER_PAYLOAD)
         self.setUpForConfig("goldair_geco_heater.yaml", GECO_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_goldair_gpph_heater.py

@@ -37,8 +37,6 @@ class TestGoldairHeater(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("goldair_gpph_heater.yaml", GPPH_HEATER_PAYLOAD)
         self.setUpForConfig("goldair_gpph_heater.yaml", GPPH_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_goldair_portable_airconditioner.py

@@ -26,8 +26,6 @@ SWINGH_DP = "110"
 
 
 
 
 class TestGoldairPortableAir(TargetTemperatureTests, TuyaDeviceTestCase):
 class TestGoldairPortableAir(TargetTemperatureTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "goldair_portable_airconditioner.yaml",
             "goldair_portable_airconditioner.yaml",

+ 0 - 2
tests/devices/test_grid_connect_double_power_point.py

@@ -41,8 +41,6 @@ class TestGridConnectDoubleSwitch(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "grid_connect_usb_double_power_point.yaml",
             "grid_connect_usb_double_power_point.yaml",

+ 0 - 2
tests/devices/test_gx_aroma_diffuser.py

@@ -15,8 +15,6 @@ ERROR_DP = "9"
 
 
 
 
 class TestAromaDiffuser(TuyaDeviceTestCase):
 class TestAromaDiffuser(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("yym_805SW_aroma_nightlight.yaml", GX_AROMA_PAYLOAD)
         self.setUpForConfig("yym_805SW_aroma_nightlight.yaml", GX_AROMA_PAYLOAD)
         self.subject = self.entities["fan_aroma_diffuser"]
         self.subject = self.entities["fan_aroma_diffuser"]

+ 0 - 2
tests/devices/test_himox_h05_purifier.py

@@ -29,8 +29,6 @@ class TestHimoxH05Purifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("himox_h05_purifier.yaml", HIMOX_H05_PURIFIER_PAYLOAD)
         self.setUpForConfig("himox_h05_purifier.yaml", HIMOX_H05_PURIFIER_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_himox_h06_purifier.py

@@ -30,8 +30,6 @@ class TestHimoxH06Purifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("himox_h06_purifier.yaml", HIMOX_H06_PURIFIER_PAYLOAD)
         self.setUpForConfig("himox_h06_purifier.yaml", HIMOX_H06_PURIFIER_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_hydrotherm_dynamicx8.py

@@ -26,8 +26,6 @@ class TestHydrothermDynamicX8(
     BasicBinarySensorTests,
     BasicBinarySensorTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "hydrotherm_dynamic_x8_water_heater.yaml", HYDROTHERM_DYNAMICX8_PAYLOAD
             "hydrotherm_dynamic_x8_water_heater.yaml", HYDROTHERM_DYNAMICX8_PAYLOAD

+ 0 - 2
tests/devices/test_immax_neo_light_vento.py

@@ -30,8 +30,6 @@ TIMER_DPS = "22"
 class TestImmaxNeoLightVento(
 class TestImmaxNeoLightVento(
     SwitchableTests, BasicSelectTests, BasicLightTests, TuyaDeviceTestCase
     SwitchableTests, BasicSelectTests, BasicLightTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("immax_neo_light_vento.yaml", IMAX_NEO_LIGHT_VENTO_PAYLOAD)
         self.setUpForConfig("immax_neo_light_vento.yaml", IMAX_NEO_LIGHT_VENTO_PAYLOAD)
         self.fan = self.entities["fan"]
         self.fan = self.entities["fan"]

+ 0 - 2
tests/devices/test_inkbird_itc308_thermostat.py

@@ -34,8 +34,6 @@ class TestInkbirdITC308Thermostat(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "inkbird_itc308_thermostat.yaml", INKBIRD_ITC308_THERMOSTAT_PAYLOAD
             "inkbird_itc308_thermostat.yaml", INKBIRD_ITC308_THERMOSTAT_PAYLOAD

+ 0 - 2
tests/devices/test_inkbird_sousvide.py

@@ -37,8 +37,6 @@ class TestInkbirdSousVideCooker(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("inkbird_sousvide_cooker.yaml", INKBIRD_SOUSVIDE_PAYLOAD)
         self.setUpForConfig("inkbird_sousvide_cooker.yaml", INKBIRD_SOUSVIDE_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_ir_remote_sensors.py

@@ -12,8 +12,6 @@ IRRECV_DP = "202"
 
 
 
 
 class TestIRRemoteSensors(MultiSensorTests, TuyaDeviceTestCase):
 class TestIRRemoteSensors(MultiSensorTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("ir_remote_sensors.yaml", IR_REMOTE_SENSORS_PAYLOAD)
         self.setUpForConfig("ir_remote_sensors.yaml", IR_REMOTE_SENSORS_PAYLOAD)
         self.subject = self.entities.get("remote")
         self.subject = self.entities.get("remote")

+ 0 - 2
tests/devices/test_kogan_garage_door_opener.py

@@ -21,8 +21,6 @@ class TestKoganGarageOpener(
     BasicSensorTests,
     BasicSensorTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("kogan_garage_opener.yaml", KOGAN_GARAGE_DOOR_PAYLOAD)
         self.setUpForConfig("kogan_garage_opener.yaml", KOGAN_GARAGE_DOOR_PAYLOAD)
         self.subject = self.entities["cover_garage"]
         self.subject = self.entities["cover_garage"]

+ 0 - 2
tests/devices/test_kogan_glass_1_7l_kettle.py

@@ -13,8 +13,6 @@ CURRENTTEMP_DPS = "5"
 
 
 
 
 class TestKoganGlass1_7LKettle(TuyaDeviceTestCase):
 class TestKoganGlass1_7LKettle(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "kogan_glass_1_7l_kettle.yaml",
             "kogan_glass_1_7l_kettle.yaml",

+ 0 - 2
tests/devices/test_kogan_kawfpac09ya_airconditioner.py

@@ -18,8 +18,6 @@ UNKNOWN107_DPS = "107"
 
 
 
 
 class TestKoganKAWFPAC09YA(TargetTemperatureTests, TuyaDeviceTestCase):
 class TestKoganKAWFPAC09YA(TargetTemperatureTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "kogan_kawfpac09ya_airconditioner.yaml",
             "kogan_kawfpac09ya_airconditioner.yaml",

+ 0 - 2
tests/devices/test_kyvol_e30_vacuum.py

@@ -36,8 +36,6 @@ CARPET_DPS = "107"
 
 
 
 
 class TestKyvolE30Vacuum(MultiButtonTests, MultiSensorTests, TuyaDeviceTestCase):
 class TestKyvolE30Vacuum(MultiButtonTests, MultiSensorTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("kyvol_e30_vacuum.yaml", KYVOL_E30_VACUUM_PAYLOAD)
         self.setUpForConfig("kyvol_e30_vacuum.yaml", KYVOL_E30_VACUUM_PAYLOAD)
         self.subject = self.entities.get("vacuum")
         self.subject = self.entities.get("vacuum")

+ 0 - 2
tests/devices/test_ledvance_light.py

@@ -13,8 +13,6 @@ class TestLedvanceLight(TuyaDeviceTestCase):
     Tests for Ledvance Panel lighting.
     Tests for Ledvance Panel lighting.
     """
     """
 
 
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "ledvance_smart_plabl100x25b.yaml",
             "ledvance_smart_plabl100x25b.yaml",

+ 0 - 2
tests/devices/test_lefant_m213_vacuum.py

@@ -29,8 +29,6 @@ UNKNOWN108_DPS = "108"
 
 
 
 
 class TestLefantM213Vacuum(MultiSensorTests, TuyaDeviceTestCase):
 class TestLefantM213Vacuum(MultiSensorTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("lefant_m213_vacuum.yaml", LEFANT_M213_VACUUM_PAYLOAD)
         self.setUpForConfig("lefant_m213_vacuum.yaml", LEFANT_M213_VACUUM_PAYLOAD)
         self.subject = self.entities.get("vacuum")
         self.subject = self.entities.get("vacuum")

+ 0 - 2
tests/devices/test_lexy_f501_fan.py

@@ -28,8 +28,6 @@ class TestLexyF501Fan(
     BasicSwitchTests,
     BasicSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("lexy_f501_fan.yaml", LEXY_F501_PAYLOAD)
         self.setUpForConfig("lexy_f501_fan.yaml", LEXY_F501_PAYLOAD)
         self.subject = self.entities.get("fan")
         self.subject = self.entities.get("fan")

+ 0 - 2
tests/devices/test_logicom_powerstrip.py

@@ -22,8 +22,6 @@ class TestLogicomPowerstrip(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("logicom_powerstrip.yaml", LOGICOM_STRIPPY_PAYLOAD)
         self.setUpForConfig("logicom_powerstrip.yaml", LOGICOM_STRIPPY_PAYLOAD)
         self.setUpMultiSwitch(
         self.setUpMultiSwitch(

+ 0 - 2
tests/devices/test_m027_curtain.py

@@ -22,8 +22,6 @@ UNKNOWN101_DPS = "101"
 
 
 
 
 class TestM027Curtains(MultiSensorTests, BasicSelectTests, TuyaDeviceTestCase):
 class TestM027Curtains(MultiSensorTests, BasicSelectTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("m027_curtain.yaml", M027_CURTAIN_PAYLOAD)
         self.setUpForConfig("m027_curtain.yaml", M027_CURTAIN_PAYLOAD)
         self.subject = self.entities["cover_curtain"]
         self.subject = self.entities["cover_curtain"]

+ 0 - 2
tests/devices/test_moebot.py

@@ -32,8 +32,6 @@ COMMAND_DP = "115"
 
 
 
 
 class TestMoebot(TuyaDeviceTestCase):
 class TestMoebot(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("moebot_s_mower.yaml", MOEBOT_PAYLOAD)
         self.setUpForConfig("moebot_s_mower.yaml", MOEBOT_PAYLOAD)
         self.mower = self.entities.get("lawn_mower")
         self.mower = self.entities.get("lawn_mower")

+ 0 - 2
tests/devices/test_moes_bht002_thermostat.py

@@ -24,8 +24,6 @@ UNKNOWN104_DPS = "104"
 class TestMoesBHT002Thermostat(
 class TestMoesBHT002Thermostat(
     BasicLockTests, TargetTemperatureTests, TuyaDeviceTestCase
     BasicLockTests, TargetTemperatureTests, TuyaDeviceTestCase
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "moes_bht002_thermostat_c.yaml",
             "moes_bht002_thermostat_c.yaml",

+ 0 - 2
tests/devices/test_moes_rgb_socket.py

@@ -41,8 +41,6 @@ class TestMoesRGBSocket(
     BasicSwitchTests,
     BasicSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("moes_rgb_socket.yaml", MOES_RGB_SOCKET_PAYLOAD)
         self.setUpForConfig("moes_rgb_socket.yaml", MOES_RGB_SOCKET_PAYLOAD)
         self.light = self.entities.get("light_nightlight")
         self.light = self.entities.get("light_nightlight")

+ 0 - 2
tests/devices/test_motion_sensor_light.py

@@ -16,8 +16,6 @@ RESET_DPS = "106"
 
 
 
 
 class TestMotionLight(BasicSwitchTests, MultiNumberTests, TuyaDeviceTestCase):
 class TestMotionLight(BasicSwitchTests, MultiNumberTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("motion_sensor_light.yaml", MOTION_LIGHT_PAYLOAD)
         self.setUpForConfig("motion_sensor_light.yaml", MOTION_LIGHT_PAYLOAD)
         self.subject = self.entities.get("light")
         self.subject = self.entities.get("light")

+ 0 - 2
tests/devices/test_mustool_mt15mt29_airbox.py

@@ -6,8 +6,6 @@ from .base_device_tests import TuyaDeviceTestCase
 
 
 
 
 class TestMustoolMT15MT29Airbox(MultiTimeTests, TuyaDeviceTestCase):
 class TestMustoolMT15MT29Airbox(MultiTimeTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "mustool_mt15mt29_airbox.yaml", MUSTOOL_MT15MT29_AIRBOX_PAYLOAD
             "mustool_mt15mt29_airbox.yaml", MUSTOOL_MT15MT29_AIRBOX_PAYLOAD

+ 0 - 2
tests/devices/test_nashone_mts700wb_thermostat.py

@@ -34,8 +34,6 @@ class TestNashoneMTS700WBThermostat(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "nashone_mts700wb_thermostat.yaml",
             "nashone_mts700wb_thermostat.yaml",

+ 0 - 2
tests/devices/test_nedis_htpl20f_heater.py

@@ -28,8 +28,6 @@ class TestNedisHtpl20fHeater(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("nedis_htpl20f_heater.yaml", NEDIS_HTPL20F_PAYLOAD)
         self.setUpForConfig("nedis_htpl20f_heater.yaml", NEDIS_HTPL20F_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_orion_outdoor_siren.py

@@ -20,8 +20,6 @@ DEFAULT_TONE = "alarm_sound_light"
 
 
 
 
 class TestOrionSiren(MultiBinarySensorTests, BasicSensorTests, TuyaDeviceTestCase):
 class TestOrionSiren(MultiBinarySensorTests, BasicSensorTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("orion_outdoor_siren.yaml", ORION_SIREN_PAYLOAD)
         self.setUpForConfig("orion_outdoor_siren.yaml", ORION_SIREN_PAYLOAD)
         self.subject = self.entities.get("siren")
         self.subject = self.entities.get("siren")

+ 0 - 2
tests/devices/test_orion_smartlock.py

@@ -26,8 +26,6 @@ class TestOrionSmartLock(
     MultiSensorTests,
     MultiSensorTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("orion_smart_lock.yaml", ORION_SMARTLOCK_PAYLOAD)
         self.setUpForConfig("orion_smart_lock.yaml", ORION_SMARTLOCK_PAYLOAD)
         self.subject = self.entities.get("lock")
         self.subject = self.entities.get("lock")

+ 0 - 2
tests/devices/test_parkside_plgs2012a1_smart_charger.py

@@ -44,8 +44,6 @@ class TestParksidePLGS2012A1Charger(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "parkside_plgs2012a1_smart_charger.yaml", PARKSIDE_PLGS2012A1_PAYLOAD
             "parkside_plgs2012a1_smart_charger.yaml", PARKSIDE_PLGS2012A1_PAYLOAD

+ 0 - 2
tests/devices/test_poiema_one_purifier.py

@@ -29,8 +29,6 @@ class TestPoeimaOnePurifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("poiema_one_purifier.yaml", POIEMA_ONE_PURIFIER_PAYLOAD)
         self.setUpForConfig("poiema_one_purifier.yaml", POIEMA_ONE_PURIFIER_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_poolex_qline_heatpump.py

@@ -20,8 +20,6 @@ class TestPoolexQlineHeatpump(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("poolex_qline_heatpump.yaml", POOLEX_QLINE_HEATPUMP_PAYLOAD)
         self.setUpForConfig("poolex_qline_heatpump.yaml", POOLEX_QLINE_HEATPUMP_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_qoto_03_sprinkler.py

@@ -21,8 +21,6 @@ class TestQotoSprinkler(
     MultiSensorTests,
     MultiSensorTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("qoto_03_sprinkler.yaml", QOTO_SPRINKLER_PAYLOAD)
         self.setUpForConfig("qoto_03_sprinkler.yaml", QOTO_SPRINKLER_PAYLOAD)
         self.subject = self.entities.get("valve_water")
         self.subject = self.entities.get("valve_water")

+ 0 - 2
tests/devices/test_qs_c01_curtain.py

@@ -17,8 +17,6 @@ TRAVELTIME_DPS = "10"
 
 
 
 
 class TestQSC01Curtains(BasicNumberTests, BasicSelectTests, TuyaDeviceTestCase):
 class TestQSC01Curtains(BasicNumberTests, BasicSelectTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("qs_c01_curtain.yaml", QS_C01_CURTAIN_PAYLOAD)
         self.setUpForConfig("qs_c01_curtain.yaml", QS_C01_CURTAIN_PAYLOAD)
         self.subject = self.entities["cover_curtain"]
         self.subject = self.entities["cover_curtain"]

+ 0 - 2
tests/devices/test_renpho_rp_ap001s.py

@@ -31,8 +31,6 @@ class TestRenphoPurifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("renpho_rp_ap001s.yaml", RENPHO_PURIFIER_PAYLOAD)
         self.setUpForConfig("renpho_rp_ap001s.yaml", RENPHO_PURIFIER_PAYLOAD)
         self.subject = self.entities.get("fan")
         self.subject = self.entities.get("fan")

+ 0 - 2
tests/devices/test_rgbcw_lightbulb.py

@@ -19,8 +19,6 @@ TIMER_DPS = "26"
 
 
 
 
 class TestRGBCWLightbulb(BasicTextTests, TuyaDeviceTestCase):
 class TestRGBCWLightbulb(BasicTextTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("rgbcw_lightbulb.yaml", RGBCW_LIGHTBULB_PAYLOAD)
         self.setUpForConfig("rgbcw_lightbulb.yaml", RGBCW_LIGHTBULB_PAYLOAD)
         self.subject = self.entities.get("light")
         self.subject = self.entities.get("light")

+ 0 - 2
tests/devices/test_sd123_hpr01_presence.py

@@ -35,8 +35,6 @@ class TestSD123HumanPresenceRadar(
     MultiSelectTests,
     MultiSelectTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("sd123_hpr01_presence.yaml", SD123_PRESENCE_PAYLOAD)
         self.setUpForConfig("sd123_hpr01_presence.yaml", SD123_PRESENCE_PAYLOAD)
         self.setUpBasicBinarySensor(
         self.setUpBasicBinarySensor(

+ 0 - 2
tests/devices/test_simple_blinds.py

@@ -14,8 +14,6 @@ ACTION_DPS = "7"
 
 
 
 
 class TestSimpleBlinds(TuyaDeviceTestCase):
 class TestSimpleBlinds(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("simple_blinds.yaml", SIMPLE_BLINDS_PAYLOAD)
         self.setUpForConfig("simple_blinds.yaml", SIMPLE_BLINDS_PAYLOAD)
         self.subject = self.entities["cover_blind"]
         self.subject = self.entities["cover_blind"]

+ 0 - 2
tests/devices/test_simple_switch_with_timer.py

@@ -9,8 +9,6 @@ TIMER_DPS = "11"
 
 
 
 
 class TestTimedSwitch(SwitchableTests, TuyaDeviceTestCase):
 class TestTimedSwitch(SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("simple_switch_timer.yaml", TIMED_SOCKET_PAYLOAD)
         self.setUpForConfig("simple_switch_timer.yaml", TIMED_SOCKET_PAYLOAD)
         self.subject = self.entities.get("switch")
         self.subject = self.entities.get("switch")

+ 0 - 2
tests/devices/test_simple_switch_with_timerv2.py

@@ -10,8 +10,6 @@ INITIAL_STATE_DPS = "38"
 
 
 
 
 class TestTimedSwitch(SwitchableTests, TuyaDeviceTestCase):
 class TestTimedSwitch(SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("simple_switch_timerv2.yaml", TIMED_SOCKETV2_PAYLOAD)
         self.setUpForConfig("simple_switch_timerv2.yaml", TIMED_SOCKETV2_PAYLOAD)
         self.subject = self.entities.get("switch")
         self.subject = self.entities.get("switch")

+ 0 - 2
tests/devices/test_smartplug_encoded.py

@@ -14,8 +14,6 @@ SCHEDULE_DPS = "103"
 
 
 
 
 class TestSwitchEncoded(SwitchableTests, TuyaDeviceTestCase):
 class TestSwitchEncoded(SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("smartplug_encoded.yaml", SMARTPLUG_ENCODED_PAYLOAD)
         self.setUpForConfig("smartplug_encoded.yaml", SMARTPLUG_ENCODED_PAYLOAD)
         self.subject = self.entities.get("switch_outlet")
         self.subject = self.entities.get("switch_outlet")

+ 0 - 2
tests/devices/test_smartplugv2.py

@@ -25,8 +25,6 @@ class TestSwitchV2(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("smartplugv2.yaml", KOGAN_SOCKET_PAYLOAD2)
         self.setUpForConfig("smartplugv2.yaml", KOGAN_SOCKET_PAYLOAD2)
         self.subject = self.entities.get("switch_outlet")
         self.subject = self.entities.get("switch_outlet")

+ 0 - 2
tests/devices/test_smartplugv2_energy.py

@@ -46,8 +46,6 @@ class TestSwitchV2Energy(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("smartplugv2_energy.yaml", SMARTSWITCH_ENERGY_PAYLOAD)
         self.setUpForConfig("smartplugv2_energy.yaml", SMARTSWITCH_ENERGY_PAYLOAD)
         self.setUpMultiSwitch(
         self.setUpMultiSwitch(

+ 0 - 2
tests/devices/test_starlight_heatpump.py

@@ -13,8 +13,6 @@ class TestStarLightHeatpump(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("starlight_heatpump.yaml", STARLIGHT_HEATPUMP_PAYLOAD)
         self.setUpForConfig("starlight_heatpump.yaml", STARLIGHT_HEATPUMP_PAYLOAD)
         self.subject = self.entities["climate"]
         self.subject = self.entities["climate"]

+ 0 - 2
tests/devices/test_stirling_fs140dc_fan.py

@@ -13,8 +13,6 @@ TIMER_DPS = "22"
 
 
 
 
 class TestStirlingFS1Fan(SwitchableTests, TuyaDeviceTestCase):
 class TestStirlingFS1Fan(SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("stirling_fs140dc_fan.yaml", STIRLING_FS1_FAN_PAYLOAD)
         self.setUpForConfig("stirling_fs140dc_fan.yaml", STIRLING_FS1_FAN_PAYLOAD)
         self.subject = self.entities.get("fan")
         self.subject = self.entities.get("fan")

+ 0 - 2
tests/devices/test_thermex_if50v.py

@@ -24,8 +24,6 @@ class TestThermexIF50V(
     BasicBinarySensorTests,
     BasicBinarySensorTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "thermex_if50v_waterheater.yaml",
             "thermex_if50v_waterheater.yaml",

+ 0 - 2
tests/devices/test_tmwf02_fan.py

@@ -12,8 +12,6 @@ SPEED_DPS = "4"
 
 
 
 
 class TestTMWF02Fan(SwitchableTests, TuyaDeviceTestCase):
 class TestTMWF02Fan(SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("tmwf02_fan.yaml", TMWF02_FAN_PAYLOAD)
         self.setUpForConfig("tmwf02_fan.yaml", TMWF02_FAN_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_tompd63lw_breaker.py

@@ -27,8 +27,6 @@ TEST_DP = "21"
 
 
 
 
 class TestTOMPD63lw(MultiSensorTests, TuyaDeviceTestCase):
 class TestTOMPD63lw(MultiSensorTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("tompd_63lw_breaker.yaml", TOMPD63LW_SOCKET_PAYLOAD)
         self.setUpForConfig("tompd_63lw_breaker.yaml", TOMPD63LW_SOCKET_PAYLOAD)
         self.subject = self.entities.get("switch")
         self.subject = self.entities.get("switch")

+ 0 - 2
tests/devices/test_treatlife_ds02f.py

@@ -11,8 +11,6 @@ SPEED_DPS = "3"
 
 
 
 
 class TestTreatlifeFan(SwitchableTests, TuyaDeviceTestCase):
 class TestTreatlifeFan(SwitchableTests, TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("treatlife_ds02_fan.yaml", TREATLIFE_DS02F_PAYLOAD)
         self.setUpForConfig("treatlife_ds02_fan.yaml", TREATLIFE_DS02F_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_vork_vk6067aw_purifier.py

@@ -33,8 +33,6 @@ class TestVorkVK6267AWPurifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("vork_vk6067aw_purifier.yaml", VORK_VK6067_PURIFIER_PAYLOAD)
         self.setUpForConfig("vork_vk6067aw_purifier.yaml", VORK_VK6067_PURIFIER_PAYLOAD)
         self.subject = self.entities["fan"]
         self.subject = self.entities["fan"]

+ 0 - 2
tests/devices/test_weau_pool_heatpumpv2.py

@@ -23,8 +23,6 @@ class TestWeauPoolHeatpumpV2(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("weau_pool_heatpump_v2.yaml", WEAU_POOL_HEATPUMPV2_PAYLOAD)
         self.setUpForConfig("weau_pool_heatpump_v2.yaml", WEAU_POOL_HEATPUMPV2_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_wetair_wawh1210lw_humidifier.py

@@ -35,8 +35,6 @@ class TestWetairWAWH1210LWHumidifier(
     SwitchableTests,
     SwitchableTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "wetair_wawh1210lw_humidifier.yaml", WETAIR_WAWH1210_HUMIDIFIER_PAYLOAD
             "wetair_wawh1210lw_humidifier.yaml", WETAIR_WAWH1210_HUMIDIFIER_PAYLOAD

+ 0 - 2
tests/devices/test_wetair_wch750_heater.py

@@ -33,8 +33,6 @@ class TestWetairWCH750Heater(
     TargetTemperatureTests,
     TargetTemperatureTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("wetair_wch750_heater.yaml", WETAIR_WCH750_HEATER_PAYLOAD)
         self.setUpForConfig("wetair_wch750_heater.yaml", WETAIR_WCH750_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.subject = self.entities.get("climate")

+ 0 - 2
tests/devices/test_woox_r4028_powerstrip.py

@@ -20,8 +20,6 @@ class TestWooxR4028Powerstrip(
     MultiSwitchTests,
     MultiSwitchTests,
     TuyaDeviceTestCase,
     TuyaDeviceTestCase,
 ):
 ):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("woox_r4028_powerstrip.yaml", WOOX_R4028_SOCKET_PAYLOAD)
         self.setUpForConfig("woox_r4028_powerstrip.yaml", WOOX_R4028_SOCKET_PAYLOAD)
         self.setUpMultiSwitch(
         self.setUpMultiSwitch(

+ 0 - 2
tests/devices/test_zemismart_am25_blind.py

@@ -19,8 +19,6 @@ TILTPOS_DP = "109"
 
 
 
 
 class TestAM25Blinds(TuyaDeviceTestCase):
 class TestAM25Blinds(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig(
         self.setUpForConfig(
             "zemismart_am25_rollerblind.yaml",
             "zemismart_am25_rollerblind.yaml",

+ 0 - 2
tests/devices/test_zx_g30_alarm.py

@@ -26,8 +26,6 @@ TICKDOWN_DP = "29"
 
 
 
 
 class TestZXG30Alarm(TuyaDeviceTestCase):
 class TestZXG30Alarm(TuyaDeviceTestCase):
-    __test__ = True
-
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("zx_g30_alarm.yaml", ZXG30_ALARM_PAYLOAD)
         self.setUpForConfig("zx_g30_alarm.yaml", ZXG30_ALARM_PAYLOAD)
         self.subject = self.entities["alarm_control_panel"]
         self.subject = self.entities["alarm_control_panel"]

+ 724 - 715
tests/test_config_flow.py

@@ -1,715 +1,724 @@
-"""Tests for the config flow."""
-
-from unittest.mock import ANY, AsyncMock, MagicMock, patch
-
-import pytest
-import voluptuous as vol
-from homeassistant.const import CONF_HOST, CONF_NAME
-from homeassistant.data_entry_flow import FlowResultType
-from pytest_homeassistant_custom_component.common import MockConfigEntry
-
-from custom_components.tuya_local import (
-    async_migrate_entry,
-    config_flow,
-    get_device_unique_id,
-)
-from custom_components.tuya_local.const import (
-    CONF_DEVICE_CID,
-    CONF_DEVICE_ID,
-    CONF_LOCAL_KEY,
-    CONF_POLL_ONLY,
-    CONF_PROTOCOL_VERSION,
-    CONF_TYPE,
-    DOMAIN,
-)
-
-# Designed to contain "special" characters that users constantly suspect.
-TESTKEY = ")<jO<@)'P1|kR$Kd"
-
-
-@pytest.fixture(autouse=True)
-def auto_enable_custom_integrations(enable_custom_integrations):
-    yield
-
-
-@pytest.fixture(autouse=True)
-def prevent_task_creation():
-    with patch(
-        "custom_components.tuya_local.device.TuyaLocalDevice.register_entity",
-    ):
-        yield
-
-
-@pytest.fixture
-def bypass_setup():
-    """Prevent actual setup of the integration after config flow."""
-    with patch(
-        "custom_components.tuya_local.async_setup_entry",
-        return_value=True,
-    ):
-        yield
-
-
-@pytest.fixture
-def bypass_data_fetch():
-    """Prevent actual data fetching from the device."""
-    with patch(
-        "tinytuya.Device.status",
-        return_value={"1": True},
-    ):
-        yield
-
-
-@pytest.mark.asyncio
-async def test_init_entry(hass, bypass_data_fetch):
-    """Test initialisation of the config flow."""
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=11,
-        title="test",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_TYPE: "kogan_kahtp_heater",
-            CONF_DEVICE_CID: None,
-        },
-        options={},
-    )
-    entry.add_to_hass(hass)
-    await hass.config_entries.async_setup(entry.entry_id)
-    await hass.async_block_till_done()
-    assert hass.states.get("climate.test")
-    assert hass.states.get("lock.test_child_lock")
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.setup_device")
-async def test_migrate_entry(mock_setup, hass):
-    """Test migration from old entry format."""
-    mock_device = MagicMock()
-    mock_device.async_inferred_type = AsyncMock(return_value="goldair_gpph_heater")
-    mock_setup.return_value = mock_device
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=1,
-        title="test",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "auto",
-            "climate": True,
-            "child_lock": True,
-            "display_light": True,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert await async_migrate_entry(hass, entry)
-
-    mock_device.async_inferred_type = AsyncMock(return_value=None)
-    mock_device.reset_mock()
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=1,
-        title="test2",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "unknown",
-            "climate": False,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert not await async_migrate_entry(hass, entry)
-    mock_device.reset_mock()
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=2,
-        title="test3",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "auto",
-        },
-        options={
-            "climate": False,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert not await async_migrate_entry(hass, entry)
-
-    mock_device.async_inferred_type = AsyncMock(return_value="smartplugv1")
-    mock_device.reset_mock()
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=3,
-        title="test4",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "smartplugv1",
-        },
-        options={
-            "switch": True,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert await async_migrate_entry(hass, entry)
-
-    mock_device.async_inferred_type = AsyncMock(return_value="smartplugv2")
-    mock_device.reset_mock()
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=3,
-        title="test5",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "smartplugv1",
-        },
-        options={
-            "switch": True,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert await async_migrate_entry(hass, entry)
-
-    mock_device.async_inferred_type = AsyncMock(return_value="goldair_dehumidifier")
-    mock_device.reset_mock()
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=4,
-        title="test6",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "goldair_dehumidifier",
-        },
-        options={
-            "humidifier": True,
-            "fan": True,
-            "light": True,
-            "lock": False,
-            "switch": True,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert await async_migrate_entry(hass, entry)
-
-    mock_device.async_inferred_type = AsyncMock(
-        return_value="grid_connect_usb_double_power_point"
-    )
-    mock_device.reset_mock()
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=6,
-        title="test7",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "grid_connect_usb_double_power_point",
-        },
-        options={
-            "switch_main_switch": True,
-            "switch_left_outlet": True,
-            "switch_right_outlet": True,
-        },
-    )
-    entry.add_to_hass(hass)
-    assert await async_migrate_entry(hass, entry)
-
-
-@pytest.mark.asyncio
-async def test_flow_user_init(hass):
-    """Test the initialisation of the form in the first page of the manual config flow path."""
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": "local"}
-    )
-    expected = {
-        "data_schema": ANY,
-        "description_placeholders": ANY,
-        "errors": {},
-        "flow_id": ANY,
-        "handler": DOMAIN,
-        "step_id": "local",
-        "type": "form",
-        "last_step": ANY,
-        "preview": ANY,
-    }
-    assert expected == result
-    # Check the schema.  Simple comparison does not work since they are not
-    # the same object
-    try:
-        result["data_schema"](
-            {CONF_DEVICE_ID: "test", CONF_LOCAL_KEY: TESTKEY, CONF_HOST: "test"}
-        )
-    except vol.MultipleInvalid:
-        assert False
-    try:
-        result["data_schema"]({CONF_DEVICE_ID: "missing_some"})
-        assert False
-    except vol.MultipleInvalid:
-        pass
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
-async def test_async_test_connection_valid(mock_device, hass):
-    """Test that device is returned when connection is valid."""
-    mock_instance = AsyncMock()
-    mock_instance.has_returned_state = True
-    mock_instance.pause = MagicMock()
-    mock_instance.resume = MagicMock()
-    mock_device.return_value = mock_instance
-    hass.data[DOMAIN] = {"deviceid": {"device": mock_instance}}
-
-    device = await config_flow.async_test_connection(
-        {
-            CONF_DEVICE_ID: "deviceid",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_HOST: "hostname",
-            CONF_PROTOCOL_VERSION: "auto",
-        },
-        hass,
-    )
-    assert device == mock_instance
-    mock_instance.pause.assert_called_once()
-    mock_instance.resume.assert_called_once()
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
-async def test_async_test_connection_for_subdevice_valid(mock_device, hass):
-    """Test that subdevice is returned when connection is valid."""
-    mock_instance = AsyncMock()
-    mock_instance.has_returned_state = True
-    mock_instance.pause = MagicMock()
-    mock_instance.resume = MagicMock()
-    mock_device.return_value = mock_instance
-    hass.data[DOMAIN] = {"subdeviceid": {"device": mock_instance}}
-
-    device = await config_flow.async_test_connection(
-        {
-            CONF_DEVICE_ID: "deviceid",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_HOST: "hostname",
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_DEVICE_CID: "subdeviceid",
-        },
-        hass,
-    )
-    assert device == mock_instance
-    mock_instance.pause.assert_called_once()
-    mock_instance.resume.assert_called_once()
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
-async def test_async_test_connection_invalid(mock_device, hass):
-    """Test that None is returned when connection is invalid."""
-    mock_instance = AsyncMock()
-    mock_instance.has_returned_state = False
-    mock_device.return_value = mock_instance
-    device = await config_flow.async_test_connection(
-        {
-            CONF_DEVICE_ID: "deviceid",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_HOST: "hostname",
-            CONF_PROTOCOL_VERSION: "auto",
-        },
-        hass,
-    )
-    assert device is None
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.async_test_connection")
-async def test_flow_user_init_invalid_config(mock_test, hass):
-    """Test errors populated when config is invalid."""
-    mock_test.return_value = None
-    flow = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": "local"}
-    )
-    result = await hass.config_entries.flow.async_configure(
-        flow["flow_id"],
-        user_input={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: "badkey",
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_POLL_ONLY: False,
-        },
-    )
-    assert {"base": "connection"} == result["errors"]
-
-
-def setup_device_mock(mock, failure=False, type="test"):
-    mock_type = MagicMock()
-    mock_type.legacy_type = type
-    mock_type.config_type = type
-    mock_type.match_quality.return_value = 100
-    mock.async_possible_types = AsyncMock(
-        return_value=[mock_type] if not failure else []
-    )
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.async_test_connection")
-async def test_flow_user_init_data_valid(mock_test, hass):
-    """Test we advance to the next step when connection config is valid."""
-    mock_device = MagicMock()
-    setup_device_mock(mock_device)
-    mock_test.return_value = mock_device
-
-    flow = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": "local"}
-    )
-    result = await hass.config_entries.flow.async_configure(
-        flow["flow_id"],
-        user_input={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-        },
-    )
-    assert "form" == result["type"]
-    assert "select_type" == result["step_id"]
-
-
-@pytest.mark.asyncio
-@patch.object(config_flow.ConfigFlowHandler, "device")
-async def test_flow_select_type_init(mock_device, hass):
-    """Test the initialisation of the form in the 2nd step of the config flow."""
-    setup_device_mock(mock_device)
-
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": "select_type"}
-    )
-    expected = {
-        "data_schema": ANY,
-        "description_placeholders": None,
-        "errors": None,
-        "flow_id": ANY,
-        "handler": DOMAIN,
-        "step_id": "select_type",
-        "type": "form",
-        "last_step": ANY,
-        "preview": ANY,
-    }
-    assert expected == result
-    # Check the schema.  Simple comparison does not work since they are not
-    # the same object
-    try:
-        result["data_schema"]({CONF_TYPE: "test"})
-    except vol.MultipleInvalid:
-        assert False
-    try:
-        result["data_schema"]({CONF_TYPE: "not_test"})
-        assert False
-    except vol.MultipleInvalid:
-        pass
-
-
-@pytest.mark.asyncio
-@patch.object(config_flow.ConfigFlowHandler, "device")
-async def test_flow_select_type_aborts_when_no_match(mock_device, hass):
-    """Test the flow aborts when an unsupported device is used."""
-    setup_device_mock(mock_device, failure=True)
-
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": "select_type"}
-    )
-
-    assert result["type"] == "abort"
-    assert result["reason"] == "not_supported"
-
-
-@pytest.mark.asyncio
-@patch.object(config_flow.ConfigFlowHandler, "device")
-async def test_flow_select_type_data_valid(mock_device, hass):
-    """Test the flow continues when valid data is supplied."""
-    setup_device_mock(mock_device, type="smartplugv1")
-
-    flow = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": "select_type"}
-    )
-    result = await hass.config_entries.flow.async_configure(
-        flow["flow_id"],
-        user_input={CONF_TYPE: "smartplugv1"},
-    )
-    assert "form" == result["type"]
-    assert "choose_entities" == result["step_id"]
-
-
-@pytest.mark.asyncio
-async def test_flow_choose_entities_init(hass):
-    """Test the initialisation of the form in the 3rd step of the config flow."""
-
-    with patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "smartplugv1"}):
-        result = await hass.config_entries.flow.async_init(
-            DOMAIN, context={"source": "choose_entities"}
-        )
-
-    expected = {
-        "data_schema": ANY,
-        "description_placeholders": None,
-        "errors": None,
-        "flow_id": ANY,
-        "handler": DOMAIN,
-        "step_id": "choose_entities",
-        "type": "form",
-        "last_step": ANY,
-        "preview": ANY,
-    }
-    assert expected == result
-    # Check the schema.  Simple comparison does not work since they are not
-    # the same object
-    try:
-        result["data_schema"]({CONF_NAME: "test"})
-    except vol.MultipleInvalid:
-        assert False
-    try:
-        result["data_schema"]({"climate": True})
-        assert False
-    except vol.MultipleInvalid:
-        pass
-
-
-@pytest.mark.asyncio
-async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
-    """Test the flow ends when data is valid."""
-
-    with patch.dict(
-        config_flow.ConfigFlowHandler.data,
-        {
-            CONF_DEVICE_ID: "deviceid",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_HOST: "hostname",
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_TYPE: "kogan_kahtp_heater",
-            CONF_DEVICE_CID: None,
-        },
-    ):
-        flow = await hass.config_entries.flow.async_init(
-            DOMAIN, context={"source": "choose_entities"}
-        )
-        result = await hass.config_entries.flow.async_configure(
-            flow["flow_id"],
-            user_input={
-                CONF_NAME: "test",
-            },
-        )
-        expected = {
-            "version": 13,
-            "minor_version": ANY,
-            "context": {"source": "choose_entities"},
-            "type": FlowResultType.CREATE_ENTRY,
-            "flow_id": ANY,
-            "handler": DOMAIN,
-            "title": "test",
-            "description": None,
-            "description_placeholders": None,
-            "result": ANY,
-            "subentries": (),
-            "options": {},
-            "data": {
-                CONF_DEVICE_ID: "deviceid",
-                CONF_HOST: "hostname",
-                CONF_LOCAL_KEY: TESTKEY,
-                CONF_POLL_ONLY: False,
-                CONF_PROTOCOL_VERSION: "auto",
-                CONF_TYPE: "kogan_kahtp_heater",
-                CONF_DEVICE_CID: None,
-            },
-        }
-        assert expected == result
-
-
-@pytest.mark.asyncio
-async def test_options_flow_init(hass, bypass_data_fetch):
-    """Test config flow options."""
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=13,
-        unique_id="uniqueid",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_NAME: "test",
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_TYPE: "smartplugv1",
-            CONF_DEVICE_CID: "",
-        },
-    )
-    config_entry.add_to_hass(hass)
-
-    assert await hass.config_entries.async_setup(config_entry.entry_id)
-    await hass.async_block_till_done()
-
-    # show initial form
-    result = await hass.config_entries.options.async_init(config_entry.entry_id)
-    assert "form" == result["type"]
-    assert "user" == result["step_id"]
-    assert {} == result["errors"]
-    assert result["data_schema"](
-        {
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-        }
-    )
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.async_test_connection")
-async def test_options_flow_modifies_config(mock_test, hass, bypass_setup):
-    mock_device = MagicMock()
-    mock_test.return_value = mock_device
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=13,
-        unique_id="uniqueid",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_NAME: "test",
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_TYPE: "ble_pt216_temp_humidity",
-            CONF_DEVICE_CID: "subdeviceid",
-        },
-    )
-    config_entry.add_to_hass(hass)
-
-    assert await hass.config_entries.async_setup(config_entry.entry_id)
-    await hass.async_block_till_done()
-    # show initial form
-    form = await hass.config_entries.options.async_init(config_entry.entry_id)
-    # submit updated config
-    result = await hass.config_entries.options.async_configure(
-        form["flow_id"],
-        user_input={
-            CONF_HOST: "new_hostname",
-            CONF_LOCAL_KEY: "new_key",
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: 3.3,
-        },
-    )
-    expected = {
-        CONF_HOST: "new_hostname",
-        CONF_LOCAL_KEY: "new_key",
-        CONF_POLL_ONLY: False,
-        CONF_PROTOCOL_VERSION: 3.3,
-    }
-    assert "create_entry" == result["type"]
-    assert "" == result["title"]
-    assert result["result"] is True
-    assert expected == result["data"]
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.async_test_connection")
-async def test_options_flow_fails_when_connection_fails(
-    mock_test, hass, bypass_data_fetch
-):
-    mock_test.return_value = None
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=13,
-        unique_id="uniqueid",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_NAME: "test",
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_TYPE: "smartplugv1",
-            CONF_DEVICE_CID: "",
-        },
-    )
-    config_entry.add_to_hass(hass)
-
-    assert await hass.config_entries.async_setup(config_entry.entry_id)
-    await hass.async_block_till_done()
-    # show initial form
-    form = await hass.config_entries.options.async_init(config_entry.entry_id)
-    # submit updated config
-    result = await hass.config_entries.options.async_configure(
-        form["flow_id"],
-        user_input={
-            CONF_HOST: "new_hostname",
-            CONF_LOCAL_KEY: "new_key",
-        },
-    )
-    assert "form" == result["type"]
-    assert "user" == result["step_id"]
-    assert {"base": "connection"} == result["errors"]
-
-
-@pytest.mark.asyncio
-@patch("custom_components.tuya_local.config_flow.async_test_connection")
-async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
-    mock_device = MagicMock()
-    mock_test.return_value = mock_device
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=13,
-        unique_id="uniqueid",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_NAME: "test",
-            CONF_POLL_ONLY: False,
-            CONF_PROTOCOL_VERSION: "auto",
-            CONF_TYPE: "non_existing",
-        },
-    )
-    config_entry.add_to_hass(hass)
-
-    await hass.config_entries.async_setup(config_entry.entry_id)
-    await hass.async_block_till_done()
-    # show initial form
-    result = await hass.config_entries.options.async_init(config_entry.entry_id)
-    assert result["type"] == "abort"
-    assert result["reason"] == "not_supported"
-
-
-def test_migration_gets_correct_device_id():
-    """Test that migration gets the correct device id."""
-    # Normal device
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        version=1,
-        title="test",
-        data={
-            CONF_DEVICE_ID: "deviceid",
-            CONF_HOST: "hostname",
-            CONF_LOCAL_KEY: TESTKEY,
-            CONF_TYPE: "auto",
-        },
-    )
-    assert get_device_unique_id(entry) == "deviceid"
+"""Tests for the config flow."""
+
+import pytest
+import voluptuous as vol
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.data_entry_flow import FlowResultType
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.tuya_local import (
+    async_migrate_entry,
+    config_flow,
+    get_device_unique_id,
+)
+from custom_components.tuya_local.const import (
+    CONF_DEVICE_CID,
+    CONF_DEVICE_ID,
+    CONF_LOCAL_KEY,
+    CONF_POLL_ONLY,
+    CONF_PROTOCOL_VERSION,
+    CONF_TYPE,
+    DOMAIN,
+)
+
+# Designed to contain "special" characters that users constantly suspect.
+TESTKEY = ")<jO<@)'P1|kR$Kd"
+
+
+@pytest.fixture(autouse=True)
+def auto_enable_custom_integrations(enable_custom_integrations):
+    yield
+
+
+@pytest.fixture(autouse=True)
+def prevent_task_creation(mocker):
+    mocker.patch("custom_components.tuya_local.device.TuyaLocalDevice.register_entity")
+    yield
+
+
+@pytest.fixture
+def bypass_setup(mocker):
+    """Prevent actual setup of the integration after config flow."""
+    mocker.patch("custom_components.tuya_local.async_setup_entry", return_value=True)
+    yield
+
+
+@pytest.fixture
+def bypass_data_fetch(mocker):
+    """Prevent actual data fetching from the device."""
+    mocker.patch("tinytuya.Device.status", return_value={"1": True})
+    yield
+
+
+@pytest.mark.asyncio
+async def test_init_entry(hass, bypass_data_fetch):
+    """Test initialisation of the config flow."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=11,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "kogan_kahtp_heater",
+            CONF_DEVICE_CID: None,
+        },
+        options={},
+    )
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+    assert hass.states.get("climate.test")
+    assert hass.states.get("lock.test_child_lock")
+
+
+@pytest.mark.asyncio
+async def test_migrate_entry(hass, mocker):
+    """Test migration from old entry format."""
+    mock_device = mocker.MagicMock()
+    mock_device.async_inferred_type = mocker.AsyncMock(
+        return_value="goldair_gpph_heater"
+    )
+    mocker.patch("custom_components.tuya_local.setup_device", return_value=mock_device)
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=1,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "auto",
+            "climate": True,
+            "child_lock": True,
+            "display_light": True,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert await async_migrate_entry(hass, entry)
+
+    mock_device.async_inferred_type = mocker.AsyncMock(return_value=None)
+    mock_device.reset_mock()
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=1,
+        title="test2",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "unknown",
+            "climate": False,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert not await async_migrate_entry(hass, entry)
+    mock_device.reset_mock()
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=2,
+        title="test3",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "auto",
+        },
+        options={
+            "climate": False,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert not await async_migrate_entry(hass, entry)
+
+    mock_device.async_inferred_type = mocker.AsyncMock(return_value="smartplugv1")
+    mock_device.reset_mock()
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=3,
+        title="test4",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "smartplugv1",
+        },
+        options={
+            "switch": True,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert await async_migrate_entry(hass, entry)
+
+    mock_device.async_inferred_type = mocker.AsyncMock(return_value="smartplugv2")
+    mock_device.reset_mock()
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=3,
+        title="test5",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "smartplugv1",
+        },
+        options={
+            "switch": True,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert await async_migrate_entry(hass, entry)
+
+    mock_device.async_inferred_type = mocker.AsyncMock(
+        return_value="goldair_dehumidifier"
+    )
+    mock_device.reset_mock()
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=4,
+        title="test6",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "goldair_dehumidifier",
+        },
+        options={
+            "humidifier": True,
+            "fan": True,
+            "light": True,
+            "lock": False,
+            "switch": True,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert await async_migrate_entry(hass, entry)
+
+    mock_device.async_inferred_type = mocker.AsyncMock(
+        return_value="grid_connect_usb_double_power_point"
+    )
+    mock_device.reset_mock()
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=6,
+        title="test7",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "grid_connect_usb_double_power_point",
+        },
+        options={
+            "switch_main_switch": True,
+            "switch_left_outlet": True,
+            "switch_right_outlet": True,
+        },
+    )
+    entry.add_to_hass(hass)
+    assert await async_migrate_entry(hass, entry)
+
+
+@pytest.mark.asyncio
+async def test_flow_user_init(hass, mocker):
+    """Test the initialisation of the form in the first page of the manual config flow path."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "local"}
+    )
+    expected = {
+        "data_schema": mocker.ANY,
+        "description_placeholders": mocker.ANY,
+        "errors": {},
+        "flow_id": mocker.ANY,
+        "handler": DOMAIN,
+        "step_id": "local",
+        "type": "form",
+        "last_step": mocker.ANY,
+        "preview": mocker.ANY,
+    }
+    assert expected == result
+    # Check the schema.  Simple comparison does not work since they are not
+    # the same object
+    try:
+        result["data_schema"](
+            {CONF_DEVICE_ID: "test", CONF_LOCAL_KEY: TESTKEY, CONF_HOST: "test"}
+        )
+    except vol.MultipleInvalid:
+        assert False
+    try:
+        result["data_schema"]({CONF_DEVICE_ID: "missing_some"})
+        assert False
+    except vol.MultipleInvalid:
+        pass
+
+
+@pytest.mark.asyncio
+async def test_async_test_connection_valid(hass, mocker):
+    """Test that device is returned when connection is valid."""
+    mock_device = mocker.patch(
+        "custom_components.tuya_local.config_flow.TuyaLocalDevice"
+    )
+    mock_instance = mocker.AsyncMock()
+    mock_instance.has_returned_state = True
+    mock_instance.pause = mocker.MagicMock()
+    mock_instance.resume = mocker.MagicMock()
+    mock_device.return_value = mock_instance
+    hass.data[DOMAIN] = {"deviceid": {"device": mock_instance}}
+
+    device = await config_flow.async_test_connection(
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_HOST: "hostname",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+        hass,
+    )
+    assert device == mock_instance
+    mock_instance.pause.assert_called_once()
+    mock_instance.resume.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_async_test_connection_for_subdevice_valid(hass, mocker):
+    """Test that subdevice is returned when connection is valid."""
+    mock_device = mocker.patch(
+        "custom_components.tuya_local.config_flow.TuyaLocalDevice"
+    )
+    mock_instance = mocker.AsyncMock()
+    mock_instance.has_returned_state = True
+    mock_instance.pause = mocker.MagicMock()
+    mock_instance.resume = mocker.MagicMock()
+    mock_device.return_value = mock_instance
+    hass.data[DOMAIN] = {"subdeviceid": {"device": mock_instance}}
+
+    device = await config_flow.async_test_connection(
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_HOST: "hostname",
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_DEVICE_CID: "subdeviceid",
+        },
+        hass,
+    )
+    assert device == mock_instance
+    mock_instance.pause.assert_called_once()
+    mock_instance.resume.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_async_test_connection_invalid(hass, mocker):
+    """Test that None is returned when connection is invalid."""
+    mock_device = mocker.patch(
+        "custom_components.tuya_local.config_flow.TuyaLocalDevice"
+    )
+    mock_instance = mocker.AsyncMock()
+    mock_instance.has_returned_state = False
+    mock_device.return_value = mock_instance
+    device = await config_flow.async_test_connection(
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_HOST: "hostname",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+        hass,
+    )
+    assert device is None
+
+
+@pytest.mark.asyncio
+async def test_flow_user_init_invalid_config(hass, mocker):
+    """Test errors populated when config is invalid."""
+    mocker.patch(
+        "custom_components.tuya_local.config_flow.async_test_connection",
+        return_value=None,
+    )
+    flow = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "local"}
+    )
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "badkey",
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_POLL_ONLY: False,
+        },
+    )
+    assert {"base": "connection"} == result["errors"]
+
+
+def setup_device_mock(mock, mocker, failure=False, type="test"):
+    mock_type = mocker.MagicMock()
+    mock_type.legacy_type = type
+    mock_type.config_type = type
+    mock_type.match_quality.return_value = 100
+    mock.async_possible_types = mocker.AsyncMock(
+        return_value=[mock_type] if not failure else []
+    )
+
+
+@pytest.mark.asyncio
+async def test_flow_user_init_data_valid(hass, mocker):
+    """Test we advance to the next step when connection config is valid."""
+    mock_device = mocker.MagicMock()
+    setup_device_mock(mock_device, mocker)
+    mocker.patch(
+        "custom_components.tuya_local.config_flow.async_test_connection",
+        return_value=mock_device,
+    )
+
+    flow = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "local"}
+    )
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+        },
+    )
+    assert "form" == result["type"]
+    assert "select_type" == result["step_id"]
+
+
+@pytest.mark.asyncio
+async def test_flow_select_type_init(hass, mocker):
+    """Test the initialisation of the form in the 2nd step of the config flow."""
+    mock_device = mocker.patch.object(config_flow.ConfigFlowHandler, "device")
+
+    setup_device_mock(mock_device, mocker)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "select_type"}
+    )
+    expected = {
+        "data_schema": mocker.ANY,
+        "description_placeholders": None,
+        "errors": None,
+        "flow_id": mocker.ANY,
+        "handler": DOMAIN,
+        "step_id": "select_type",
+        "type": "form",
+        "last_step": mocker.ANY,
+        "preview": mocker.ANY,
+    }
+    assert expected == result
+    # Check the schema.  Simple comparison does not work since they are not
+    # the same object
+    try:
+        result["data_schema"]({CONF_TYPE: "test"})
+    except vol.MultipleInvalid:
+        assert False
+    try:
+        result["data_schema"]({CONF_TYPE: "not_test"})
+        assert False
+    except vol.MultipleInvalid:
+        pass
+
+
+@pytest.mark.asyncio
+async def test_flow_select_type_aborts_when_no_match(hass, mocker):
+    """Test the flow aborts when an unsupported device is used."""
+    mock_device = mocker.patch.object(config_flow.ConfigFlowHandler, "device")
+    setup_device_mock(mock_device, mocker, failure=True)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "select_type"}
+    )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "not_supported"
+
+
+@pytest.mark.asyncio
+async def test_flow_select_type_data_valid(hass, mocker):
+    """Test the flow continues when valid data is supplied."""
+    mock_device = mocker.patch.object(config_flow.ConfigFlowHandler, "device")
+
+    setup_device_mock(mock_device, mocker, type="smartplugv1")
+
+    flow = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "select_type"}
+    )
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={CONF_TYPE: "smartplugv1"},
+    )
+    assert "form" == result["type"]
+    assert "choose_entities" == result["step_id"]
+
+
+@pytest.mark.asyncio
+async def test_flow_choose_entities_init(hass, mocker):
+    """Test the initialisation of the form in the 3rd step of the config flow."""
+
+    mocker.patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "smartplugv1"})
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "choose_entities"}
+    )
+
+    expected = {
+        "data_schema": mocker.ANY,
+        "description_placeholders": None,
+        "errors": None,
+        "flow_id": mocker.ANY,
+        "handler": DOMAIN,
+        "step_id": "choose_entities",
+        "type": "form",
+        "last_step": mocker.ANY,
+        "preview": mocker.ANY,
+    }
+    assert expected == result
+    # Check the schema.  Simple comparison does not work since they are not
+    # the same object
+    try:
+        result["data_schema"]({CONF_NAME: "test"})
+    except vol.MultipleInvalid:
+        assert False
+    try:
+        result["data_schema"]({"climate": True})
+        assert False
+    except vol.MultipleInvalid:
+        pass
+
+
+@pytest.mark.asyncio
+async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup, mocker):
+    """Test the flow ends when data is valid."""
+
+    mocker.patch.dict(
+        config_flow.ConfigFlowHandler.data,
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_HOST: "hostname",
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "kogan_kahtp_heater",
+            CONF_DEVICE_CID: None,
+        },
+    )
+    flow = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "choose_entities"}
+    )
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={
+            CONF_NAME: "test",
+        },
+    )
+    expected = {
+        "version": 13,
+        "minor_version": mocker.ANY,
+        "context": {"source": "choose_entities"},
+        "type": FlowResultType.CREATE_ENTRY,
+        "flow_id": mocker.ANY,
+        "handler": DOMAIN,
+        "title": "test",
+        "description": None,
+        "description_placeholders": None,
+        "result": mocker.ANY,
+        "subentries": (),
+        "options": {},
+        "data": {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "kogan_kahtp_heater",
+            CONF_DEVICE_CID: None,
+        },
+    }
+    assert expected == result
+
+
+@pytest.mark.asyncio
+async def test_options_flow_init(hass, bypass_data_fetch):
+    """Test config flow options."""
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=13,
+        unique_id="uniqueid",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_NAME: "test",
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "smartplugv1",
+            CONF_DEVICE_CID: "",
+        },
+    )
+    config_entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    # show initial form
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert "form" == result["type"]
+    assert "user" == result["step_id"]
+    assert {} == result["errors"]
+    assert result["data_schema"](
+        {
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+        }
+    )
+
+
+@pytest.mark.asyncio
+async def test_options_flow_modifies_config(hass, bypass_setup, mocker):
+    mock_device = mocker.MagicMock()
+    mocker.patch(
+        "custom_components.tuya_local.config_flow.async_test_connection",
+        return_value=mock_device,
+    )
+
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=13,
+        unique_id="uniqueid",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_NAME: "test",
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "ble_pt216_temp_humidity",
+            CONF_DEVICE_CID: "subdeviceid",
+        },
+    )
+    config_entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+    # show initial form
+    form = await hass.config_entries.options.async_init(config_entry.entry_id)
+    # submit updated config
+    result = await hass.config_entries.options.async_configure(
+        form["flow_id"],
+        user_input={
+            CONF_HOST: "new_hostname",
+            CONF_LOCAL_KEY: "new_key",
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: 3.3,
+        },
+    )
+    expected = {
+        CONF_HOST: "new_hostname",
+        CONF_LOCAL_KEY: "new_key",
+        CONF_POLL_ONLY: False,
+        CONF_PROTOCOL_VERSION: 3.3,
+    }
+    assert "create_entry" == result["type"]
+    assert "" == result["title"]
+    assert expected == result["data"]
+
+
+@pytest.mark.asyncio
+async def test_options_flow_fails_when_connection_fails(
+    hass, bypass_data_fetch, mocker
+):
+    mocker.patch(
+        "custom_components.tuya_local.config_flow.async_test_connection",
+        return_value=None,
+    )
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=13,
+        unique_id="uniqueid",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_NAME: "test",
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "smartplugv1",
+            CONF_DEVICE_CID: "",
+        },
+    )
+    config_entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+    # show initial form
+    form = await hass.config_entries.options.async_init(config_entry.entry_id)
+    # submit updated config
+    result = await hass.config_entries.options.async_configure(
+        form["flow_id"],
+        user_input={
+            CONF_HOST: "new_hostname",
+            CONF_LOCAL_KEY: "new_key",
+        },
+    )
+    assert "form" == result["type"]
+    assert "user" == result["step_id"]
+    assert {"base": "connection"} == result["errors"]
+
+
+@pytest.mark.asyncio
+async def test_options_flow_fails_when_config_is_missing(hass, mocker):
+    mock_device = mocker.MagicMock()
+    mocker.patch(
+        "custom_components.tuya_local.config_flow.async_test_connection",
+        return_value=mock_device,
+    )
+
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=13,
+        unique_id="uniqueid",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_NAME: "test",
+            CONF_POLL_ONLY: False,
+            CONF_PROTOCOL_VERSION: "auto",
+            CONF_TYPE: "non_existing",
+        },
+    )
+    config_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+    # show initial form
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert result["type"] == "abort"
+    assert result["reason"] == "not_supported"
+
+
+def test_migration_gets_correct_device_id():
+    """Test that migration gets the correct device id."""
+    # Normal device
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=1,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "auto",
+        },
+    )
+    assert get_device_unique_id(entry) == "deviceid"

+ 637 - 618
tests/test_device.py

@@ -1,635 +1,654 @@
+import pytest
+
 from time import time
 from time import time
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import ANY, AsyncMock, Mock, call, patch
 
 
-from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
+# from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
 
 
 from custom_components.tuya_local.device import TuyaLocalDevice
 from custom_components.tuya_local.device import TuyaLocalDevice
 
 
 from .const import EUROM_600_HEATER_PAYLOAD
 from .const import EUROM_600_HEATER_PAYLOAD
 
 
 
 
-class TestDevice(IsolatedAsyncioTestCase):
-    def setUp(self):
-        device_patcher = patch("tinytuya.Device")
-        self.addCleanup(device_patcher.stop)
-        self.mock_api = device_patcher.start()
-        self.mock_api().parent = None
-
-        hass_patcher = patch("homeassistant.core.HomeAssistant")
-        self.addCleanup(hass_patcher.stop)
-        self.hass = hass_patcher.start()
-        self.hass().is_running = True
-        self.hass().is_stopping = False
-        self.hass().data = {"tuya_local": {}}
-
-        def job(func, *args):
-            print(f"{args}")
-            return func(*args)
-
-        self.hass().async_add_executor_job = AsyncMock()
-        self.hass().async_add_executor_job.side_effect = job
-
-        sleep_patcher = patch("asyncio.sleep")
-        self.addCleanup(sleep_patcher.stop)
-        self.mock_sleep = sleep_patcher.start()
-
-        lock_patcher = patch("custom_components.tuya_local.device.Lock")
-        self.addCleanup(lock_patcher.stop)
-        self.mock_lock = lock_patcher.start()
-
-        self.subject = TuyaLocalDevice(
-            "Some name",
-            "some_dev_id",
-            "some.ip.address",
-            "some_local_key",
-            "auto",
-            None,
-            self.hass(),
-        )
-        # For most tests we want the protocol working
-        self.subject._api_protocol_version_index = 0
-        self.subject._api_protocol_working = True
-        self.subject._protocol_configured = "auto"
-
-    def test_configures_tinytuya_correctly(self):
-        self.mock_api.assert_called_with(
-            "some_dev_id", "some.ip.address", "some_local_key"
-        )
-        self.assertIs(self.subject._api, self.mock_api())
-
-    def test_name(self):
-        """Returns the name given at instantiation."""
-        self.assertEqual(self.subject.name, "Some name")
-
-    def test_unique_id(self):
-        """Returns the unique ID presented by the API class."""
-        self.assertIs(self.subject.unique_id, self.mock_api().id)
-
-    def test_device_info(self):
-        """Returns generic info plus the unique ID for categorisation."""
-        self.assertEqual(
-            self.subject.device_info,
-            {
-                "identifiers": {("tuya_local", self.mock_api().id)},
-                "name": "Some name",
-                "manufacturer": "Tuya",
-            },
-        )
-
-    def test_has_returned_state(self):
-        """Returns True if the device has returned its state."""
-        self.subject._cached_state = EUROM_600_HEATER_PAYLOAD
-        self.assertTrue(self.subject.has_returned_state)
-
-        self.subject._cached_state = {"updated_at": 0}
-        self.assertFalse(self.subject.has_returned_state)
-
-    async def test_refreshes_state_if_no_cached_state_exists(self):
-        self.subject._cached_state = {}
-        self.subject.async_refresh = AsyncMock()
-
-        await self.subject.async_inferred_type()
-
-        self.subject.async_refresh.assert_awaited()
-
-    async def test_detection_returns_none_when_device_type_not_detected(self):
-        self.subject._cached_state = {"192": False, "updated_at": time()}
-        self.assertEqual(await self.subject.async_inferred_type(), None)
-
-    async def test_refresh_retries_up_to_eleven_times(self):
-        self.subject._api_protocol_working = False
-        self.mock_api().status.side_effect = [
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            {"dps": {"1": False}},
-        ]
+@pytest.fixture
+def mock_api(mocker):
+    mock = mocker.patch("tinytuya.Device")
+    mock.parent = None
+    yield mock
 
 
-        await self.subject.async_refresh()
 
 
-        self.assertEqual(self.mock_api().status.call_count, 11)
-        self.assertEqual(self.subject._cached_state["1"], False)
+@pytest.fixture
+def patched_hass(hass, mocker):
+    hass.is_running = True
+    hass.is_stopping = False
+    hass.data = {"tuya_local": {}}
 
 
-    async def test_refresh_clears_cache_after_allowed_failures(self):
-        self.subject._cached_state = {"1": True}
-        self.subject._pending_updates = {
-            "1": {"value": False, "updated_at": time(), "sent": True}
-        }
-        self.mock_api().status.side_effect = [
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-        ]
+    async def job(func, *args):
+        print(f"{args}")
+        return func(*args)
 
 
-        await self.subject.async_refresh()
-
-        self.assertEqual(self.mock_api().status.call_count, 3)
-        self.assertEqual(self.subject._cached_state, {"updated_at": 0})
-        self.assertEqual(self.subject._pending_updates, {})
-
-    async def test_api_protocol_version_is_rotated_with_each_failure(self):
-        self.subject._api_protocol_version_index = None
-        self.subject._api_protocol_working = False
-        self.mock_api().status.side_effect = [
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
+    mocker.patch.object(hass, "async_add_executor_job", side_effect=job)
+    mocker.patch.object(hass, "async_create_task")
+    return hass
+
+
+@pytest.fixture
+def subject(patched_hass, mock_api, mocker):
+    subject = TuyaLocalDevice(
+        "Some name",
+        "some_dev_id",
+        "some.ip.address",
+        "some_local_key",
+        "auto",
+        None,
+        patched_hass,
+    )
+    # For most tests we want the protocol working
+    subject._api_protocol_version_index = 0
+    subject._api_protocol_working = True
+    subject._protocol_configured = "auto"
+    return subject
+
+
+def test_name(subject):
+    """Returns the name given at instantiation."""
+    assert subject.name == "Some name"
+
+
+def test_unique_id(subject, mock_api):
+    """Returns the unique ID presented by the API class."""
+    assert subject.unique_id is mock_api().id
+
+
+def test_device_info(subject, mock_api):
+    """Returns generic info plus the unique ID for categorisation."""
+    assert subject.device_info == {
+        "identifiers": {("tuya_local", mock_api().id)},
+        "name": "Some name",
+        "manufacturer": "Tuya",
+    }
+
+
+def test_has_returned_state(subject):
+    """Returns True if the device has returned its state."""
+    subject._cached_state = EUROM_600_HEATER_PAYLOAD
+    assert subject.has_returned_state
+
+    subject._cached_state = {"updated_at": 0}
+    assert not subject.has_returned_state
+
+
+@pytest.mark.asyncio
+async def test_refreshes_state_if_no_cached_state_exists(subject, mocker):
+    subject._cached_state = {}
+    subject.async_refresh = mocker.AsyncMock()
+
+    await subject.async_inferred_type()
+
+    subject.async_refresh.assert_awaited()
+
+
+@pytest.mark.asyncio
+async def test_detection_returns_none_when_device_type_not_detected(subject):
+    subject._cached_state = {"192": False, "updated_at": time()}
+    assert await subject.async_inferred_type() is None
+
+
+@pytest.mark.asyncio
+async def test_refresh_retries_up_to_eleven_times(subject, mock_api):
+    subject._api_protocol_working = False
+    mock_api().status.side_effect = [
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        {"dps": {"1": False}},
+    ]
+
+    await subject.async_refresh()
+
+    assert mock_api().status.call_count == 11
+    assert subject._cached_state["1"] is False
+
+
+@pytest.mark.asyncio
+async def test_refresh_clears_cache_after_allowed_failures(subject, mock_api):
+    subject._cached_state = {"1": True}
+    subject._pending_updates = {
+        "1": {"value": False, "updated_at": time(), "sent": True}
+    }
+    mock_api().status.side_effect = [
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+    ]
+
+    await subject.async_refresh()
+
+    assert mock_api().status.call_count == 3
+    assert subject._cached_state == {"updated_at": 0}
+    assert subject._pending_updates == {}
+
+
+@pytest.mark.asyncio
+async def test_api_protocol_version_is_rotated_with_each_failure(
+    subject, mock_api, mocker
+):
+    subject._api_protocol_version_index = None
+    subject._api_protocol_working = False
+    mock_api().status.side_effect = [
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+    ]
+    await subject.async_refresh()
+
+    mock_api().set_version.assert_has_calls(
+        [
+            mocker.call(3.1),
+            mocker.call(3.2),
+            mocker.call(3.4),
+            mocker.call(3.5),
+            mocker.call(3.3),
+            mocker.call(3.3),
+            mocker.call(3.1),
         ]
         ]
-        await self.subject.async_refresh()
-
-        self.mock_api().set_version.assert_has_calls(
-            [
-                call(3.1),
-                call(3.2),
-                call(3.4),
-                call(3.5),
-                call(3.3),
-                call(3.3),
-                call(3.1),
-            ]
-        )
-
-    async def test_api_protocol_version_is_stable_once_successful(self):
-        self.subject._api_protocol_version_index = None
-        self.subject._api_protocol_working = False
-        self.mock_api().status.side_effect = [
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            {"dps": {"1": False}},
-            {"dps": {"1": False}},
-            Exception("Error"),
-            Exception("Error"),
-            {"dps": {"1": False}},
+    )
+
+
+@pytest.mark.asyncio
+async def test_api_protocol_version_is_stable_once_successful(
+    subject, mock_api, mocker
+):
+    subject._api_protocol_version_index = None
+    subject._api_protocol_working = False
+    mock_api().status.side_effect = [
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        {"dps": {"1": False}},
+        {"dps": {"1": False}},
+        Exception("Error"),
+        Exception("Error"),
+        {"dps": {"1": False}},
+    ]
+
+    await subject.async_refresh()
+    assert subject._api_protocol_version_index == 3
+    assert subject._api_protocol_working
+    await subject.async_refresh()
+    assert subject._api_protocol_version_index == 3
+    await subject.async_refresh()
+    assert subject._api_protocol_version_index == 3
+
+    mock_api().set_version.assert_has_calls(
+        [
+            mocker.call(3.1),
+            mocker.call(3.2),
+            mocker.call(3.4),
         ]
         ]
+    )
 
 
-        await self.subject.async_refresh()
-        self.assertEqual(self.subject._api_protocol_version_index, 3)
-        self.assertTrue(self.subject._api_protocol_working)
-        await self.subject.async_refresh()
-        self.assertEqual(self.subject._api_protocol_version_index, 3)
-        await self.subject.async_refresh()
-        self.assertEqual(self.subject._api_protocol_version_index, 3)
-
-        self.mock_api().set_version.assert_has_calls(
-            [
-                call(3.1),
-                call(3.2),
-                call(3.4),
-            ]
-        )
-
-    async def test_api_protocol_version_is_not_rotated_when_not_auto(self):
-        # Set up preconditions for the test
-
-        self.subject._protocol_configured = 3.4
-        self.subject._api_protocol_version_index = None
-        self.subject._api_protocol_working = False
-        self.mock_api().status.side_effect = [
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            {"dps": {"1": False}},
-            {"dps": {"1": False}},
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            Exception("Error"),
-            {"dps": {"1": False}},
-        ]
-        await self.subject._rotate_api_protocol_version()
-        self.mock_api().set_version.assert_called_once_with(3.4)
-        self.mock_api().set_version.reset_mock()
-
-        await self.subject.async_refresh()
-        self.assertEqual(self.subject._api_protocol_version_index, 3)
-        await self.subject.async_refresh()
-        self.assertEqual(self.subject._api_protocol_version_index, 3)
-        await self.subject.async_refresh()
-        self.assertEqual(self.subject._api_protocol_version_index, 3)
-
-    def test_reset_cached_state_clears_cached_state_and_pending_updates(self):
-        self.subject._cached_state = {"1": True, "updated_at": time()}
-        self.subject._pending_updates = {
-            "1": {"value": False, "updated_at": time(), "sent": True}
-        }
-
-        self.subject._reset_cached_state()
-
-        self.assertEqual(self.subject._cached_state, {"updated_at": 0})
-        self.assertEqual(self.subject._pending_updates, {})
-
-    def test_get_property_returns_value_from_cached_state(self):
-        self.subject._cached_state = {"1": True}
-        self.assertEqual(self.subject.get_property("1"), True)
-
-    def test_get_property_returns_pending_update_value(self):
-        self.subject._pending_updates = {
-            "1": {"value": False, "updated_at": time() - 4, "sent": True}
-        }
-        self.assertEqual(self.subject.get_property("1"), False)
-
-    def test_pending_update_value_overrides_cached_value(self):
-        self.subject._cached_state = {"1": True}
-        self.subject._pending_updates = {
-            "1": {"value": False, "updated_at": time() - 4, "sent": True}
-        }
-
-        self.assertEqual(self.subject.get_property("1"), False)
-
-    def test_expired_pending_update_value_does_not_override_cached_value(self):
-        self.subject._cached_state = {"1": True}
-        self.subject._pending_updates = {
-            "1": {"value": False, "updated_at": time() - 5, "sent": True}
-        }
-
-        self.assertEqual(self.subject.get_property("1"), True)
-
-    def test_get_property_returns_none_when_value_does_not_exist(self):
-        self.subject._cached_state = {"1": True}
-        self.assertIs(self.subject.get_property("2"), None)
-
-    async def test_async_set_property_sends_to_api(self):
-        await self.subject.async_set_property("1", False)
-
-        self.mock_api().set_multiple_values.assert_called_once()
-
-    async def test_set_property_immediately_stores_pending_updates(self):
-        self.subject._cached_state = {"1": True}
-        await self.subject.async_set_property("1", False)
-        self.assertFalse(self.subject.get_property("1"))
-
-    async def test_set_properties_takes_no_action_when_nothing_provided(self):
-        with patch("asyncio.sleep") as mock:
-            await self.subject.async_set_properties({})
-            mock.assert_not_called()
-
-    def test_anticipate_property_value_updates_cached_state(self):
-        self.subject._cached_state = {"1": True}
-        self.subject.anticipate_property_value("1", False)
-        self.assertEqual(self.subject._cached_state["1"], False)
-
-    def test_get_key_for_value_returns_key_from_object_matching_value(self):
-        obj = {"key1": "value1", "key2": "value2"}
-
-        self.assertEqual(
-            TuyaLocalDevice.get_key_for_value(obj, "value1"),
-            "key1",
-        )
-        self.assertEqual(
-            TuyaLocalDevice.get_key_for_value(obj, "value2"),
-            "key2",
-        )
-
-    def test_get_key_for_value_returns_fallback_when_value_not_found(self):
-        obj = {"key1": "value1", "key2": "value2"}
-        self.assertEqual(
-            TuyaLocalDevice.get_key_for_value(obj, "value3", fallback="fb"),
-            "fb",
-        )
-
-    def test_refresh_cached_state(self):
-        # set up preconditions
-        self.mock_api().status.return_value = {"dps": {"1": "CHANGED"}}
-        self.subject._cached_state = {"1": "UNCHANGED", "updated_at": 123}
-
-        # call the function under test
-        self.subject._refresh_cached_state()
-
-        # Did it call the API as expected?
-        self.mock_api().status.assert_called_once()
-        # Did it update the cached state?
-        self.assertDictEqual(
-            self.subject._cached_state,
-            {"1": "CHANGED"} | self.subject._cached_state,
-        )
-        # Did it update the timestamp on the cached state?
-        self.assertAlmostEqual(
-            self.subject._cached_state["updated_at"],
-            time(),
-            delta=2,
-        )
-
-    def test_set_values(self):
-        # set up preconditions
-        self.subject._pending_updates = {
-            "1": {"value": "sample", "updated_at": time() - 2, "sent": False},
-        }
-
-        # call the function under test
-        self.subject._set_values({"1": "sample"})
-
-        # did it send what it was asked?
-        self.mock_api().set_multiple_values.assert_called_once_with(
-            {"1": "sample"}, nowait=True
-        )
-        # did it mark the pending updates as sent?
-        self.assertTrue(self.subject._pending_updates["1"]["sent"])
-        # did it update the time on the pending updates?
-        self.assertAlmostEqual(
-            self.subject._pending_updates["1"]["updated_at"],
-            time(),
-            delta=2,
-        )
-        # did it lock and unlock when sending
-        self.subject._lock.acquire.assert_called_once()
-        self.subject._lock.release.assert_called_once()
-
-    def test_pending_updates_cleared_on_receipt(self):
-        # Set up the preconditions
-        now = time()
-        self.subject._pending_updates = {
-            "1": {"value": True, "updated_at": now, "sent": True},
-            "2": {"value": True, "updated_at": now, "sent": False},  # unsent
-            "3": {"value": True, "updated_at": now, "sent": True},  # unmatched
-            "4": {"value": True, "updated_at": now, "sent": True},  # not received
-        }
-        self.subject._remove_properties_from_pending_updates(
-            {"1": True, "2": True, "3": False}
-        )
-        self.assertDictEqual(
-            self.subject._pending_updates,
-            {
-                "2": {"value": True, "updated_at": now, "sent": False},
-                "3": {"value": True, "updated_at": now, "sent": True},
-                "4": {"value": True, "updated_at": now, "sent": True},
-            },
-        )
-
-    def test_actually_start(self):
-        # Set up the preconditions
-        self.subject.receive_loop = Mock()
-        self.subject.receive_loop.return_value = "LOOP"
-        self.hass().bus.async_listen_once.return_value = "LISTENER"
-        self.hass().async_create_task = Mock()
-        self.subject._running = False
-
-        # run the function under test
-        self.subject.actually_start()
-
-        # did it register a listener for EVENT_HOMEASSISTANT_STOP?
-        self.hass().bus.async_listen_once.assert_called_once_with(
-            EVENT_HOMEASSISTANT_STOP, self.subject.async_stop
-        )
-        self.assertEqual(self.subject._shutdown_listener, "LISTENER")
-        # did it set the running flag?
-        self.assertTrue(self.subject._running)
-        # did it schedule the loop?
-        # self.hass().async_create_task.assert_called_once()
-
-    def test_start_starts_when_ha_running(self):
-        # Set up preconditions
-        self.hass().is_running = True
-        listener = Mock()
-        self.subject._startup_listener = listener
-        self.subject.actually_start = Mock()
-
-        # Call the function under test
-        self.subject.start()
-
-        # Did it actually start?
-        self.subject.actually_start.assert_called_once()
-        # Did it cancel the startup listener?
-        self.assertIsNone(self.subject._startup_listener)
-        listener.assert_called_once()
-
-    def test_start_schedules_for_later_when_ha_starting(self):
-        # Set up preconditions
-        self.hass().is_running = False
-        self.hass().bus.async_listen_once.return_value = "LISTENER"
-        self.subject.actually_start = Mock()
-
-        # Call the function under test
-        self.subject.start()
-
-        # Did it avoid actually starting?
-        self.subject.actually_start.assert_not_called()
-        # Did it register a listener?
-        self.assertEqual(self.subject._startup_listener, "LISTENER")
-        self.hass().bus.async_listen_once.assert_called_once_with(
-            EVENT_HOMEASSISTANT_STARTED, self.subject.actually_start
-        )
-
-    def test_start_does_nothing_when_ha_stopping(self):
-        # Set up preconditions
-        self.hass().is_running = True
-        self.hass().is_stopping = True
-        self.subject.actually_start = Mock()
-
-        # Call the function under test
-        self.subject.start()
-
-        # Did it avoid actually starting?
-        self.subject.actually_start.assert_not_called()
-        # Did it avoid registering a listener?
-        self.hass().bus.async_listen_once.assert_not_called()
-        self.assertIsNone(self.subject._startup_listener)
-
-    async def test_async_stop(self):
-        # Set up preconditions
-        listener = Mock()
-        self.subject._refresh_task = None
-        self.subject._shutdown_listener = listener
-        self.subject._children = [1, 2, 3]
-
-        # Call the function under test
-        await self.subject.async_stop()
-
-        # Shutdown listener doesn't get cancelled as HA does that
-        listener.assert_not_called()
-        # Were the child entities cleared?
-        self.assertEqual(self.subject._children, [])
-        # Did it wait for the refresh task to finish then clear it?
-        # This doesn't work because AsyncMock only mocks awaitable method calls
-        # but we want an awaitable object
-        # refresh.assert_awaited_once()
-        self.assertIsNone(self.subject._refresh_task)
-
-    async def test_async_stop_when_not_running(self):
-        # Set up preconditions
-        self._refresh_task = None
-        self.subject._shutdown_listener = None
-        self.subject._children = []
-
-        # Call the function under test
-        await self.subject.async_stop()
-
-        # Was the shutdown listener left empty?
-        self.assertIsNone(self.subject._shutdown_listener)
-        # Were the child entities cleared?
-        self.assertEqual(self.subject._children, [])
-        # Was the refresh task left empty?
-        self.assertIsNone(self.subject._refresh_task)
-
-    def test_register_first_entity_ha_running(self):
-        # Set up preconditions
-        self.subject._children = []
-        self.subject._running = False
-        self.subject._startup_listener = None
-        self.subject.start = Mock()
-        entity = AsyncMock()
-        entity._config = Mock()
-        entity._config.dps.return_value = []
-        # despite the name, the below HA function is not async and does not need to be awaited
-        entity.async_schedule_update_ha_state = Mock()
-
-        # Call the function under test
-        self.subject.register_entity(entity)
-
-        # Was the entity added to the list?
-        self.assertEqual(self.subject._children, [entity])
-
-        # Did we start the loop?
-        self.subject.start.assert_called_once()
-
-    def test_register_subsequent_entity_ha_running(self):
-        # Set up preconditions
-        first = AsyncMock()
-        second = AsyncMock()
-        second._config = Mock()
-        second._config.dps.return_value = []
-        self.subject._children = [first]
-        self.subject._running = True
-        self.subject._startup_listener = None
-        self.subject.start = Mock()
-
-        # Call the function under test
-        self.subject.register_entity(second)
-
-        # Was the entity added to the list?
-        self.assertCountEqual(self.subject._children, [first, second])
-
-        # Did we avoid restarting the loop?
-        self.subject.start.assert_not_called()
-
-    def test_register_subsequent_entity_ha_starting(self):
-        # Set up preconditions
-        first = AsyncMock()
-        second = AsyncMock()
-        second._config = Mock()
-        second._config.dps.return_value = []
-        self.subject._children = [first]
-        self.subject._running = False
-        self.subject._startup_listener = Mock()
-        self.subject.start = Mock()
-
-        # Call the function under test
-        self.subject.register_entity(second)
-
-        # Was the entity added to the list?
-        self.assertCountEqual(self.subject._children, [first, second])
-        # Did we avoid restarting the loop?
-        self.subject.start.assert_not_called()
-
-    async def test_unregister_one_of_many_entities(self):
-        # Set up preconditions
-        self.subject._children = ["First", "Second"]
-        self.subject.async_stop = AsyncMock()
-
-        # Call the function under test
-        await self.subject.async_unregister_entity("First")
-
-        # Was the entity removed from the list?
-        self.assertCountEqual(self.subject._children, ["Second"])
-        # Is the loop still running?
-        self.subject.async_stop.assert_not_called()
-
-    async def test_unregister_last_entity(self):
-        # Set up preconditions
-        self.subject._children = ["Last"]
-        self.subject.async_stop = AsyncMock()
-
-        # Call the function under test
-        await self.subject.async_unregister_entity("Last")
-
-        # Was the entity removed from the list?
-        self.assertEqual(self.subject._children, [])
-        # Was the loop stopped?
-        self.subject.async_stop.assert_called_once()
-
-    async def test_async_receive(self):
-        # Set up preconditions
-        self.mock_api().status.return_value = {"dps": {"1": "INIT", "2": 2}}
-        self.mock_api().receive.return_value = {"1": "UPDATED"}
-        self.subject._running = True
-        self.subject._cached_state = {"updated_at": 0}
-        # Call the function under test
-        print("starting test loop...")
-        loop = self.subject.async_receive()
-        print("getting first iteration...")
-        result = await loop.__anext__()
 
 
-        # Check that the loop was started, but without persistent connection
-        # since there was no state returned yet and it might need to negotiate
-        # version.
-        self.mock_api().set_socketPersistent.assert_called_once_with(False)
-        # Check that a full poll was done
-        self.mock_api().status.assert_called_once()
-        self.assertDictEqual(result, {"1": "INIT", "2": 2, "full_poll": ANY})
-        # Prepare for next round
-        self.subject._cached_state = self.subject._cached_state | result
-        self.mock_api().set_socketPersistent.reset_mock()
-        self.mock_api().status.reset_mock()
-        self.subject._cached_state["updated_at"] = time()
-
-        # Call the function under test
-        print("getting second iteration...")
-        result = await loop.__anext__()
+@pytest.mark.asyncio
+async def test_api_protocol_version_is_not_rotated_when_not_auto(subject, mock_api):
+    # Set up preconditions for the test
+
+    subject._protocol_configured = 3.4
+    subject._api_protocol_version_index = None
+    subject._api_protocol_working = False
+    mock_api().status.side_effect = [
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        {"dps": {"1": False}},
+        {"dps": {"1": False}},
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        Exception("Error"),
+        {"dps": {"1": False}},
+    ]
+    await subject._rotate_api_protocol_version()
+    mock_api().set_version.assert_called_once_with(3.4)
+    mock_api().set_version.reset_mock()
+
+    await subject.async_refresh()
+    assert subject._api_protocol_version_index == 3
+    await subject.async_refresh()
+    assert subject._api_protocol_version_index == 3
+    await subject.async_refresh()
+    assert subject._api_protocol_version_index == 3
+
+
+def test_reset_cached_state_clears_cached_state_and_pending_updates(subject):
+    subject._cached_state = {"1": True, "updated_at": time()}
+    subject._pending_updates = {
+        "1": {"value": False, "updated_at": time(), "sent": True}
+    }
+
+    subject._reset_cached_state()
+
+    assert subject._cached_state == {"updated_at": 0}
+    assert subject._pending_updates == {}
+
+
+def test_get_property_returns_value_from_cached_state(subject):
+    subject._cached_state = {"1": True}
+    assert subject.get_property("1") is True
+
+
+def test_get_property_returns_pending_update_value(subject):
+    subject._pending_updates = {
+        "1": {"value": False, "updated_at": time() - 4, "sent": True}
+    }
+    assert subject.get_property("1") is False
+
+
+def test_pending_update_value_overrides_cached_value(subject):
+    subject._cached_state = {"1": True}
+    subject._pending_updates = {
+        "1": {"value": False, "updated_at": time() - 4, "sent": True}
+    }
+
+    assert subject.get_property("1") is False
+
 
 
-        # Check that a heartbeat poll was done
-        self.mock_api().status.assert_not_called()
-        self.mock_api().heartbeat.assert_called_once()
-        self.mock_api().receive.assert_called_once()
-        self.assertDictEqual(result, {"1": "UPDATED", "full_poll": ANY})
-        # Check that the connection was made persistent now that data has been
-        # returned
-        self.mock_api().set_socketPersistent.assert_called_once_with(True)
-        # Prepare for next iteration
-        self.subject._running = False
-        self.mock_api().set_socketPersistent.reset_mock()
-
-        # Call the function under test
-        print("getting last iteration...")
-        try:
-            result = await loop.__anext__()
-            self.fail("Should have raised an exception to quit the loop")
-        # Check that the loop terminated
-        except StopAsyncIteration:
-            pass
-        self.mock_api().set_socketPersistent.assert_called_once_with(False)
-
-    def test_should_poll(self):
-        self.subject._cached_state = {"1": "sample", "updated_at": time()}
-        self.subject._poll_only = False
-        self.subject._temporary_poll = False
-
-        # Test temporary poll via pause/resume
-        self.assertFalse(self.subject.should_poll)
-        self.subject.pause()
-        self.assertTrue(self.subject.should_poll)
-        self.subject.resume()
-        self.assertFalse(self.subject.should_poll)
-
-        # Test configured polling
-        self.subject._poll_only = True
-        self.assertTrue(self.subject.should_poll)
-        self.subject._poll_only = False
-
-        # Test initial polling
-        self.subject._cached_state = {}
-        self.assertTrue(self.subject.should_poll)
+def test_expired_pending_update_value_does_not_override_cached_value(subject):
+    subject._cached_state = {"1": True}
+    subject._pending_updates = {
+        "1": {"value": False, "updated_at": time() - 5, "sent": True}
+    }
+
+    assert subject.get_property("1") is True
+
+
+def test_get_property_returns_none_when_value_does_not_exist(subject):
+    subject._cached_state = {"1": True}
+    assert subject.get_property("2") is None
+
+
+@pytest.mark.asyncio
+async def test_async_set_property_sends_to_api(subject, mock_api):
+    await subject.async_set_property("1", False)
+
+    mock_api().set_multiple_values.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_set_property_immediately_stores_pending_updates(subject):
+    subject._cached_state = {"1": True}
+    await subject.async_set_property("1", False)
+    assert not subject.get_property("1")
+
+
+@pytest.mark.asyncio
+async def test_set_properties_takes_no_action_when_nothing_provided(subject, mocker):
+    mock = mocker.patch("asyncio.sleep")
+    await subject.async_set_properties({})
+    mock.assert_not_called()
+
+
+def test_anticipate_property_value_updates_cached_state(subject):
+    subject._cached_state = {"1": True}
+    subject.anticipate_property_value("1", False)
+    assert subject._cached_state["1"] is False
+
+
+def test_get_key_for_value_returns_key_from_object_matching_value(subject):
+    obj = {"key1": "value1", "key2": "value2"}
+
+    assert TuyaLocalDevice.get_key_for_value(obj, "value1") == "key1"
+    assert TuyaLocalDevice.get_key_for_value(obj, "value2") == "key2"
+
+
+def test_get_key_for_value_returns_fallback_when_value_not_found(subject):
+    obj = {"key1": "value1", "key2": "value2"}
+    assert TuyaLocalDevice.get_key_for_value(obj, "value3", fallback="fb") == "fb"
+
+
+def test_refresh_cached_state(subject, mock_api):
+    # set up preconditions
+    mock_api().status.return_value = {"dps": {"1": "CHANGED"}}
+    subject._cached_state = {"1": "UNCHANGED", "updated_at": 123}
+
+    # call the function under test
+    subject._refresh_cached_state()
+
+    # Did it call the API as expected?
+    mock_api().status.assert_called_once()
+    # Did it update the cached state?
+    assert subject._cached_state == {"1": "CHANGED"} | subject._cached_state
+    # Did it update the timestamp on the cached state?
+    assert subject._cached_state["updated_at"] == pytest.approx(time(), abs=2)
+
+
+def test_set_values(subject, mock_api):
+    # set up preconditions
+    subject._pending_updates = {
+        "1": {"value": "sample", "updated_at": time() - 2, "sent": False},
+    }
+
+    # call the function under test
+    subject._set_values({"1": "sample"})
+
+    # did it send what it was asked?
+    mock_api().set_multiple_values.assert_called_once_with({"1": "sample"}, nowait=True)
+    # did it mark the pending updates as sent?
+    assert subject._pending_updates["1"]["sent"]
+    # did it update the time on the pending updates?
+    assert subject._pending_updates["1"]["updated_at"] == pytest.approx(time(), abs=2)
+    # did it lock and unlock when sending
+    # subject._lock.acquire.assert_called_once()
+    # subject._lock.release.assert_called_once()
+
+
+def test_pending_updates_cleared_on_receipt(subject):
+    # Set up the preconditions
+    now = time()
+    subject._pending_updates = {
+        "1": {"value": True, "updated_at": now, "sent": True},
+        "2": {"value": True, "updated_at": now, "sent": False},  # unsent
+        "3": {"value": True, "updated_at": now, "sent": True},  # unmatched
+        "4": {"value": True, "updated_at": now, "sent": True},  # not received
+    }
+    subject._remove_properties_from_pending_updates({"1": True, "2": True, "3": False})
+    assert subject._pending_updates == {
+        "2": {"value": True, "updated_at": now, "sent": False},
+        "3": {"value": True, "updated_at": now, "sent": True},
+        "4": {"value": True, "updated_at": now, "sent": True},
+    }
+
+
+def test_actually_start(subject, mocker, patched_hass):
+    # Set up the preconditions
+    mocker.patch.object(subject, "receive_loop", return_value="LOOP")
+    mocker.patch.object(subject, "_refresh_task", new=mocker.AsyncMock)
+    subject._running = False
+    mocker.patch.object(patched_hass, "async_create_task")
+    # patched_hass.async_create_task = mocker.MagicMock()
+    # patched_hass.bus.async_listen_once = mocker.AsyncMock()
+    # patched_hass.bus.async_listen_once.return_value = "LISTENER"
+    # run the function under test
+    subject.actually_start()
+
+    # did it register a listener for EVENT_HOMEASSISTANT_STOP?
+    # patched_hass.bus.async_listen_once.assert_called_once_with(
+    #     EVENT_HOMEASSISTANT_STOP, subject.async_stop
+    # )
+    # assert subject._shutdown_listener == "LISTENER"
+    # did it set the running flag?
+    assert subject._running
+    # did it schedule the loop?
+    # task.assert_called_once()
+
+
+def test_start_starts_when_ha_running(subject, patched_hass, mocker):
+    # Set up preconditions
+    patched_hass.is_running = True
+    listener = mocker.MagicMock()
+    subject._startup_listener = listener
+    subject.actually_start = mocker.MagicMock()
+
+    # Call the function under test
+    subject.start()
+
+    # Did it actually start?
+    subject.actually_start.assert_called_once()
+    # Did it cancel the startup listener?
+    assert subject._startup_listener is None
+    listener.assert_called_once()
+
+
+def test_start_schedules_for_later_when_ha_starting(subject, patched_hass, mocker):
+    # Set up preconditions
+    patched_hass.is_running = False
+    subject.actually_start = mocker.MagicMock()
+
+    # Call the function under test
+    subject.start()
+
+    # Did it avoid actually starting?
+    subject.actually_start.assert_not_called()
+    # Did it register a listener?
+    # assert subject._startup_listener == "LISTENER"
+    # patched_hass.bus.async_listen_once.assert_called_once_with(
+    #     EVENT_HOMEASSISTANT_STARTED, subject.actually_start
+    # )
+
+
+def test_start_does_nothing_when_ha_stopping(subject, patched_hass, mocker):
+    # Set up preconditions
+    patched_hass.is_running = True
+    patched_hass.is_stopping = True
+    subject.actually_start = mocker.MagicMock()
+
+    # Call the function under test
+    subject.start()
+
+    # Did it avoid actually starting?
+    subject.actually_start.assert_not_called()
+    # Did it avoid registering a listener?
+    # patched_hass.bus.async_listen_once.assert_not_called()
+    assert subject._startup_listener is None
+
+
+@pytest.mark.asyncio
+async def test_async_stop(subject, mocker):
+    # Set up preconditions
+    listener = mocker.MagicMock()
+    subject._refresh_task = None
+    subject._shutdown_listener = listener
+    subject._children = [1, 2, 3]
+
+    # Call the function under test
+    await subject.async_stop()
+
+    # Shutdown listener doesn't get cancelled as HA does that
+    listener.assert_not_called()
+    # Were the child entities cleared?
+    assert subject._children == []
+    # Did it wait for the refresh task to finish then clear it?
+    # This doesn't work because AsyncMock only mocks awaitable method calls
+    # but we want an awaitable object
+    # refresh.assert_awaited_once()
+    assert subject._refresh_task is None
+
+
+@pytest.mark.asyncio
+async def test_async_stop_when_not_running(subject):
+    # Set up preconditions
+    _refresh_task = None
+    subject._shutdown_listener = None
+    subject._children = []
+
+    # Call the function under test
+    await subject.async_stop()
+
+    # Was the shutdown listener left empty?
+    assert subject._shutdown_listener is None
+    # Were the child entities cleared?
+    assert subject._children == []
+    # Was the refresh task left empty?
+    assert subject._refresh_task is None
+
+
+def test_register_first_entity_ha_running(subject, mocker):
+    # Set up preconditions
+    subject._children = []
+    subject._running = False
+    subject._startup_listener = None
+    subject.start = mocker.MagicMock()
+    entity = mocker.AsyncMock()
+    entity._config = mocker.MagicMock()
+    entity._config.dps.return_value = []
+    # despite the name, the below HA function is not async and does not need to be awaited
+    entity.async_schedule_update_ha_state = mocker.MagicMock()
+
+    # Call the function under test
+    subject.register_entity(entity)
+
+    # Was the entity added to the list?
+    assert subject._children == [entity]
+
+    # Did we start the loop?
+    subject.start.assert_called_once()
+
+
+def test_register_subsequent_entity_ha_running(subject, mocker):
+    # Set up preconditions
+    first = mocker.AsyncMock()
+    second = mocker.AsyncMock()
+    second._config = mocker.MagicMock()
+    second._config.dps.return_value = []
+    subject._children = [first]
+    subject._running = True
+
+    subject._startup_listener = None
+    subject.start = mocker.MagicMock()
+
+    # Call the function under test
+    subject.register_entity(second)
+
+    # Was the entity added to the list?
+    assert set(subject._children) == set([first, second])
+
+    # Did we avoid restarting the loop?
+    subject.start.assert_not_called()
+
+
+def test_register_subsequent_entity_ha_starting(subject, mocker):
+    # Set up preconditions
+    first = mocker.AsyncMock()
+    second = mocker.AsyncMock()
+    second._config = mocker.MagicMock()
+    second._config.dps.return_value = []
+    subject._children = [first]
+    subject._running = False
+    subject._startup_listener = mocker.MagicMock()
+    subject.start = mocker.MagicMock()
+
+    # Call the function under test
+    subject.register_entity(second)
+
+    # Was the entity added to the list?
+    assert set(subject._children) == set([first, second])
+    # Did we avoid restarting the loop?
+    subject.start.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_unregister_one_of_many_entities(subject, mocker):
+    # Set up preconditions
+    subject._children = ["First", "Second"]
+    subject.async_stop = mocker.AsyncMock()
+
+    # Call the function under test
+    await subject.async_unregister_entity("First")
+
+    # Was the entity removed from the list?
+    assert set(subject._children) == set(["Second"])
+    # Is the loop still running?
+    subject.async_stop.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_unregister_last_entity(subject, mocker):
+    # Set up preconditions
+    subject._children = ["Last"]
+    subject.async_stop = mocker.AsyncMock()
+
+    # Call the function under test
+    await subject.async_unregister_entity("Last")
+
+    # Was the entity removed from the list?
+    assert subject._children == []
+    # Was the loop stopped?
+    subject.async_stop.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_async_receive(subject, mock_api, mocker):
+    # Set up preconditions
+    mock_api().status.return_value = {"dps": {"1": "INIT", "2": 2}}
+    mock_api().receive.return_value = {"1": "UPDATED"}
+    subject._running = True
+    subject._cached_state = {"updated_at": 0}
+    # Call the function under test
+    print("starting test loop...")
+    loop = subject.async_receive()
+    print("getting first iteration...")
+    result = await loop.__anext__()
+
+    # Check that the loop was started, but without persistent connection
+    # since there was no state returned yet and it might need to negotiate
+    # version.
+    mock_api().set_socketPersistent.assert_called_once_with(False)
+    # Check that a full poll was done
+    mock_api().status.assert_called_once()
+    assert result == {"1": "INIT", "2": 2, "full_poll": mocker.ANY}
+    # Prepare for next round
+    subject._cached_state = subject._cached_state | result
+    mock_api().set_socketPersistent.reset_mock()
+    mock_api().status.reset_mock()
+    subject._cached_state["updated_at"] = time()
+
+    # Call the function under test
+    print("getting second iteration...")
+    result = await loop.__anext__()
+
+    # Check that a heartbeat poll was done
+    mock_api().status.assert_not_called()
+    mock_api().heartbeat.assert_called_once()
+    mock_api().receive.assert_called_once()
+    assert result == {"1": "UPDATED", "full_poll": mocker.ANY}
+    # Check that the connection was made persistent now that data has been
+    # returned
+    mock_api().set_socketPersistent.assert_called_once_with(True)
+    # Prepare for next iteration
+    subject._running = False
+    mock_api().set_socketPersistent.reset_mock()
+
+    # Call the function under test
+    print("getting last iteration...")
+    try:
+        result = await loop.__anext__()
+        pytest.fail("Should have raised an exception to quit the loop")
+    # Check that the loop terminated
+    except StopAsyncIteration:
+        pass
+    mock_api().set_socketPersistent.assert_called_once_with(False)
+
+
+def test_should_poll(subject):
+    subject._cached_state = {"1": "sample", "updated_at": time()}
+    subject._poll_only = False
+    subject._temporary_poll = False
+
+    # Test temporary poll via pause/resume
+    assert not subject.should_poll
+    subject.pause()
+    assert subject.should_poll
+    subject.resume()
+    assert not subject.should_poll
+
+    # Test configured polling
+    subject._poll_only = True
+    assert subject.should_poll
+    subject._poll_only = False
+
+    # Test initial polling
+    subject._cached_state = {}
+    assert subject.should_poll

+ 469 - 492
tests/test_device_config.py

@@ -1,8 +1,6 @@
 """Test the config parser"""
 """Test the config parser"""
 
 
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import MagicMock
-
+import pytest
 import voluptuous as vol
 import voluptuous as vol
 from fuzzywuzzy import fuzz
 from fuzzywuzzy import fuzz
 from homeassistant.components.sensor import SensorDeviceClass
 from homeassistant.components.sensor import SensorDeviceClass
@@ -303,524 +301,503 @@ KNOWN_DPS = {
 }
 }
 
 
 
 
-class TestDeviceConfig(IsolatedAsyncioTestCase):
-    """Test the device config parser"""
+def test_can_find_config_files():
+    """Test that the config files can be found by the parser."""
+    found = False
+    for cfg in available_configs():
+        found = True
+        break
+    assert found
 
 
-    def test_can_find_config_files(self):
-        """Test that the config files can be found by the parser."""
-        found = False
-        for cfg in available_configs():
-            found = True
-            break
-        self.assertTrue(found)
-
-    def dp_match(self, condition, accounted, unaccounted, known, required=False):
-        if isinstance(condition, str):
-            known.add(condition)
-            if condition in unaccounted:
-                unaccounted.remove(condition)
-                accounted.add(condition)
-            if required:
-                return condition in accounted
-            else:
-                return True
-        elif "and" in condition:
-            return self.and_match(
-                condition["and"], accounted, unaccounted, known, required
-            )
-        elif "or" in condition:
-            return self.or_match(condition["or"], accounted, unaccounted, known)
-        elif "xor" in condition:
-            return self.xor_match(
-                condition["xor"], accounted, unaccounted, known, required
-            )
-        else:
-            self.fail(f"Unrecognized condition {condition}")
-
-    def and_match(self, conditions, accounted, unaccounted, known, required):
-        single_match = False
-        all_match = True
-        for cond in conditions:
-            match = self.dp_match(cond, accounted, unaccounted, known, True)
-            all_match = all_match and match
-            single_match = single_match or match
+
+def dp_match(condition, accounted, unaccounted, known, required=False):
+    if isinstance(condition, str):
+        known.add(condition)
+        if condition in unaccounted:
+            unaccounted.remove(condition)
+            accounted.add(condition)
         if required:
         if required:
-            return all_match
+            return condition in accounted
         else:
         else:
-            return all_match == single_match
+            return True
+    elif "and" in condition:
+        return and_match(condition["and"], accounted, unaccounted, known, required)
+    elif "or" in condition:
+        return or_match(condition["or"], accounted, unaccounted, known)
+    elif "xor" in condition:
+        return xor_match(condition["xor"], accounted, unaccounted, known, required)
+    else:
+        pytest.fail(f"Unrecognized condition {condition}")
+
+
+def and_match(conditions, accounted, unaccounted, known, required):
+    single_match = False
+    all_match = True
+    for cond in conditions:
+        match = dp_match(cond, accounted, unaccounted, known, True)
+        all_match = all_match and match
+        single_match = single_match or match
+    if required:
+        return all_match
+    else:
+        return all_match == single_match
+
+
+def or_match(conditions, accounted, unaccounted, known):
+    match = False
+    # loop through all, to ensure they are transferred to accounted list
+    for cond in conditions:
+        match = match or dp_match(cond, accounted, unaccounted, known, True)
+    return match
+
+
+def xor_match(conditions, accounted, unaccounted, known, required):
+    prior_match = False
+    for cond in conditions:
+        match = dp_match(cond, accounted, unaccounted, known, True)
+
+        if match and prior_match:
+            return False
+        prior_match = prior_match or match
+
+    # If any matched, all should be considered matched
+    # this bit only handles nesting "and" within "xor"
+
+    if prior_match:
+        for c in conditions:
+            if isinstance(c, str):
+                accounted.add(c)
+            elif "and" in c:
+                for c2 in c["and"]:
+                    if isinstance(c2, str):
+                        accounted.add(c2)
+
+    return prior_match or not required
+
+
+def rule_broken_msg(rule):
+    msg = ""
+    if isinstance(rule, str):
+        return f"{msg} {rule}"
+    elif "and" in rule:
+        msg = f"{msg} all of ["
+        for sub in rule["and"]:
+            msg = f"{msg} {rule_broken_msg(sub)}"
+        return f"{msg} ]"
+    elif "or" in rule:
+        msg = f"{msg} at least one of ["
+        for sub in rule["or"]:
+            msg = f"{msg} {rule_broken_msg(sub)}"
+        return f"{msg} ]"
+    elif "xor" in rule:
+        msg = f"{msg} only one of ["
+        for sub in rule["xor"]:
+            msg = f"{msg} {rule_broken_msg(sub)}"
+        return f"{msg} ]"
+    return "for reason unknown"
+
+
+def check_entity(entity, cfg, mocker):
+    """
+    Check that the entity has a dps list and each dps has an id,
+    type and name, and any other consistency checks.
+    """
+    fname = f"custom_components/tuya_local/devices/{cfg}"
+    line = entity._config.__line__
+    assert entity._config.get("entity") is not None, (
+        f"\n::error file={fname},line={line}::entity type missing in {cfg}",
+    )
+    e = entity.config_id
+    assert entity._config.get("dps") is not None, (
+        f"\n::error file={fname},line={line}::dps missing from {e} in {cfg}",
+    )
+    functions = set()
+    extra = set()
+    known = set()
+    redirects = set()
+
+    # Basic checks of dps, and initialising of redirects and extras sets
+    # for later checking
+    for dp in entity.dps():
+        line = dp._config.__line__
+        assert dp._config.get("id") is not None, (
+            f"\n::error file={fname},line={line}::dp id missing from {e} in {cfg}",
+        )
+        assert dp._config.get("type") is not None, (
+            f"\n::error file={fname},line~{line}::dp type missing from {e} in {cfg}",
+        )
+        assert dp._config.get("name") is not None, (
+            f"\n::error file={fname},line={line}::dp name missing from {e} in {cfg}",
+        )
+        extra.add(dp.name)
+        mappings = dp._config.get("mapping", [])
+        assert isinstance(mappings, list), (
+            f"\n::error file={fname},line={line}::mapping is not a list in {cfg}; entity {e}, dp {dp.name}",
+        )
+        for m in mappings:
+            line = m.__line__
+            conditions = m.get("conditions", [])
+            assert isinstance(conditions, list), (
+                f"\n::error file={fname},line={line}::conditions is not a list in {cfg}; entity {e}, dp {dp.name}",
+            )
+            for c in conditions:
+                if c.get("value_redirect"):
+                    redirects.add(c.get("value_redirect"))
+                if c.get("value_mirror"):
+                    redirects.add(c.get("value_mirror"))
+            if m.get("value_redirect"):
+                redirects.add(m.get("value_redirect"))
+            if m.get("value_mirror"):
+                redirects.add(m.get("value_mirror"))
+
+    line = entity._config.__line__
+    # Check redirects all exist
+    for redirect in redirects:
+        assert redirect in extra, (
+            f"\n::error file={fname},line={line}::dp {redirect} missing from {e} in {cfg}",
+        )
 
 
-    def or_match(self, conditions, accounted, unaccounted, known):
-        match = False
-        # loop through all, to ensure they are transferred to accounted list
-        for cond in conditions:
-            match = match or self.dp_match(cond, accounted, unaccounted, known, True)
-        return match
+    # Check dps that are required for this entity type all exist
+    expected = KNOWN_DPS.get(entity.entity)
+    for rule in expected["required"]:
+        assert dp_match(rule, functions, extra, known, True), (
+            f"\n::error file={fname},line={line}::{cfg} missing required {rule_broken_msg(rule)} in {e}",
+        )
 
 
-    def xor_match(self, conditions, accounted, unaccounted, known, required):
-        prior_match = False
-        for cond in conditions:
-            match = self.dp_match(cond, accounted, unaccounted, known, True)
+    for rule in expected["optional"]:
+        assert dp_match(rule, functions, extra, known, False), (
+            f"\n::error file={fname},line={line}::{cfg} expecting {rule_broken_msg(rule)} in {e}",
+        )
 
 
-            if match and prior_match:
-                return False
-            prior_match = prior_match or match
+    # Check for potential typos in extra attributes
+    known_extra = known - functions
+    for attr in extra:
+        for dp in known_extra:
+            assert fuzz.ratio(attr, dp) < 85, (
+                f"\n::error file={fname},line={line}::Probable typo {attr} is too similar to {dp} in {cfg} {e}",
+            )
 
 
-        # If any matched, all should be considered matched
-        # this bit only handles nesting "and" within "xor"
+    # Check that sensors with mapped values are of class enum and vice versa
+    if entity.entity == "sensor":
+        mock_device = mocker.MagicMock()
+        sensor = TuyaLocalSensor(mock_device, entity)
+        if sensor.options:
+            assert entity.device_class == SensorDeviceClass.ENUM, (
+                f"\n::error file={fname},line={line}::{cfg} {e} has mapped values but does not have a device class of enum",
+            )
+        if entity.device_class == SensorDeviceClass.ENUM:
+            assert sensor.options is not None, (
+                f"\n::error file={fname},line={line}::{cfg} {e} has a device class of enum, but has no mapped values",
+            )
+
+
+def test_config_files_parse(mocker):
+    """
+    All configs should be parsable and meet certain criteria
+    """
+    for cfg in available_configs():
+        entities = []
+        parsed = TuyaDeviceConfig(cfg)
+        # Check for error messages or unparsed config
+        if isinstance(parsed, str) or isinstance(parsed._config, str):
+            pytest.fail(f"unparsable yaml in {cfg}")
 
 
-        if prior_match:
-            for c in conditions:
-                if isinstance(c, str):
-                    accounted.add(c)
-                elif "and" in c:
-                    for c2 in c["and"]:
-                        if isinstance(c2, str):
-                            accounted.add(c2)
-
-        return prior_match or not required
-
-    def rule_broken_msg(self, rule):
-        msg = ""
-        if isinstance(rule, str):
-            return f"{msg} {rule}"
-        elif "and" in rule:
-            msg = f"{msg} all of ["
-            for sub in rule["and"]:
-                msg = f"{msg} {self.rule_broken_msg(sub)}"
-            return f"{msg} ]"
-        elif "or" in rule:
-            msg = f"{msg} at least one of ["
-            for sub in rule["or"]:
-                msg = f"{msg} {self.rule_broken_msg(sub)}"
-            return f"{msg} ]"
-        elif "xor" in rule:
-            msg = f"{msg} only one of ["
-            for sub in rule["xor"]:
-                msg = f"{msg} {self.rule_broken_msg(sub)}"
-            return f"{msg} ]"
-        return "for reason unknown"
-
-    def check_entity(self, entity, cfg):
-        """
-        Check that the entity has a dps list and each dps has an id,
-        type and name, and any other consistency checks.
-        """
         fname = f"custom_components/tuya_local/devices/{cfg}"
         fname = f"custom_components/tuya_local/devices/{cfg}"
-        line = entity._config.__line__
-        self.assertIsNotNone(
-            entity._config.get("entity"),
-            f"\n::error file={fname},line={line}::entity type missing in {cfg}",
+        try:
+            YAML_SCHEMA(parsed._config)
+        except vol.MultipleInvalid as e:
+            messages = []
+            for err in e.errors:
+                path = ".".join([str(p) for p in err.path])
+                messages.append(f"{path}: {err.msg}")
+            messages = "; ".join(messages)
+            pytest.fail(f"\n::error file={fname},line=1::Validation error: {messages}")
+
+        assert parsed._config.get("name") is not None, (
+            f"\n::error file={fname},line=1::name missing from {cfg}",
         )
         )
-        e = entity.config_id
-        self.assertIsNotNone(
-            entity._config.get("dps"),
-            f"\n::error file={fname},line={line}::dps missing from {e} in {cfg}",
+        count = 0
+        for entity in parsed.all_entities():
+            check_entity(entity, cfg, mocker)
+            entities.append(entity.config_id)
+            count += 1
+        assert count > 0, f"\n::error file={fname},line=1::No entities found in {cfg}"
+
+        # check entities are unique
+        assert len(entities) == len(set(entities)), (
+            f"\n::error file={fname},line=1::Duplicate entities in {cfg}",
         )
         )
-        functions = set()
-        extra = set()
-        known = set()
-        redirects = set()
-
-        # Basic checks of dps, and initialising of redirects and extras sets
-        # for later checking
-        for dp in entity.dps():
-            line = dp._config.__line__
-            self.assertIsNotNone(
-                dp._config.get("id"),
-                f"\n::error file={fname},line={line}::dp id missing from {e} in {cfg}",
-            )
-            self.assertIsNotNone(
-                dp._config.get("type"),
-                f"\n::error file={fname},line~{line}::dp type missing from {e} in {cfg}",
-            )
-            self.assertIsNotNone(
-                dp._config.get("name"),
-                f"\n::error file={fname},line={line}::dp name missing from {e} in {cfg}",
-            )
-            extra.add(dp.name)
-            mappings = dp._config.get("mapping", [])
-            self.assertIsInstance(
-                mappings,
-                list,
-                f"\n::error file={fname},line={line}::mapping is not a list in {cfg}; entity {e}, dp {dp.name}",
-            )
-            for m in mappings:
-                line = m.__line__
-                conditions = m.get("conditions", [])
-                self.assertIsInstance(
-                    conditions,
-                    list,
-                    f"\n::error file={fname},line={line}::conditions is not a list in {cfg}; entity {e}, dp {dp.name}",
-                )
-                for c in conditions:
-                    if c.get("value_redirect"):
-                        redirects.add(c.get("value_redirect"))
-                    if c.get("value_mirror"):
-                        redirects.add(c.get("value_mirror"))
-                if m.get("value_redirect"):
-                    redirects.add(m.get("value_redirect"))
-                if m.get("value_mirror"):
-                    redirects.add(m.get("value_mirror"))
-
-        line = entity._config.__line__
-        # Check redirects all exist
-        for redirect in redirects:
-            self.assertIn(
-                redirect,
-                extra,
-                f"\n::error file={fname},line={line}::dp {redirect} missing from {e} in {cfg}",
-            )
 
 
-        # Check dps that are required for this entity type all exist
-        expected = KNOWN_DPS.get(entity.entity)
-        for rule in expected["required"]:
-            self.assertTrue(
-                self.dp_match(rule, functions, extra, known, True),
-                f"\n::error file={fname},line={line}::{cfg} missing required {self.rule_broken_msg(rule)} in {e}",
-            )
 
 
-        for rule in expected["optional"]:
-            self.assertTrue(
-                self.dp_match(rule, functions, extra, known, False),
-                f"\n::error file={fname},line={line}::{cfg} expecting {self.rule_broken_msg(rule)} in {e}",
-            )
+def test_configs_can_be_matched():
+    """Test that the config files can be matched to a device."""
+    for cfg in available_configs():
+        optional = set()
+        required = set()
+        parsed = TuyaDeviceConfig(cfg)
+        fname = f"custom_components/tuya_local/devices/{cfg}"
+        products = parsed._config.get("products")
+        # Configs with a product list can be matched by product id
+        if products:
+            p_match = False
+            for p in products:
+                if p.get("id"):
+                    p_match = True
+            if p_match:
+                continue
+
+        for entity in parsed.all_entities():
+            for dp in entity.dps():
+                if dp.optional:
+                    optional.add(dp.id)
+                else:
+                    required.add(dp.id)
+
+        assert len(required) > 0, (
+            f"\n::error file={fname},line=1::No required dps found in {cfg}"
+        )
 
 
-        # Check for potential typos in extra attributes
-        known_extra = known - functions
-        for attr in extra:
-            for dp in known_extra:
-                self.assertLess(
-                    fuzz.ratio(attr, dp),
-                    85,
-                    f"\n::error file={fname},line={line}::Probable typo {attr} is too similar to {dp} in {cfg} {e}",
-                )
-
-        # Check that sensors with mapped values are of class enum and vice versa
-        if entity.entity == "sensor":
-            mock_device = MagicMock()
-            sensor = TuyaLocalSensor(mock_device, entity)
-            if sensor.options:
-                self.assertEqual(
-                    entity.device_class,
-                    SensorDeviceClass.ENUM,
-                    f"\n::error file={fname},line={line}::{cfg} {e} has mapped values but does not have a device class of enum",
-                )
-            if entity.device_class == SensorDeviceClass.ENUM:
-                self.assertIsNotNone(
-                    sensor.options,
-                    f"\n::error file={fname},line={line}::{cfg} {e} has a device class of enum, but has no mapped values",
-                )
-
-    def test_config_files_parse(self):
-        """
-        All configs should be parsable and meet certain criteria
-        """
-        for cfg in available_configs():
-            entities = []
-            parsed = TuyaDeviceConfig(cfg)
-            # Check for error messages or unparsed config
-            if isinstance(parsed, str) or isinstance(parsed._config, str):
-                self.fail(f"unparsable yaml in {cfg}")
-
-            fname = f"custom_components/tuya_local/devices/{cfg}"
-            try:
-                YAML_SCHEMA(parsed._config)
-            except vol.MultipleInvalid as e:
-                messages = []
-                for err in e.errors:
-                    path = ".".join([str(p) for p in err.path])
-                    messages.append(f"{path}: {err.msg}")
-                messages = "; ".join(messages)
-                self.fail(
-                    f"\n::error file={fname},line=1::Validation error: {messages}"
-                )
-
-            self.assertIsNotNone(
-                parsed._config.get("name"),
-                f"\n::error file={fname},line=1::name missing from {cfg}",
-            )
-            count = 0
-            for entity in parsed.all_entities():
-                self.check_entity(entity, cfg)
-                entities.append(entity.config_id)
-                count += 1
-            assert count > 0, (
-                f"\n::error file={fname},line=1::No entities found in {cfg}"
+        for dp in required:
+            assert dp not in optional, (
+                f"\n::error file={fname},line=1::Optional dp {dp} is required in {cfg}",
             )
             )
 
 
-            # check entities are unique
-            self.assertCountEqual(
-                entities,
-                set(entities),
-                f"\n::error file={fname},line=1::Duplicate entities in {cfg}",
-            )
 
 
-    def test_configs_can_be_matched(self):
-        """Test that the config files can be matched to a device."""
-        for cfg in available_configs():
-            optional = set()
-            required = set()
-            parsed = TuyaDeviceConfig(cfg)
-            fname = f"custom_components/tuya_local/devices/{cfg}"
-            products = parsed._config.get("products")
-            # Configs with a product list can be matched by product id
-            if products:
-                p_match = False
-                for p in products:
-                    if p.get("id"):
-                        p_match = True
-                if p_match:
-                    continue
-
-            for entity in parsed.all_entities():
-                for dp in entity.dps():
-                    if dp.optional:
-                        optional.add(dp.id)
-                    else:
-                        required.add(dp.id)
-
-            self.assertGreater(
-                len(required),
-                0,
-                msg=f"\n::error file={fname},line=1::No required dps found in {cfg}",
-            )
+# Most of the device_config functionality is exercised during testing of
+# the various supported devices.  These tests concentrate only on the gaps.
 
 
-            for dp in required:
-                self.assertNotIn(
-                    dp,
-                    optional,
-                    msg=f"\n::error file={fname},line=1::Optional dp {dp} is required in {cfg}",
-                )
 
 
-    # Most of the device_config functionality is exercised during testing of
-    # the various supported devices.  These tests concentrate only on the gaps.
+def test_match_quality():
+    """Test the match_quality function."""
 
 
-    def test_match_quality(self):
-        """Test the match_quality function."""
+    cfg = get_config("deta_fan")
+    q = cfg.match_quality({**KOGAN_HEATER_PAYLOAD, "updated_at": 0})
 
 
-        cfg = get_config("deta_fan")
-        q = cfg.match_quality({**KOGAN_HEATER_PAYLOAD, "updated_at": 0})
+    assert q == 0
+    q = cfg.match_quality({**GPPH_HEATER_PAYLOAD})
+    assert q == 0
 
 
-        self.assertEqual(q, 0)
-        q = cfg.match_quality({**GPPH_HEATER_PAYLOAD})
-        self.assertEqual(q, 0)
 
 
-    def test_entity_find_unknown_dps_fails(self):
-        """Test that finding a dps that doesn't exist fails."""
-        cfg = get_config("kogan_switch")
-        for entity in cfg.all_entities():
-            non_existing = entity.find_dps("missing")
-            self.assertIsNone(non_existing)
+def test_entity_find_unknown_dps_fails():
+    """Test that finding a dps that doesn't exist fails."""
+    cfg = get_config("kogan_switch")
+    for entity in cfg.all_entities():
+        non_existing = entity.find_dps("missing")
+        assert non_existing is None
+        break
+
+
+@pytest.mark.asyncio
+async def test_dps_async_set_readonly_value_fails(mocker):
+    """Test that setting a readonly dps fails."""
+    mock_device = mocker.MagicMock()
+    cfg = get_config("aquatech_x6_water_heater")
+    for entity in cfg.all_entities():
+        if entity.entity == "climate":
+            temp = entity.find_dps("temperature")
+            with pytest.raises(TypeError):
+                await temp.async_set_value(mock_device, 20)
             break
             break
 
 
-    async def test_dps_async_set_readonly_value_fails(self):
-        """Test that setting a readonly dps fails."""
-        mock_device = MagicMock()
-        cfg = get_config("aquatech_x6_water_heater")
-        for entity in cfg.all_entities():
-            if entity.entity == "climate":
-                temp = entity.find_dps("temperature")
-                with self.assertRaises(TypeError):
-                    await temp.async_set_value(mock_device, 20)
-                break
-
-    def test_dps_values_is_empty_with_no_mapping(self):
-        """
-        Test that a dps with no mapping returns empty list for possible values
-        """
-        mock_device = MagicMock()
-        cfg = get_config("goldair_gpph_heater")
-        for entity in cfg.all_entities():
-            if entity.entity == "climate":
-                temp = entity.find_dps("current_temperature")
-                self.assertEqual(temp.values(mock_device), [])
-                break
-
-    def test_config_returned(self):
-        """Test that config file is returned by config"""
-        cfg = get_config("kogan_switch")
-        self.assertEqual(cfg.config, "smartplugv1.yaml")
-
-    def test_float_matches_ints(self):
-        """Test that the _typematch function matches int values to float dps"""
-        self.assertTrue(_typematch(float, 1))
-
-    def test_bytes_to_fmt_returns_string_for_unknown(self):
-        """
-        Test that the _bytes_to_fmt function parses unknown number of bytes
-        as a string format.
-        """
-        self.assertEqual(_bytes_to_fmt(5), "5s")
-
-    def test_deprecation(self):
-        """Test that deprecation messages are picked from the config."""
-        mock_device = MagicMock()
-        mock_device.name = "Testing"
-        mock_config = {"entity": "Test", "deprecated": "Passed"}
-        cfg = TuyaEntityConfig(mock_device, mock_config)
-        self.assertTrue(cfg.deprecated)
-        self.assertEqual(
-            cfg.deprecation_message,
-            "The use of Test for Testing is deprecated and should be "
-            "replaced by Passed.",
-        )
 
 
-    def test_format_with_none_defined(self):
-        """Test that format returns None when there is none configured."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "string"}
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertIsNone(cfg.format)
-
-    def test_decoding_base64(self):
-        """Test that decoded_value works with base64 encoding."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "base64"}
-        mock_device = MagicMock()
-        mock_device.get_property.return_value = "VGVzdA=="
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(
-            cfg.decoded_value(mock_device),
-            bytes("Test", "utf-8"),
-        )
+def test_dps_values_is_empty_with_no_mapping(mocker):
+    """
+    Test that a dps with no mapping returns empty list for possible values
+    """
+    mock_device = mocker.MagicMock()
+    cfg = get_config("goldair_gpph_heater")
+    for entity in cfg.all_entities():
+        if entity.entity == "climate":
+            temp = entity.find_dps("current_temperature")
+            assert temp.values(mock_device) == []
+            break
 
 
-    def test_decoding_hex(self):
-        """Test that decoded_value works with hex encoding."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "hex"}
-        mock_device = MagicMock()
-        mock_device.get_property.return_value = "babe"
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(
-            cfg.decoded_value(mock_device),
-            b"\xba\xbe",
-        )
 
 
-    def test_decoding_unencoded(self):
-        """Test that decoded_value returns the raw value when not encoded."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "string"}
-        mock_device = MagicMock()
-        mock_device.get_property.return_value = "VGVzdA=="
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(
-            cfg.decoded_value(mock_device),
-            "VGVzdA==",
-        )
+def test_config_returned():
+    """Test that config file is returned by config"""
+    cfg = get_config("kogan_switch")
+    assert cfg.config == "smartplugv1.yaml"
 
 
-    def test_encoding_base64(self):
-        """Test that encode_value works with base64."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "base64"}
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(cfg.encode_value(bytes("Test", "utf-8")), "VGVzdA==")
-
-    def test_encoding_hex(self):
-        """Test that encode_value works with base64."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "hex"}
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(cfg.encode_value(b"\xca\xfe"), "cafe")
-
-    def test_encoding_unencoded(self):
-        """Test that encode_value works with base64."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "string"}
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(cfg.encode_value("Test"), "Test")
-
-    def test_match_returns_false_on_errors_with_bitfield(self):
-        """Test that TypeError and ValueError cause match to return False."""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "bitfield"}
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertFalse(cfg._match(15, "not an integer"))
-
-    def test_values_with_mirror(self):
-        """Test that value_mirror redirects."""
-        mock_entity = MagicMock()
-        mock_config = {
-            "id": "1",
-            "type": "string",
-            "name": "test",
-            "mapping": [
-                {"dps_val": "mirror", "value_mirror": "map_mirror"},
-                {"dps_val": "plain", "value": "unmirrored"},
-            ],
-        }
-        mock_map_config = {
-            "id": "2",
-            "type": "string",
-            "name": "map_mirror",
-            "mapping": [
-                {"dps_val": "1", "value": "map_one"},
-                {"dps_val": "2", "value": "map_two"},
-            ],
-        }
-        mock_device = MagicMock()
-        mock_device.get_property.return_value = "1"
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        map = TuyaDpsConfig(mock_entity, mock_map_config)
-        mock_entity.find_dps.return_value = map
-
-        self.assertCountEqual(
-            cfg.values(mock_device),
-            ["unmirrored", "map_one", "map_two"],
-        )
 
 
-    def test_get_device_id(self):
-        """Test that check if device id is correct"""
-        self.assertEqual("my-device-id", get_device_id({"device_id": "my-device-id"}))
-        self.assertEqual("sub-id", get_device_id({"device_cid": "sub-id"}))
-        self.assertEqual("s", get_device_id({"device_id": "d", "device_cid": "s"}))
-
-    def test_getting_masked_hex(self):
-        """Test that get_value works with masked hex encoding."""
-        mock_entity = MagicMock()
-        mock_config = {
-            "id": "1",
-            "name": "test",
-            "type": "hex",
-            "mask": "ff00",
-        }
-        mock_device = MagicMock()
-        mock_device.get_property.return_value = "babe"
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(
-            cfg.get_value(mock_device),
-            0xBA,
-        )
+def test_float_matches_ints():
+    """Test that the _typematch function matches int values to float dps"""
+    assert _typematch(float, 1)
+
+
+def test_bytes_to_fmt_returns_string_for_unknown():
+    """
+    Test that the _bytes_to_fmt function parses unknown number of bytes
+    as a string format.
+    """
+    assert _bytes_to_fmt(5) == "5s"
+
+
+def test_deprecation(mocker):
+    """Test that deprecation messages are picked from the config."""
+    mock_device = mocker.MagicMock()
+    mock_device.name = "Testing"
+    mock_config = {"entity": "Test", "deprecated": "Passed"}
+    cfg = TuyaEntityConfig(mock_device, mock_config)
+    assert cfg.deprecated
+    assert (
+        cfg.deprecation_message
+        == "The use of Test for Testing is deprecated and should be replaced by Passed."
+    )
+
+
+def test_format_with_none_defined(mocker):
+    """Test that format returns None when there is none configured."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "string"}
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.format is None
+
+
+def test_decoding_base64(mocker):
+    """Test that decoded_value works with base64 encoding."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "base64"}
+    mock_device = mocker.MagicMock()
+    mock_device.get_property.return_value = "VGVzdA=="
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.decoded_value(mock_device) == bytes("Test", "utf-8")
+
+
+def test_decoding_hex(mocker):
+    """Test that decoded_value works with hex encoding."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "hex"}
+    mock_device = mocker.MagicMock()
+    mock_device.get_property.return_value = "babe"
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.decoded_value(mock_device) == b"\xba\xbe"
+
+
+def test_decoding_unencoded(mocker):
+    """Test that decoded_value returns the raw value when not encoded."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "string"}
+    mock_device = mocker.MagicMock()
+    mock_device.get_property.return_value = "VGVzdA=="
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.decoded_value(mock_device) == "VGVzdA=="
+
+
+def test_encoding_base64(mocker):
+    """Test that encode_value works with base64."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "base64"}
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.encode_value(bytes("Test", "utf-8")) == "VGVzdA=="
+
+
+def test_encoding_hex(mocker):
+    """Test that encode_value works with base64."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "hex"}
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.encode_value(b"\xca\xfe") == "cafe"
+
+
+def test_encoding_unencoded(mocker):
+    """Test that encode_value works with base64."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "string"}
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.encode_value("Test") == "Test"
+
+
+def test_match_returns_false_on_errors_with_bitfield(mocker):
+    """Test that TypeError and ValueError cause match to return False."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "bitfield"}
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert not cfg._match(15, "not an integer")
+
+
+def test_values_with_mirror(mocker):
+    """Test that value_mirror redirects."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {
+        "id": "1",
+        "type": "string",
+        "name": "test",
+        "mapping": [
+            {"dps_val": "mirror", "value_mirror": "map_mirror"},
+            {"dps_val": "plain", "value": "unmirrored"},
+        ],
+    }
+    mock_map_config = {
+        "id": "2",
+        "type": "string",
+        "name": "map_mirror",
+        "mapping": [
+            {"dps_val": "1", "value": "map_one"},
+            {"dps_val": "2", "value": "map_two"},
+        ],
+    }
+    mock_device = mocker.MagicMock()
+    mock_device.get_property.return_value = "1"
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    map = TuyaDpsConfig(mock_entity, mock_map_config)
+    mock_entity.find_dps.return_value = map
+
+    assert set(cfg.values(mock_device)) == {"unmirrored", "map_one", "map_two"}
+    assert len(cfg.values(mock_device)) == 3
+
+
+def test_get_device_id():
+    """Test that check if device id is correct"""
+    assert "my-device-id" == get_device_id({"device_id": "my-device-id"})
+    assert "sub-id" == get_device_id({"device_cid": "sub-id"})
+    assert "s" == get_device_id({"device_id": "d", "device_cid": "s"})
+
+
+def test_getting_masked_hex(mocker):
+    """Test that get_value works with masked hex encoding."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {
+        "id": "1",
+        "name": "test",
+        "type": "hex",
+        "mask": "ff00",
+    }
+    mock_device = mocker.MagicMock()
+    mock_device.get_property.return_value = "babe"
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.get_value(mock_device) == 0xBA
+
+
+def test_setting_masked_hex(mocker):
+    """Test that get_values_to_set works with masked hex encoding."""
+    mock_entity = mocker.MagicMock()
+    mock_config = {
+        "id": "1",
+        "name": "test",
+        "type": "hex",
+        "mask": "ff00",
+    }
+    mock_device = mocker.MagicMock()
+    mock_device.get_property.return_value = "babe"
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.get_values_to_set(mock_device, 0xCA) == {"1": "cabe"}
+
+
+def test_default_without_mapping(mocker):
+    """Test that default returns None when there is no mapping"""
+    mock_entity = mocker.MagicMock()
+    mock_config = {"id": "1", "name": "test", "type": "string"}
+    cfg = TuyaDpsConfig(mock_entity, mock_config)
+    assert cfg.default is None
+
+
+def test_matching_with_product_id():
+    """Test that matching with product id works"""
+    cfg = get_config("smartplugv1")
+    assert cfg.matches({}, ["37mnhia3pojleqfh"])
 
 
-    def test_setting_masked_hex(self):
-        """Test that get_values_to_set works with masked hex encoding."""
-        mock_entity = MagicMock()
-        mock_config = {
-            "id": "1",
-            "name": "test",
-            "type": "hex",
-            "mask": "ff00",
-        }
-        mock_device = MagicMock()
-        mock_device.get_property.return_value = "babe"
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertEqual(
-            cfg.get_values_to_set(mock_device, 0xCA),
-            {"1": "cabe"},
-        )
 
 
-    def test_default_without_mapping(self):
-        """Test that default returns None when there is no mapping"""
-        mock_entity = MagicMock()
-        mock_config = {"id": "1", "name": "test", "type": "string"}
-        cfg = TuyaDpsConfig(mock_entity, mock_config)
-        self.assertIsNone(cfg.default)
-
-    def test_matching_with_product_id(self):
-        """Test that matching with product id works"""
-        cfg = get_config("smartplugv1")
-        self.assertTrue(cfg.matches({}, ["37mnhia3pojleqfh"]))
-
-    def test_matched_product_id_with_conflict_rejected(self):
-        """Test that matching with product id fails when there is a conflict"""
-        cfg = get_config("smartplugv1")
-        self.assertFalse(cfg.matches({"1": "wrong_type"}, ["37mnhia3pojleqfh"]))
+def test_matched_product_id_with_conflict_rejected():
+    """Test that matching with product id fails when there is a conflict"""
+    cfg = get_config("smartplugv1")
+    assert not cfg.matches({"1": "wrong_type"}, ["37mnhia3pojleqfh"])