diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index 04f353e96b8..9f594fc6dae 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client from .const import CONF_COUNTRY, DOMAIN -PLATFORMS = [Platform.ALARM_CONTROL_PANEL] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py index 133c182e2cc..cfcb07773f5 100644 --- a/homeassistant/components/prosegur/alarm_control_panel.py +++ b/homeassistant/components/prosegur/alarm_control_panel.py @@ -15,6 +15,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN @@ -59,6 +60,14 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity): self._attr_name = f"contract {self.contract}" self._attr_unique_id = self.contract + self._attr_device_info = DeviceInfo( + name="Prosegur Alarm", + manufacturer="Prosegur", + model="smart", + identifiers={(DOMAIN, self.contract)}, + configuration_url="https://smart.prosegur.com", + ) + async def async_update(self) -> None: """Update alarm status.""" diff --git a/homeassistant/components/prosegur/camera.py b/homeassistant/components/prosegur/camera.py new file mode 100644 index 00000000000..40f8e18fb66 --- /dev/null +++ b/homeassistant/components/prosegur/camera.py @@ -0,0 +1,97 @@ +"""Support for Prosegur cameras.""" +from __future__ import annotations + +import logging + +from pyprosegur.auth import Auth +from pyprosegur.exceptions import ProsegurException +from pyprosegur.installation import Camera as InstallationCamera, Installation + +from homeassistant.components.camera import Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) + +from . import DOMAIN +from .const import SERVICE_REQUEST_IMAGE + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Prosegur camera platform.""" + + platform = async_get_current_platform() + platform.async_register_entity_service( + SERVICE_REQUEST_IMAGE, + {}, + "async_request_image", + ) + + _installation = await Installation.retrieve(hass.data[DOMAIN][entry.entry_id]) + + async_add_entities( + [ + ProsegurCamera(_installation, camera, hass.data[DOMAIN][entry.entry_id]) + for camera in _installation.cameras + ], + update_before_add=True, + ) + + +class ProsegurCamera(Camera): + """Representation of a Smart Prosegur Camera.""" + + def __init__( + self, installation: Installation, camera: InstallationCamera, auth: Auth + ) -> None: + """Initialize Prosegur Camera component.""" + Camera.__init__(self) + + self._installation = installation + self._camera = camera + self._auth = auth + self._attr_name = camera.description + self._attr_unique_id = f"{self._installation.contract} {camera.id}" + + self._attr_device_info = DeviceInfo( + name=self._camera.description, + manufacturer="Prosegur", + model="smart camera", + identifiers={(DOMAIN, self._installation.contract)}, + configuration_url="https://smart.prosegur.com", + ) + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return bytes of camera image.""" + + try: + _LOGGER.debug("Get image for %s", self._camera.description) + return await self._installation.get_image(self._auth, self._camera.id) + + except ProsegurException as err: + _LOGGER.error("Image %s doesn't exist: %s", self._camera.description, err) + + return None + + async def async_request_image(self): + """Request new image from the camera.""" + + try: + _LOGGER.debug("Request image for %s", self._camera.description) + await self._installation.request_image(self._auth, self._camera.id) + + except ProsegurException as err: + _LOGGER.error( + "Could not request image from camera %s: %s", + self._camera.description, + err, + ) diff --git a/homeassistant/components/prosegur/const.py b/homeassistant/components/prosegur/const.py index b066b320a17..3f5b8691970 100644 --- a/homeassistant/components/prosegur/const.py +++ b/homeassistant/components/prosegur/const.py @@ -3,3 +3,5 @@ DOMAIN = "prosegur" CONF_COUNTRY = "country" + +SERVICE_REQUEST_IMAGE = "request_image" diff --git a/homeassistant/components/prosegur/diagnostics.py b/homeassistant/components/prosegur/diagnostics.py new file mode 100644 index 00000000000..d2445698348 --- /dev/null +++ b/homeassistant/components/prosegur/diagnostics.py @@ -0,0 +1,29 @@ +"""Diagnostics support for Prosegur.""" +from __future__ import annotations + +from typing import Any + +from pyprosegur.installation import Installation + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +TO_REDACT = {"description", "latitude", "longitude", "contractId", "address"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + installation = await Installation.retrieve(hass.data[DOMAIN][entry.entry_id]) + + activity = await installation.activity(hass.data[DOMAIN][entry.entry_id]) + + return { + "installation": async_redact_data(installation.data, TO_REDACT), + "activity": activity, + } diff --git a/homeassistant/components/prosegur/manifest.json b/homeassistant/components/prosegur/manifest.json index 1827939d097..d5081a82dbf 100644 --- a/homeassistant/components/prosegur/manifest.json +++ b/homeassistant/components/prosegur/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/prosegur", "iot_class": "cloud_polling", "loggers": ["pyprosegur"], - "requirements": ["pyprosegur==0.0.5"] + "requirements": ["pyprosegur==0.0.8"] } diff --git a/homeassistant/components/prosegur/services.yaml b/homeassistant/components/prosegur/services.yaml new file mode 100644 index 00000000000..0db63cb7adf --- /dev/null +++ b/homeassistant/components/prosegur/services.yaml @@ -0,0 +1,7 @@ +request_image: + name: Request Camera image + description: Request a new image from a Prosegur Camera + target: + entity: + domain: camera + integration: prosegur diff --git a/requirements_all.txt b/requirements_all.txt index 10dfef7810a..6f8143b0547 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1884,7 +1884,7 @@ pypoint==2.3.0 pyprof2calltree==1.4.5 # homeassistant.components.prosegur -pyprosegur==0.0.5 +pyprosegur==0.0.8 # homeassistant.components.prusalink pyprusalink==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b67a588729..33aa5f39ca2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ pypoint==2.3.0 pyprof2calltree==1.4.5 # homeassistant.components.prosegur -pyprosegur==0.0.5 +pyprosegur==0.0.8 # homeassistant.components.prusalink pyprusalink==1.1.0 diff --git a/tests/components/prosegur/common.py b/tests/components/prosegur/common.py deleted file mode 100644 index bed9d987ceb..00000000000 --- a/tests/components/prosegur/common.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Common methods used across tests for Prosegur.""" -from homeassistant.components.prosegur import DOMAIN as PROSEGUR_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry - -CONTRACT = "1234abcd" - - -async def setup_platform(hass): - """Set up the Prosegur platform.""" - mock_entry = MockConfigEntry( - domain=PROSEGUR_DOMAIN, - data={ - "contract": "1234abcd", - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - "country": "PT", - }, - ) - mock_entry.add_to_hass(hass) - - assert await async_setup_component(hass, PROSEGUR_DOMAIN, {}) - await hass.async_block_till_done() - - return mock_entry diff --git a/tests/components/prosegur/conftest.py b/tests/components/prosegur/conftest.py new file mode 100644 index 00000000000..ea906fdcbff --- /dev/null +++ b/tests/components/prosegur/conftest.py @@ -0,0 +1,58 @@ +"""Define test fixtures for Prosegur.""" +from unittest.mock import AsyncMock, patch + +from pyprosegur.installation import Camera +import pytest + +from homeassistant.components.prosegur import DOMAIN as PROSEGUR_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONTRACT = "1234abcd" + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=PROSEGUR_DOMAIN, + data={ + "contract": CONTRACT, + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + "country": "PT", + }, + ) + + +@pytest.fixture +def mock_install() -> AsyncMock: + """Return the mocked alarm install.""" + install = AsyncMock() + install.contract = CONTRACT + install.cameras = [Camera("1", "test_cam")] + install.get_image = AsyncMock(return_value=b"ABC") + install.request_image = AsyncMock() + + install.data = {"contract": CONTRACT} + install.activity = AsyncMock(return_value={"event": "armed"}) + + return install + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_install: AsyncMock +) -> MockConfigEntry: + """Set up the Prosegur integration for testing.""" + mock_config_entry.add_to_hass(hass) + + with patch( + "pyprosegur.installation.Installation.retrieve", return_value=mock_install + ), patch("pyprosegur.auth.Auth.login", return_value=AsyncMock()): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index dce5e8d3c4e..51086e74b00 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component, entity_registry as er -from .common import CONTRACT, setup_platform +from .conftest import CONTRACT PROSEGUR_ALARM_ENTITY = f"alarm_control_panel.contract_{CONTRACT}" @@ -38,17 +38,17 @@ def mock_status(request): """Mock the status of the alarm.""" install = AsyncMock() - install.contract = "123" - install.installationId = "1234abcd" + install.contract = CONTRACT install.status = request.param with patch("pyprosegur.installation.Installation.retrieve", return_value=install): yield -async def test_entity_registry(hass: HomeAssistant, mock_auth, mock_status) -> None: +async def test_entity_registry( + hass: HomeAssistant, init_integration, mock_auth, mock_status +) -> None: """Tests that the devices are registered in the entity registry.""" - await setup_platform(hass) entity_registry = er.async_get(hass) entry = entity_registry.async_get(PROSEGUR_ALARM_ENTITY) @@ -59,11 +59,13 @@ async def test_entity_registry(hass: HomeAssistant, mock_auth, mock_status) -> N state = hass.states.get(PROSEGUR_ALARM_ENTITY) - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "contract 1234abcd" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"contract {CONTRACT}" assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 3 -async def test_connection_error(hass: HomeAssistant, mock_auth) -> None: +async def test_connection_error( + hass: HomeAssistant, init_integration, mock_auth, mock_config_entry +) -> None: """Test the alarm control panel when connection can't be made to the cloud service.""" install = AsyncMock() @@ -73,8 +75,6 @@ async def test_connection_error(hass: HomeAssistant, mock_auth) -> None: install.status = Status.ARMED with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass) - await hass.async_block_till_done() with patch( @@ -95,7 +95,7 @@ async def test_connection_error(hass: HomeAssistant, mock_auth) -> None: ], ) async def test_arm( - hass: HomeAssistant, mock_auth, code, alarm_service, alarm_state + hass: HomeAssistant, init_integration, mock_auth, code, alarm_service, alarm_state ) -> None: """Test the alarm control panel can be set to away.""" @@ -106,8 +106,6 @@ async def test_arm( install.status = code with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass) - await hass.services.async_call( ALARM_DOMAIN, alarm_service, diff --git a/tests/components/prosegur/test_camera.py b/tests/components/prosegur/test_camera.py new file mode 100644 index 00000000000..75e4cbbc773 --- /dev/null +++ b/tests/components/prosegur/test_camera.py @@ -0,0 +1,69 @@ +"""The camera tests for the prosegur platform.""" +import logging +from unittest.mock import AsyncMock + +from pyprosegur.exceptions import ProsegurException +import pytest + +from homeassistant.components import camera +from homeassistant.components.camera import Image +from homeassistant.components.prosegur.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError + + +async def test_camera(hass, init_integration): + """Test prosegur get_image.""" + + image = await camera.async_get_image(hass, "camera.test_cam") + + assert image == Image(content_type="image/jpeg", content=b"ABC") + + +async def test_camera_fail(hass, init_integration, mock_install, caplog): + """Test prosegur get_image fails.""" + + mock_install.get_image = AsyncMock( + return_value=b"ABC", side_effect=ProsegurException() + ) + + with caplog.at_level(logging.ERROR, logger="homeassistant.components.prosegur"): + try: + await camera.async_get_image(hass, "camera.test_cam") + except HomeAssistantError as exc: + assert str(exc) == "Unable to get image" + else: + assert pytest.fail() + + assert "Image test_cam doesn't exist" in caplog.text + + +async def test_request_image(hass, init_integration, mock_install): + """Test the camera request image service.""" + + await hass.services.async_call( + DOMAIN, + "request_image", + {ATTR_ENTITY_ID: "camera.test_cam"}, + ) + await hass.async_block_till_done() + + assert mock_install.request_image.called + + +async def test_request_image_fail(hass, init_integration, mock_install, caplog): + """Test the camera request image service fails.""" + + mock_install.request_image = AsyncMock(side_effect=ProsegurException()) + + with caplog.at_level(logging.ERROR, logger="homeassistant.components.prosegur"): + await hass.services.async_call( + DOMAIN, + "request_image", + {ATTR_ENTITY_ID: "camera.test_cam"}, + ) + await hass.async_block_till_done() + + assert mock_install.request_image.called + + assert "Could not request image from camera test_cam" in caplog.text diff --git a/tests/components/prosegur/test_diagnostics.py b/tests/components/prosegur/test_diagnostics.py new file mode 100644 index 00000000000..85377833a74 --- /dev/null +++ b/tests/components/prosegur/test_diagnostics.py @@ -0,0 +1,21 @@ +"""Test Prosegur diagnostics.""" + +from unittest.mock import patch + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client, init_integration, mock_install): + """Test generating diagnostics for a config entry.""" + + with patch( + "pyprosegur.installation.Installation.retrieve", return_value=mock_install + ): + diag = await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) + + assert diag == { + "installation": {"contract": "1234abcd"}, + "activity": {"event": "armed"}, + } diff --git a/tests/components/prosegur/test_init.py b/tests/components/prosegur/test_init.py index 7f0373ea93d..cdc7135cf1f 100644 --- a/tests/components/prosegur/test_init.py +++ b/tests/components/prosegur/test_init.py @@ -1,12 +1,10 @@ """Tests prosegur setup.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest -from homeassistant.components.prosegur import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -17,59 +15,28 @@ from tests.test_util.aiohttp import AiohttpClientMocker ConnectionError, ], ) -async def test_setup_entry_fail_retrieve(hass: HomeAssistant, error) -> None: +async def test_setup_entry_fail_retrieve( + hass: HomeAssistant, mock_config_entry, error +) -> None: """Test loading the Prosegur entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - "username": "test-username", - "password": "test-password", - "country": "PT", - "contract": "xpto", - }, - ) - config_entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) with patch( "pyprosegur.auth.Auth.login", side_effect=error, ): - assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() async def test_unload_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration, + mock_config_entry, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test unloading the Prosegur entry.""" - aioclient_mock.post( - "https://smart.prosegur.com/smart-server/ws/access/login", - json={"data": {"token": "123456789"}}, - ) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - "username": "test-username", - "password": "test-password", - "country": "PT", - "contract": "xpto", - }, - ) - config_entry.add_to_hass(hass) - - install = MagicMock() - install.contract = "123" - - with patch( - "homeassistant.components.prosegur.config_flow.Installation.retrieve", - return_value=install, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - - assert await hass.config_entries.async_unload(config_entry.entry_id) + assert await hass.config_entries.async_unload(mock_config_entry.entry_id)