"""Tests for EZVIZ entities.""" from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import image from homeassistant.components.ezviz.const import ATTR_TYPE_CLOUD from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration from tests.common import MockConfigEntry, async_fire_time_changed SCAN_INTERVAL = timedelta(seconds=30) def _mock_camera_data(**kwargs: object) -> dict[str, object]: """Return a valid mocked EZVIZ camera payload for integration setup.""" data: dict[str, object] = { "name": "Camera 1", "device_sub_category": "CAMERA", "mac_address": "AA:BB:CC:DD:EE:FF", "version": "1.0.0", "status": 1, "encrypted": False, "supportExt": {}, "switches": {}, "local_ip": "192.168.1.100", "local_rtsp_port": 554, "alarm_notify": False, "upgrade_available": False, "upgrade_in_progress": False, "upgrade_percent": 0, "latest_firmware_info": None, } data.update(kwargs) return data @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" return MockConfigEntry( domain="ezviz", unique_id="test-username", title="test-username", data={ "session_id": "test-username", "rf_session_id": "test-password", "url": "apiieu.ezvizlife.com", "type": ATTR_TYPE_CLOUD, }, ) async def test_image_entity_does_not_expose_last_alarm_pic_attribute( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, ) -> None: """Test that image entity does not include last_alarm_pic as an attribute.""" # Mock coordinator data with last_alarm_pic mock_coordinator_data = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:00:00Z", last_alarm_pic="https://example.com/image.jpg", ) } # Mock the load_cameras method to return our test data mock_ezviz_client.load_cameras.return_value = mock_coordinator_data await setup_integration(hass, mock_config_entry) # Check that image entity was created without exposing the image URL. state = hass.states.get("image.camera_1_last_motion_image") assert state is not None assert "last_alarm_pic" not in state.attributes async def test_last_alarm_pic_sensor_not_created( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, ) -> None: """Test that last_alarm_pic sensor is not created.""" # Mock coordinator data with all sensor fields mock_coordinator_data = { "C123456789": _mock_camera_data( battery_level=85, alarm_sound_mod="soft", last_alarm_time="2023-01-01T12:00:00Z", last_alarm_pic="https://example.com/image.jpg", supported_channels=1, local_ip="192.168.1.100", wan_ip="203.0.113.1", PIR_Status="active", last_alarm_type_code="motion", last_alarm_type_name="Motion Detected", ) } # Mock the load_cameras method to return our test data mock_ezviz_client.load_cameras.return_value = mock_coordinator_data await setup_integration(hass, mock_config_entry) # Check that last_alarm_pic sensor was NOT created last_alarm_pic_state = hass.states.get("sensor.camera_1_last_alarm_pic") assert last_alarm_pic_state is None # But other sensors should be created registry = er.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures battery_entity = registry.async_get("sensor.camera_1_battery") assert battery_entity is not None async def test_migrated_last_alarm_pic_sensor_is_removed( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, entity_registry: er.EntityRegistry, ) -> None: """Test migrated last_alarm_pic sensor entities are removed on setup.""" mock_config_entry.add_to_hass(hass) migrated_entry = entity_registry.async_get_or_create( "sensor", "ezviz", "C123456789_Camera 1.last_alarm_pic", config_entry=mock_config_entry, ) mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:00:00Z", last_alarm_pic="https://example.com/image.jpg", ) } await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert entity_registry.async_get(migrated_entry.entity_id) is None async def test_sensor_cleanup_ignores_entries_without_unique_id( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, entity_registry: er.EntityRegistry, ) -> None: """Test migrated sensor cleanup ignores entries with no unique_id.""" mock_config_entry.add_to_hass(hass) entity_without_unique_id = entity_registry.async_get_or_create( "sensor", "ezviz", "C123456789_Camera 1.battery_level", config_entry=mock_config_entry, ) entity_registry.async_update_entity( entity_without_unique_id.entity_id, new_unique_id=None, ) mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:00:00Z", last_alarm_pic="https://example.com/image.jpg", ) } await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert entity_registry.async_get(entity_without_unique_id.entity_id) is not None async def test_image_entity_created_without_alarm_pic( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, ) -> None: """Test that image entity is created even when last_alarm_pic is missing.""" # Mock coordinator data without last_alarm_pic mock_coordinator_data = { "C123456789": _mock_camera_data( battery_level=85, ) } # Mock the load_cameras method to return our test data mock_ezviz_client.load_cameras.return_value = mock_coordinator_data await setup_integration(hass, mock_config_entry) # Check that image entity was created without exposing the image URL. state = hass.states.get("image.camera_1_last_motion_image") assert state is not None assert "last_alarm_pic" not in state.attributes @pytest.mark.parametrize( ("last_alarm_time", "expected_state"), [ pytest.param( "2023-01-01T12:00:00Z", "2023-01-01T12:00:00+00:00", id="valid timestamp", ), pytest.param(None, STATE_UNKNOWN, id="missing timestamp"), pytest.param(123, STATE_UNKNOWN, id="non-string timestamp"), ], ) async def test_image_entity_last_alarm_time_state( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, last_alarm_time: object, expected_state: str, ) -> None: """Test image entity state from last_alarm_time data.""" mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time=last_alarm_time, last_alarm_pic="https://example.com/image.jpg", ) } await setup_integration(hass, mock_config_entry) state = hass.states.get("image.camera_1_last_motion_image") assert state is not None assert state.state == expected_state async def test_image_entity_updates_last_alarm_pic_on_refresh( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test the image entity updates its alarm picture on refresh.""" mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:00:00Z", last_alarm_pic="https://example.com/image-1.jpg", ) } await setup_integration(hass, mock_config_entry) with patch( "homeassistant.components.ezviz.image.EzvizLastMotion._fetch_url", new_callable=AsyncMock, return_value=Mock(content=b"image-1"), ): image_data = await image.async_get_image( hass, "image.camera_1_last_motion_image" ) assert image_data.content == b"image-1" mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:05:00Z", last_alarm_pic="https://example.com/image-2.jpg", ) } freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() with patch( "homeassistant.components.ezviz.image.EzvizLastMotion._fetch_url", new_callable=AsyncMock, return_value=Mock(content=b"image-2"), ): image_data = await image.async_get_image( hass, "image.camera_1_last_motion_image" ) assert image_data.content == b"image-2" state = hass.states.get("image.camera_1_last_motion_image") assert state is not None assert "last_alarm_pic" not in state.attributes async def test_image_entity_keeps_last_alarm_pic_when_refresh_omits_it( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ezviz_client: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test the image entity keeps the previous alarm picture when omitted.""" mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:00:00Z", last_alarm_pic="https://example.com/image-1.jpg", ) } await setup_integration(hass, mock_config_entry) with patch( "homeassistant.components.ezviz.image.EzvizLastMotion._fetch_url", new_callable=AsyncMock, return_value=Mock(content=b"image-1"), ): image_data = await image.async_get_image( hass, "image.camera_1_last_motion_image" ) assert image_data.content == b"image-1" mock_ezviz_client.load_cameras.return_value = { "C123456789": _mock_camera_data( last_alarm_time="2023-01-01T12:05:00Z", ) } freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() with patch( "homeassistant.components.ezviz.image.EzvizLastMotion._fetch_url", new_callable=AsyncMock, ) as mock_fetch_url: image_data = await image.async_get_image( hass, "image.camera_1_last_motion_image" ) assert image_data.content == b"image-1" mock_fetch_url.assert_not_called() state = hass.states.get("image.camera_1_last_motion_image") assert state is not None assert "last_alarm_pic" not in state.attributes