| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- """Tests for the TuyaLocalEntity base class and unit_from_ascii helper."""
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from homeassistant.const import (
- CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- UnitOfArea,
- UnitOfTemperature,
- )
- from homeassistant.helpers.entity import EntityCategory
- from custom_components.tuya_local.entity import (
- BLACKLISTED_ATTRIBUTES,
- TuyaLocalEntity,
- unit_from_ascii,
- )
- class DummySuper:
- """Simulates a HA entity base with a name and icon property."""
- @property
- def name(self):
- return "translated_name"
- @property
- def icon(self):
- return "mdi:default-icon"
- class DummyEntity(TuyaLocalEntity, DummySuper):
- """Concrete subclass so we can instantiate TuyaLocalEntity."""
- def _default_to_device_class_name(self):
- return False
- @pytest.fixture
- def mock_device():
- device = MagicMock()
- device.has_returned_state = True
- device.unique_id = "test_device_123"
- device.name = "Test Device"
- device.device_info = {"identifiers": {("tuya_local", "test_device_123")}}
- device.register_entity = MagicMock()
- device.async_unregister_entity = AsyncMock()
- device.async_refresh = AsyncMock()
- return device
- @pytest.fixture
- def mock_config():
- config = MagicMock()
- config.name = "Test Entity"
- config.translation_key = None
- config.translation_only_key = None
- config.translation_placeholders = None
- config.entity_category = None
- config.deprecated = False
- config.deprecation_message = ""
- config.config_id = "test_config"
- config.device_class = None
- dp1 = MagicMock()
- dp1.name = "state"
- dp1.hidden = False
- dp1.optional = False
- dp1.get_value.return_value = "on"
- dp2 = MagicMock()
- dp2.name = "temperature"
- dp2.hidden = False
- dp2.optional = False
- dp2.get_value.return_value = 25
- dp3 = MagicMock()
- dp3.name = "available"
- dp3.hidden = False
- dp3.optional = False
- dp3.get_value.return_value = True
- config.dps.return_value = [dp1, dp2, dp3]
- config.icon.return_value = None
- config.available.return_value = True
- config.unique_id.return_value = "tuya_local_test_device_123_test"
- config.enabled_by_default.return_value = True
- return config
- @pytest.fixture
- def entity(mock_device, mock_config):
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- e._init_end(dps)
- return e
- class TestInitBeginEnd:
- def test_init_begin_sets_device_and_config(self, mock_device, mock_config):
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- assert e._device is mock_device
- assert e._config is mock_config
- assert isinstance(dps, dict)
- def test_init_begin_returns_dps_dict(self, mock_device, mock_config):
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- assert "state" in dps
- assert "temperature" in dps
- assert "available" in dps
- def test_init_begin_with_translation_key(self, mock_device, mock_config):
- mock_config.translation_key = "my_key"
- e = DummyEntity()
- e._init_begin(mock_device, mock_config)
- assert e._attr_translation_key == "my_key"
- def test_init_begin_with_translation_only_key(self, mock_device, mock_config):
- mock_config.translation_key = None
- mock_config.translation_only_key = "only_key"
- e = DummyEntity()
- e._init_begin(mock_device, mock_config)
- assert e._attr_translation_key == "only_key"
- def test_init_begin_with_placeholders(self, mock_device, mock_config):
- mock_config.translation_placeholders = {"x": "1"}
- e = DummyEntity()
- e._init_begin(mock_device, mock_config)
- assert e._attr_translation_placeholders == {"x": "1"}
- def test_init_end_excludes_blacklisted(self, mock_device, mock_config):
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- e._init_end(dps)
- attr_names = [d.name for d in e._attr_dps]
- for bl in BLACKLISTED_ATTRIBUTES:
- assert bl not in attr_names
- def test_init_end_excludes_hidden(self, mock_device, mock_config):
- dp = MagicMock()
- dp.name = "visible"
- dp.hidden = True
- dp.optional = False
- mock_config.dps.return_value = [dp]
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- e._init_end(dps)
- assert len(e._attr_dps) == 0
- def test_init_end_includes_non_blacklisted_non_hidden(
- self, mock_device, mock_config
- ):
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- e._init_end(dps)
- attr_names = [d.name for d in e._attr_dps]
- assert "temperature" in attr_names
- class TestProperties:
- def test_should_poll(self, entity):
- assert entity.should_poll is False
- def test_available_when_device_returned_state(self, entity):
- assert entity.available is True
- def test_available_false_when_no_state(self, entity, mock_device):
- mock_device.has_returned_state = False
- assert entity.available is False
- def test_available_false_when_config_unavailable(self, entity, mock_config):
- mock_config.available.return_value = False
- assert entity.available is False
- def test_has_entity_name(self, entity):
- assert entity.has_entity_name is True
- def test_name_returns_config_name(self, entity):
- assert entity.name == "Test Entity"
- def test_name_falls_back_to_super(self, entity, mock_config):
- mock_config.name = None
- mock_config.translation_key = "some_key"
- # When use_device_name is False (has translation_key), name calls super
- assert entity.name is not None
- def test_name_uses_device_name_when_no_own_name(self, entity, mock_config):
- mock_config.name = None
- mock_config.translation_key = None
- mock_config.device_class = None
- assert entity.use_device_name is True
- def test_use_device_name_false_with_name(self, entity):
- assert entity.use_device_name is False
- def test_use_device_name_false_with_translation_key(self, entity, mock_config):
- mock_config.name = None
- mock_config.translation_key = "some_key"
- assert entity.use_device_name is False
- def test_unique_id(self, entity):
- assert entity.unique_id == "tuya_local_test_device_123_test"
- def test_device_info(self, entity, mock_device):
- assert entity.device_info is mock_device.device_info
- def test_entity_category_none(self, entity):
- assert entity.entity_category is None
- def test_entity_category_config(self, entity, mock_config):
- mock_config.entity_category = "config"
- assert entity.entity_category == EntityCategory.CONFIG
- def test_entity_category_diagnostic(self, entity, mock_config):
- mock_config.entity_category = "diagnostic"
- assert entity.entity_category == EntityCategory.DIAGNOSTIC
- def test_icon_from_config(self, entity, mock_config):
- mock_config.icon.return_value = "mdi:custom-icon"
- assert entity.icon == "mdi:custom-icon"
- def test_icon_falls_back_to_super(self, entity, mock_config):
- mock_config.icon.return_value = None
- assert entity.icon == "mdi:default-icon"
- def test_extra_state_attributes(self, entity):
- attrs = entity.extra_state_attributes
- assert "temperature" in attrs
- assert attrs["temperature"] == 25
- def test_extra_state_attributes_skips_none_optional(self, mock_device, mock_config):
- dp = MagicMock()
- dp.name = "opt_attr"
- dp.hidden = False
- dp.optional = True
- dp.get_value.return_value = None
- mock_config.dps.return_value = [dp]
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- e._init_end(dps)
- attrs = e.extra_state_attributes
- assert "opt_attr" not in attrs
- def test_extra_state_attributes_includes_none_required(
- self, mock_device, mock_config
- ):
- dp = MagicMock()
- dp.name = "req_attr"
- dp.hidden = False
- dp.optional = False
- dp.get_value.return_value = None
- mock_config.dps.return_value = [dp]
- e = DummyEntity()
- dps = e._init_begin(mock_device, mock_config)
- e._init_end(dps)
- attrs = e.extra_state_attributes
- assert "req_attr" in attrs
- assert attrs["req_attr"] is None
- def test_entity_registry_enabled_default(self, entity, mock_config):
- assert entity.entity_registry_enabled_default is True
- mock_config.enabled_by_default.return_value = False
- assert entity.entity_registry_enabled_default is False
- class TestAsyncMethods:
- @pytest.mark.asyncio
- async def test_async_update(self, entity, mock_device):
- await entity.async_update()
- mock_device.async_refresh.assert_awaited_once()
- @pytest.mark.asyncio
- async def test_async_added_to_hass(self, entity, mock_device):
- await entity.async_added_to_hass()
- mock_device.register_entity.assert_called_once_with(entity)
- @pytest.mark.asyncio
- async def test_async_added_to_hass_logs_deprecation(
- self, entity, mock_device, mock_config
- ):
- mock_config.deprecated = True
- mock_config.deprecation_message = "This entity is deprecated"
- with patch("custom_components.tuya_local.entity._LOGGER") as mock_logger:
- await entity.async_added_to_hass()
- mock_logger.warning.assert_called_with("This entity is deprecated")
- @pytest.mark.asyncio
- async def test_async_will_remove_from_hass(self, entity, mock_device):
- await entity.async_will_remove_from_hass()
- mock_device.async_unregister_entity.assert_awaited_once_with(entity)
- def test_on_receive_does_nothing(self, entity):
- # Default implementation is a no-op
- entity.on_receive({}, False)
- class TestUnitFromAscii:
- def test_celsius(self):
- assert unit_from_ascii("C") == UnitOfTemperature.CELSIUS.value
- def test_fahrenheit(self):
- assert unit_from_ascii("F") == UnitOfTemperature.FAHRENHEIT.value
- def test_micrograms(self):
- assert unit_from_ascii("ugm3") == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
- def test_square_meters(self):
- assert unit_from_ascii("m2") == UnitOfArea.SQUARE_METERS
- def test_passthrough_unknown(self):
- assert unit_from_ascii("km/h") == "km/h"
- def test_passthrough_empty(self):
- assert unit_from_ascii("") == ""
|