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
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,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