Add sighthound integration (#28824)

* Add component files

* Add test state

* Adds person detected event

* Update CODEOWNERS

* Updates requirements

* remove unused datetime

* Bump sighthound version

* Update CODEOWNERS

* Update CODEOWNERS

* Create requirements_test_all.txt

* Address reviewer comments

* Add test for bad_api_key
This commit is contained in:
Robin 2020-01-23 08:30:06 +00:00 committed by Martin Hjelmare
parent 73a55825af
commit c71ae090fc
8 changed files with 234 additions and 0 deletions

View File

@ -295,6 +295,7 @@ homeassistant/components/seventeentrack/* @bachya
homeassistant/components/shell_command/* @home-assistant/core
homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff
homeassistant/components/sighthound/* @robmarkcole
homeassistant/components/signal_messenger/* @bbernhard
homeassistant/components/simplisafe/* @bachya
homeassistant/components/sinch/* @bendikrb

View File

@ -0,0 +1 @@
"""The sighthound integration."""

View File

@ -0,0 +1,120 @@
"""Person detection using Sighthound cloud service."""
import logging
import simplehound.core as hound
import voluptuous as vol
from homeassistant.components.image_processing import (
CONF_ENTITY_ID,
CONF_NAME,
CONF_SOURCE,
PLATFORM_SCHEMA,
ImageProcessingEntity,
)
from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
EVENT_PERSON_DETECTED = "sighthound.person_detected"
ATTR_BOUNDING_BOX = "bounding_box"
ATTR_PEOPLE = "people"
CONF_ACCOUNT_TYPE = "account_type"
DEV = "dev"
PROD = "prod"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_ACCOUNT_TYPE, default=DEV): vol.In([DEV, PROD]),
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the platform."""
# Validate credentials by processing image.
api_key = config[CONF_API_KEY]
account_type = config[CONF_ACCOUNT_TYPE]
api = hound.cloud(api_key, account_type)
try:
api.detect(b"Test")
except hound.SimplehoundException as exc:
_LOGGER.error("Sighthound error %s setup aborted", exc)
return
entities = []
for camera in config[CONF_SOURCE]:
sighthound = SighthoundEntity(
api, camera[CONF_ENTITY_ID], camera.get(CONF_NAME)
)
entities.append(sighthound)
add_entities(entities)
class SighthoundEntity(ImageProcessingEntity):
"""Create a sighthound entity."""
def __init__(self, api, camera_entity, name):
"""Init."""
self._api = api
self._camera = camera_entity
if name:
self._name = name
else:
camera_name = split_entity_id(camera_entity)[1]
self._name = f"sighthound_{camera_name}"
self._state = None
self._image_width = None
self._image_height = None
def process_image(self, image):
"""Process an image."""
detections = self._api.detect(image)
people = hound.get_people(detections)
self._state = len(people)
metadata = hound.get_metadata(detections)
self._image_width = metadata["image_width"]
self._image_height = metadata["image_height"]
for person in people:
self.fire_person_detected_event(person)
def fire_person_detected_event(self, person):
"""Send event with detected total_persons."""
self.hass.bus.fire(
EVENT_PERSON_DETECTED,
{
ATTR_ENTITY_ID: self.entity_id,
ATTR_BOUNDING_BOX: hound.bbox_to_tf_style(
person["boundingBox"], self._image_width, self._image_height
),
},
)
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
return self._camera
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def state(self):
"""Return the state of the entity."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return ATTR_PEOPLE

View File

@ -0,0 +1,12 @@
{
"domain": "sighthound",
"name": "Sighthound",
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"requirements": [
"simplehound==0.3"
],
"dependencies": [],
"codeowners": [
"@robmarkcole"
]
}

View File

@ -1809,6 +1809,9 @@ sharp_aquos_rc==0.3.2
# homeassistant.components.shodan
shodan==1.21.2
# homeassistant.components.sighthound
simplehound==0.3
# homeassistant.components.simplepush
simplepush==1.1.4

View File

@ -584,6 +584,9 @@ samsungctl[websocket]==0.7.1
# homeassistant.components.sentry
sentry-sdk==0.13.5
# homeassistant.components.sighthound
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==6.0.0

View File

@ -0,0 +1 @@
"""Tests for the Sighthound integration."""

View File

@ -0,0 +1,93 @@
"""Tests for the Sighthound integration."""
from unittest.mock import patch
import pytest
import simplehound.core as hound
import homeassistant.components.image_processing as ip
import homeassistant.components.sighthound.image_processing as sh
from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
VALID_CONFIG = {
ip.DOMAIN: {
"platform": "sighthound",
CONF_API_KEY: "abc123",
ip.CONF_SOURCE: {ip.CONF_ENTITY_ID: "camera.demo_camera"},
},
"camera": {"platform": "demo"},
}
VALID_ENTITY_ID = "image_processing.sighthound_demo_camera"
MOCK_DETECTIONS = {
"image": {"width": 960, "height": 480, "orientation": 1},
"objects": [
{
"type": "person",
"boundingBox": {"x": 227, "y": 133, "height": 245, "width": 125},
},
{
"type": "person",
"boundingBox": {"x": 833, "y": 137, "height": 268, "width": 93},
},
],
"requestId": "545cec700eac4d389743e2266264e84b",
}
@pytest.fixture
def mock_detections():
"""Return a mock detection."""
with patch(
"simplehound.core.cloud.detect", return_value=MOCK_DETECTIONS
) as detection:
yield detection
@pytest.fixture
def mock_image():
"""Return a mock camera image."""
with patch(
"homeassistant.components.demo.camera.DemoCamera.camera_image",
return_value=b"Test",
) as image:
yield image
async def test_bad_api_key(hass, caplog):
"""Catch bad api key."""
with patch("simplehound.core.cloud.detect", side_effect=hound.SimplehoundException):
await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
assert "Sighthound error" in caplog.text
assert not hass.states.get(VALID_ENTITY_ID)
async def test_setup_platform(hass, mock_detections):
"""Set up platform with one entity."""
await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
assert hass.states.get(VALID_ENTITY_ID)
async def test_process_image(hass, mock_image, mock_detections):
"""Process an image."""
await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
assert hass.states.get(VALID_ENTITY_ID)
person_events = []
@callback
def capture_person_event(event):
"""Mock event."""
person_events.append(event)
hass.bus.async_listen(sh.EVENT_PERSON_DETECTED, capture_person_event)
data = {ATTR_ENTITY_ID: VALID_ENTITY_ID}
await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data)
await hass.async_block_till_done()
state = hass.states.get(VALID_ENTITY_ID)
assert state.state == "2"
assert len(person_events) == 2