diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 0555f8a145a..d8055894201 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -3,8 +3,10 @@ from __future__ import annotations import logging +from pathlib import Path from typing import Any, NamedTuple +from tuya_device_handlers.devices import register_tuya_quirks from tuya_sharing import ( CustomerDevice, Manager, @@ -58,6 +60,10 @@ def _create_manager(entry: TuyaConfigEntry, token_listener: TokenListener) -> Ma async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool: """Async setup hass config entry.""" + await hass.async_add_executor_job( + register_tuya_quirks, str(Path(hass.config.config_dir, "tuya_quirks")) + ) + token_listener = TokenListener(hass, entry) # Move to executor as it makes blocking call to import_module diff --git a/homeassistant/components/tuya/camera.py b/homeassistant/components/tuya/camera.py index 36b69885b2e..ee721705e56 100644 --- a/homeassistant/components/tuya/camera.py +++ b/homeassistant/components/tuya/camera.py @@ -2,7 +2,9 @@ from __future__ import annotations +from tuya_device_handlers import TUYA_QUIRKS_REGISTRY from tuya_device_handlers.definition.camera import ( + CameraQuirk, TuyaCameraDefinition, get_default_definition, ) @@ -28,6 +30,20 @@ CAMERAS: dict[DeviceCategory, CameraEntityDescription] = { } +def _get_quirk_entities( + manager: Manager, device: CustomerDevice +) -> list[TuyaCameraEntity] | None: + if (quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device)) is None or ( + entity_quirks := quirk.camera_quirks + ) is None: + return None + return [ + TuyaCameraEntity(device, manager, definition, quirk=entity_quirk) + for entity_quirk in entity_quirks + if (definition := entity_quirk.definition_fn(device)) + ] + + async def async_setup_entry( hass: HomeAssistant, entry: TuyaConfigEntry, @@ -42,10 +58,13 @@ async def async_setup_entry( entities: list[TuyaCameraEntity] = [] for device_id in device_ids: device = manager.device_map[device_id] + if (quirk_entities := _get_quirk_entities(manager, device)) is not None: + entities.extend(quirk_entities) + continue if description := CAMERAS.get(device.category): entities.append( TuyaCameraEntity( - device, manager, description, get_default_definition(device) + device, manager, get_default_definition(device), description ) ) @@ -69,8 +88,10 @@ class TuyaCameraEntity(TuyaEntity, CameraEntity): self, device: CustomerDevice, device_manager: Manager, - description: CameraEntityDescription, definition: TuyaCameraDefinition, + description: CameraEntityDescription | None = None, + *, + quirk: CameraQuirk | None = None, ) -> None: """Init Tuya Camera.""" super().__init__(device, device_manager, description) @@ -78,6 +99,8 @@ class TuyaCameraEntity(TuyaEntity, CameraEntity): self._attr_model = device.product_name self._motion_detection_switch = definition.motion_detection_switch self._recording_status = definition.recording_status + if quirk and quirk.key: + self._attr_unique_id = f"tuya.{device.id}_{quirk.key}" @property def is_recording(self) -> bool: diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index 7ebe9aaf416..23abba61927 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -24,11 +24,13 @@ class TuyaEntity(Entity): self, device: CustomerDevice, device_manager: Manager, - description: EntityDescription, + description: EntityDescription | None, ) -> None: - """Init TuyaHaEntity.""" - self._attr_unique_id = f"tuya.{device.id}{description.key}" - self.entity_description = description + """Init TuyaEntity.""" + self._attr_unique_id = f"tuya.{device.id}" + if description: + self.entity_description = description + self._attr_unique_id = f"tuya.{device.id}{description.key}" # TuyaEntity initialize mq can subscribe device.set_up = True self.device = device diff --git a/tests/components/tuya/test_camera.py b/tests/components/tuya/test_camera.py index e2fddd4bac4..e288cbf2636 100644 --- a/tests/components/tuya/test_camera.py +++ b/tests/components/tuya/test_camera.py @@ -3,10 +3,12 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from syrupy.assertion import SnapshotAssertion +from tuya_device_handlers import TUYA_QUIRKS_REGISTRY +from tuya_device_handlers.definition.camera import CameraQuirk, get_default_definition from tuya_sharing import CustomerDevice, Manager from homeassistant.components.camera import ( @@ -37,16 +39,6 @@ def platform_autouse(): yield -@pytest.fixture(autouse=True) -def mock_getrandbits(): - """Mock camera access token which normally is randomized.""" - with patch( - "homeassistant.components.camera.SystemRandom.getrandbits", - return_value=1, - ): - yield - - async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: Manager, @@ -67,6 +59,34 @@ async def test_platform_setup_and_discovery( ) +@pytest.mark.parametrize("mock_device_code", ["sp_rudejjigkywujjvs"]) +@pytest.mark.parametrize( + ("get_quirks", "available"), + [ + (None, True), + ([], False), + ([CameraQuirk(key="", definition_fn=get_default_definition)], True), + ([CameraQuirk(key="", definition_fn=lambda d: None)], False), + ], +) +async def test_empty_quirk( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + get_quirks: list | None, + available: bool, +) -> None: + """Test None quirks use defaults and empty quirk list skips default entities.""" + with patch.object(TUYA_QUIRKS_REGISTRY, "get_quirk_for_device") as mock_get_quirk: + mock_get_quirk.return_value = Mock() + mock_get_quirk.return_value.camera_quirks = get_quirks + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + + state = hass.states.get("camera.burocam") + assert (state is not None) is available + + @pytest.mark.parametrize( "mock_device_code", ["sp_rudejjigkywujjvs"],