Add Camera platform to Prosegur (#76428)

* add camera to prosegur

* add tests

* address review

* better tests

* clean

* clean

* fix tests

* leftover from merge

* sorting missing

* Update homeassistant/components/prosegur/services.yaml

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Diogo Gomes 2023-02-27 01:25:55 +00:00 committed by GitHub
parent 9be3f86a4c
commit c8fc2dc440
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 317 additions and 87 deletions

View File

@ -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__)

View File

@ -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."""

View File

@ -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,
)

View File

@ -3,3 +3,5 @@
DOMAIN = "prosegur"
CONF_COUNTRY = "country"
SERVICE_REQUEST_IMAGE = "request_image"

View File

@ -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,
}

View File

@ -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"]
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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"},
}

View File

@ -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)