Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/prosegur/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from .const import CONF_COUNTRY, DOMAIN

PLATFORMS = [Platform.ALARM_CONTROL_PANEL]
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]

_LOGGER = logging.getLogger(__name__)

Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/prosegur/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import DOMAIN
Expand Down Expand Up @@ -59,6 +60,14 @@ def __init__(self, contract: str, auth: Auth) -> None:
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."""

Expand Down
97 changes: 97 additions & 0 deletions homeassistant/components/prosegur/camera.py
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this? We have an update entity service

Copy link
Copy Markdown
Contributor Author

@dgomes dgomes Dec 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The camera sends a still picture through an MMS service, so it's not advisable to update periodically and only use it seldomly (Prosegur will throttle your request heavily).

Prosegur Mobile App also requires this explicit request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more explanation:

  • The alarm camera will trigger on it's self based on motion sensing and send image to Prosegur server
  • User might request an out of order camera capture (that's the service I'm proposing)

This service is therefore independent from update entity service which will periodically get the last camera capture available on Prosegur server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This service is therefore independent from update entity service which will periodically get the last camera capture available on Prosegur server.

The update entity service allows to request a single update (yes, it could be used periodically in an automation, but by itself it isn't periodically).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_service -> request image from Prosegur server
request_image -> request Prosegur to refresh it's server stored 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)
Comment thread
dgomes marked this conversation as resolved.
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,
)
2 changes: 2 additions & 0 deletions homeassistant/components/prosegur/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
DOMAIN = "prosegur"

CONF_COUNTRY = "country"

SERVICE_REQUEST_IMAGE = "request_image"
29 changes: 29 additions & 0 deletions homeassistant/components/prosegur/diagnostics.py
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion homeassistant/components/prosegur/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
7 changes: 7 additions & 0 deletions homeassistant/components/prosegur/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
request_image:
name: Request Camera image
description: Request a new image from a Prosegur Camera
target:
entity:
domain: camera
Comment thread
dgomes marked this conversation as resolved.
integration: prosegur
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 0 additions & 27 deletions tests/components/prosegur/common.py

This file was deleted.

58 changes: 58 additions & 0 deletions tests/components/prosegur/conftest.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installation doesn't need to be an AsyncMock. It can be a MagicMock or Mock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually does:

    async def async_alarm_arm_home(self, code: str | None = None) -> None:
        """Send arm away command."""
>       await self._installation.arm_partially(self._auth)
E       TypeError: object MagicMock can't be used in 'await' expression

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either mock the attributes of the installation explicitly or spec the installation according to the original target.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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()):
Comment thread
dgomes marked this conversation as resolved.
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

return mock_config_entry
22 changes: 10 additions & 12 deletions tests/components/prosegur/test_alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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}"

Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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(
Expand All @@ -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."""

Expand All @@ -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,
Expand Down
Loading