mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
73a55825af
commit
c71ae090fc
@ -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
|
||||
|
1
homeassistant/components/sighthound/__init__.py
Normal file
1
homeassistant/components/sighthound/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The sighthound integration."""
|
120
homeassistant/components/sighthound/image_processing.py
Normal file
120
homeassistant/components/sighthound/image_processing.py
Normal 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
|
12
homeassistant/components/sighthound/manifest.json
Normal file
12
homeassistant/components/sighthound/manifest.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "sighthound",
|
||||
"name": "Sighthound",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sighthound",
|
||||
"requirements": [
|
||||
"simplehound==0.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@robmarkcole"
|
||||
]
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/sighthound/__init__.py
Normal file
1
tests/components/sighthound/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Sighthound integration."""
|
93
tests/components/sighthound/test_image_processing.py
Normal file
93
tests/components/sighthound/test_image_processing.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user