Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ca25dfc
initial
ispysoftware May 4, 2020
9d92b30
add missing fixture
ispysoftware May 4, 2020
25c01d9
fix mocks
ispysoftware May 4, 2020
a58510d
fix mocks 2
ispysoftware May 4, 2020
e3e82e5
update coverage
ispysoftware May 5, 2020
1d1b364
fix broken sync between agent and integration
ispysoftware May 5, 2020
79fd81c
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 5, 2020
0cad5c8
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 5, 2020
4e73c18
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 5, 2020
fb76a64
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
2860005
updates for review
ispysoftware May 6, 2020
7dbaecc
add back in should poll again
ispysoftware May 6, 2020
675655b
revert motion detection enabled flag in state attributes
ispysoftware May 6, 2020
b338409
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
f6328c4
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
1224e7d
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
d75d4ab
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
ed7d7e4
Update homeassistant/components/agent_dvr/__init__.py
ispysoftware May 6, 2020
e08c7ff
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
51552d1
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 6, 2020
e7d873c
Update homeassistant/components/agent_dvr/camera.py
ispysoftware May 8, 2020
50f995e
add is_streaming
ispysoftware May 8, 2020
4b73d4c
Merge branch 'agent' of https://github.com/ispysoftware/home-assistan…
ispysoftware May 8, 2020
8fba4c7
fix is_streaming bug, remove mp4 stream
ispysoftware May 8, 2020
04cc7d0
cleanup
ispysoftware May 8, 2020
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
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ omit =
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aftership/sensor.py
homeassistant/components/agent_dvr/__init__.py
homeassistant/components/agent_dvr/camera.py
homeassistant/components/agent_dvr/const.py
homeassistant/components/agent_dvr/helpers.py
homeassistant/components/airly/__init__.py
homeassistant/components/airly/air_quality.py
homeassistant/components/airly/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ homeassistant/scripts/check_config.py @kellerza
# Integrations
homeassistant/components/abode/* @shred86
homeassistant/components/adguard/* @frenck
homeassistant/components/agent_dvr/* @ispysoftware
homeassistant/components/airly/* @bieniu
homeassistant/components/airvisual/* @bachya
homeassistant/components/alarmdecoder/* @ajschmidt8
Expand Down
82 changes: 82 additions & 0 deletions homeassistant/components/agent_dvr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Support for Agent."""
import asyncio
import logging

from agent import AgentError
from agent.a import Agent

from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL

ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"

_LOGGER = logging.getLogger(__name__)

FORWARDS = ["camera"]


async def async_setup(hass, config):
"""Old way to set up integrations."""
return True


async def async_setup_entry(hass, config_entry):
"""Set up the Agent component."""
hass.data.setdefault(AGENT_DOMAIN, {})

server_origin = config_entry.data[SERVER_URL]

agent_client = Agent(server_origin, async_get_clientsession(hass))
try:
await agent_client.update()
except AgentError:
await agent_client.close()
raise ConfigEntryNotReady

if not agent_client.is_available:
Comment thread
ispysoftware marked this conversation as resolved.
raise ConfigEntryNotReady

await agent_client.get_devices()

hass.data[AGENT_DOMAIN][config_entry.entry_id] = {CONNECTION: agent_client}

device_registry = await dr.async_get_registry(hass)

device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(AGENT_DOMAIN, agent_client.unique)},
manufacturer="iSpyConnect",
name=f"Agent {agent_client.name}",
model="Agent DVR",
sw_version=agent_client.version,
)

for forward in FORWARDS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, forward)
)

return True


async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, forward)
for forward in FORWARDS
]
)
)

await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close()

if unload_ok:
hass.data[AGENT_DOMAIN].pop(config_entry.entry_id)

return unload_ok
215 changes: 215 additions & 0 deletions homeassistant/components/agent_dvr/camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"""Support for Agent camera streaming."""
from datetime import timedelta
import logging

from agent import AgentError

from homeassistant.components.camera import SUPPORT_ON_OFF
from homeassistant.components.mjpeg.camera import (
CONF_MJPEG_URL,
CONF_STILL_IMAGE_URL,
MjpegCamera,
filter_urllib3_logging,
)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
from homeassistant.helpers import entity_platform

from .const import (
ATTRIBUTION,
CAMERA_SCAN_INTERVAL_SECS,
CONNECTION,
DOMAIN as AGENT_DOMAIN,
)

SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)

_LOGGER = logging.getLogger(__name__)

_DEV_EN_ALT = "enable_alerts"
_DEV_DS_ALT = "disable_alerts"
_DEV_EN_REC = "start_recording"
_DEV_DS_REC = "stop_recording"
_DEV_SNAP = "snapshot"

CAMERA_SERVICES = {
_DEV_EN_ALT: "async_enable_alerts",
_DEV_DS_ALT: "async_disable_alerts",
_DEV_EN_REC: "async_start_recording",
_DEV_DS_REC: "async_stop_recording",
_DEV_SNAP: "async_snapshot",
}


async def async_setup_entry(
hass, config_entry, async_add_entities, discovery_info=None
):
"""Set up the Agent cameras."""
filter_urllib3_logging()
cameras = []

server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION]
if not server.devices:
_LOGGER.warning("Could not fetch cameras from Agent server")
return

for device in server.devices:
if device.typeID == 2:
camera = AgentCamera(device)
cameras.append(camera)

async_add_entities(cameras)

platform = entity_platform.current_platform.get()
for service, method in CAMERA_SERVICES.items():
platform.async_register_entity_service(service, {}, method)


class AgentCamera(MjpegCamera):
"""Representation of an Agent Device Stream."""

def __init__(self, device):
"""Initialize as a subclass of MjpegCamera."""
self._servername = device.client.name
self.server_url = device.client._server_url

device_info = {
CONF_NAME: device.name,
CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size=640x480",
CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size=640x480",
}
self.device = device
self._removed = False
self._name = f"{self._servername} {device.name}"
self._unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
super().__init__(device_info)

@property
def device_info(self):
"""Return the device info for adding the entity to the agent object."""
return {
"identifiers": {(AGENT_DOMAIN, self._unique_id)},
"name": self._name,
"manufacturer": "Agent",
"model": "Camera",
"sw_version": self.device.client.version,
}

async def async_update(self):
"""Update our state from the Agent API."""
try:
await self.device.update()
if self._removed:
_LOGGER.debug("%s reacquired", self._name)
self._removed = False
except AgentError:
if self.device.client.is_available: # server still available - camera error
if not self._removed:
_LOGGER.error("%s lost", self._name)
self._removed = True

@property
def device_state_attributes(self):
"""Return the Agent DVR camera state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
"editable": False,
"enabled": self.is_on,
"connected": self.connected,
"detected": self.is_detected,
"alerted": self.is_alerted,
"has_ptz": self.device.has_ptz,
"alerts_enabled": self.device.alerts_active,
}

@property
def should_poll(self) -> bool:
"""Update the state periodically."""
return True

@property
def is_recording(self) -> bool:
"""Return whether the monitor is recording."""
return self.device.recording

@property
def is_alerted(self) -> bool:
"""Return whether the monitor has alerted."""
return self.device.alerted

@property
def is_detected(self) -> bool:
"""Return whether the monitor has alerted."""
return self.device.detected

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.device.client.is_available

@property
def connected(self) -> bool:
"""Return True if entity is connected."""
return self.device.connected
Comment thread
ispysoftware marked this conversation as resolved.

@property
def supported_features(self) -> int:
"""Return supported features."""
return SUPPORT_ON_OFF

@property
def is_on(self) -> bool:
"""Return true if on."""
return self.device.online

@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
if self.is_on:
return "mdi:camcorder"
return "mdi:camcorder-off"

@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return self.device.detector_active

@property
def unique_id(self) -> str:
"""Return a unique identifier for this agent object."""
return self._unique_id

async def async_enable_alerts(self):
"""Enable alerts."""
await self.device.alerts_on()

async def async_disable_alerts(self):
"""Disable alerts."""
await self.device.alerts_off()

async def async_enable_motion_detection(self):
"""Enable motion detection."""
await self.device.detector_on()

async def async_disable_motion_detection(self):
"""Disable motion detection."""
await self.device.detector_off()

async def async_start_recording(self):
"""Start recording."""
await self.device.record()

async def async_stop_recording(self):
"""Stop recording."""
await self.device.record_stop()

async def async_turn_on(self):
"""Enable the camera."""
await self.device.enable()

async def async_snapshot(self):
"""Take a snapshot."""
await self.device.snapshot()

async def async_turn_off(self):
"""Disable the camera."""
await self.device.disable()
81 changes: 81 additions & 0 deletions homeassistant/components/agent_dvr/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Config flow to configure Agent devices."""
import logging

from agent import AgentConnectionError, AgentError
from agent.a import Agent
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import
from .helpers import generate_url

DEFAULT_PORT = 8090
_LOGGER = logging.getLogger(__name__)


class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an Agent config flow."""

def __init__(self):
"""Initialize the Agent config flow."""
self.device_config = {}

async def async_step_user(self, info=None):
"""Handle an Agent config flow."""
errors = {}

if info is not None:
host = info[CONF_HOST]
port = info[CONF_PORT]

server_origin = generate_url(host, port)
agent_client = Agent(server_origin, async_get_clientsession(self.hass))
Comment thread
ispysoftware marked this conversation as resolved.

try:
await agent_client.update()
except AgentConnectionError:
pass
except AgentError:
pass

await agent_client.close()

if agent_client.is_available:
await self.async_set_unique_id(agent_client.unique)

self._abort_if_unique_id_configured(
updates={
CONF_HOST: info[CONF_HOST],
CONF_PORT: info[CONF_PORT],
SERVER_URL: server_origin,
}
)

self.device_config = {
CONF_HOST: host,
CONF_PORT: port,
SERVER_URL: server_origin,
}

return await self._create_entry(agent_client.name)

errors["base"] = "device_unavailable"

data = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}

return self.async_show_form(
step_id="user",
description_placeholders=self.device_config,
data_schema=vol.Schema(data),
errors=errors,
)

async def _create_entry(self, server_name):
"""Create entry for device."""
return self.async_create_entry(title=server_name, data=self.device_config)
Loading