Skip to content

Commit

Permalink
Add sighthound integration (home-assistant#28824)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
robmarkcole authored and MartinHjelmare committed Jan 23, 2020
1 parent 73a5582 commit c71ae09
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/sighthound/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The sighthound integration."""
120 changes: 120 additions & 0 deletions homeassistant/components/sighthound/image_processing.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions homeassistant/components/sighthound/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"domain": "sighthound",
"name": "Sighthound",
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"requirements": [
"simplehound==0.3"
],
"dependencies": [],
"codeowners": [
"@robmarkcole"
]
}
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tests/components/sighthound/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Sighthound integration."""
93 changes: 93 additions & 0 deletions tests/components/sighthound/test_image_processing.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit c71ae09

Please sign in to comment.