mirror of https://github.com/home-assistant/core
Add agent_dvr integration (#32711)
* initial * add missing fixture * fix mocks * fix mocks 2 * update coverage * fix broken sync between agent and integration * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: J. Nick Koston <nick@koston.org> * updates for review * add back in should poll again * revert motion detection enabled flag in state attributes * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/agent_dvr/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * add is_streaming * fix is_streaming bug, remove mp4 stream * cleanup Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
3a0d5126ae
commit
1be41b9de8
|
@ -16,6 +16,10 @@ omit =
|
||||||
homeassistant/components/adguard/switch.py
|
homeassistant/components/adguard/switch.py
|
||||||
homeassistant/components/ads/*
|
homeassistant/components/ads/*
|
||||||
homeassistant/components/aftership/sensor.py
|
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/__init__.py
|
||||||
homeassistant/components/airly/air_quality.py
|
homeassistant/components/airly/air_quality.py
|
||||||
homeassistant/components/airly/sensor.py
|
homeassistant/components/airly/sensor.py
|
||||||
|
|
|
@ -15,6 +15,7 @@ homeassistant/scripts/check_config.py @kellerza
|
||||||
# Integrations
|
# Integrations
|
||||||
homeassistant/components/abode/* @shred86
|
homeassistant/components/abode/* @shred86
|
||||||
homeassistant/components/adguard/* @frenck
|
homeassistant/components/adguard/* @frenck
|
||||||
|
homeassistant/components/agent_dvr/* @ispysoftware
|
||||||
homeassistant/components/airly/* @bieniu
|
homeassistant/components/airly/* @bieniu
|
||||||
homeassistant/components/airvisual/* @bachya
|
homeassistant/components/airvisual/* @bachya
|
||||||
homeassistant/components/alarmdecoder/* @ajschmidt8
|
homeassistant/components/alarmdecoder/* @ajschmidt8
|
||||||
|
|
|
@ -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:
|
||||||
|
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
|
|
@ -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
|
||||||
|
|
||||||
|
@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()
|
|
@ -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))
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""Constants for agent_dvr component."""
|
||||||
|
DOMAIN = "agent_dvr"
|
||||||
|
SERVERS = "servers"
|
||||||
|
DEVICES = "devices"
|
||||||
|
ENTITIES = "entities"
|
||||||
|
CAMERA_SCAN_INTERVAL_SECS = 5
|
||||||
|
SERVICE_UPDATE = "update"
|
||||||
|
SIGNAL_UPDATE_AGENT = "agent_update"
|
||||||
|
ATTRIBUTION = "Data provided by ispyconnect.com"
|
||||||
|
SERVER_URL = "server_url"
|
||||||
|
CONNECTION = "connection"
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Helpers for Agent DVR component."""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_url(host, port) -> str:
|
||||||
|
"""Create a URL from the host and port."""
|
||||||
|
server_origin = host
|
||||||
|
if "://" not in host:
|
||||||
|
server_origin = f"http://{host}"
|
||||||
|
|
||||||
|
if server_origin[-1] == "/":
|
||||||
|
server_origin = server_origin[:-1]
|
||||||
|
|
||||||
|
return f"{server_origin}:{port}/"
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"domain": "agent_dvr",
|
||||||
|
"name": "Agent DVR",
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/agent_dvr/",
|
||||||
|
"requirements": ["agent-py==0.0.20"],
|
||||||
|
"config_flow": true,
|
||||||
|
"codeowners": ["@ispysoftware"]
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
start_recording:
|
||||||
|
description: Enable continuous recording.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: "Name(s) of the entity to start recording."
|
||||||
|
example: "camera.camera_1"
|
||||||
|
|
||||||
|
stop_recording:
|
||||||
|
description: Disable continuous recording.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: "Name(s) of the entity to stop recording."
|
||||||
|
example: "camera.camera_1"
|
||||||
|
|
||||||
|
enable_alerts:
|
||||||
|
description: Enable alerts
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: "Name(s) of the entity to enable alerts."
|
||||||
|
example: "camera.camera_1"
|
||||||
|
|
||||||
|
disable_alerts:
|
||||||
|
description: Disable alerts
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: "Name(s) of the entity to disable alerts."
|
||||||
|
example: "camera.camera_1"
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
description: Take a photo
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: "Name(s) of the entity to take a snapshot."
|
||||||
|
example: "camera.camera_1"
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "Agent DVR",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Set up Agent DVR",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"already_in_progress": "Config flow for device is already in progress.",
|
||||||
|
"device_unavailable": "Device is not available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "Agent DVR",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Set up Agent DVR",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"already_in_progress": "Config flow for device is already in progress.",
|
||||||
|
"device_unavailable": "Device is not available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ To update, run python3 -m script.hassfest
|
||||||
FLOWS = [
|
FLOWS = [
|
||||||
"abode",
|
"abode",
|
||||||
"adguard",
|
"adguard",
|
||||||
|
"agent_dvr",
|
||||||
"airly",
|
"airly",
|
||||||
"airvisual",
|
"airvisual",
|
||||||
"almond",
|
"almond",
|
||||||
|
|
|
@ -128,6 +128,9 @@ adguardhome==0.4.2
|
||||||
# homeassistant.components.frontier_silicon
|
# homeassistant.components.frontier_silicon
|
||||||
afsapi==0.0.4
|
afsapi==0.0.4
|
||||||
|
|
||||||
|
# homeassistant.components.agent_dvr
|
||||||
|
agent-py==0.0.20
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_quakes
|
# homeassistant.components.geonetnz_quakes
|
||||||
aio_geojson_geonetnz_quakes==0.12
|
aio_geojson_geonetnz_quakes==0.12
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,9 @@ adb-shell==0.1.3
|
||||||
# homeassistant.components.adguard
|
# homeassistant.components.adguard
|
||||||
adguardhome==0.4.2
|
adguardhome==0.4.2
|
||||||
|
|
||||||
|
# homeassistant.components.agent_dvr
|
||||||
|
agent-py==0.0.20
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_quakes
|
# homeassistant.components.geonetnz_quakes
|
||||||
aio_geojson_geonetnz_quakes==0.12
|
aio_geojson_geonetnz_quakes==0.12
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""Tests for the agent_dvr component."""
|
||||||
|
|
||||||
|
from homeassistant.components.agent_dvr.const import DOMAIN, SERVER_URL
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the Agent DVR integration in Home Assistant."""
|
||||||
|
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://example.local:8090/command.cgi?cmd=getStatus",
|
||||||
|
text=load_fixture("agent_dvr/status.json"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://example.local:8090/command.cgi?cmd=getObjects",
|
||||||
|
text=load_fixture("agent_dvr/objects.json"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id="c0715bba-c2d0-48ef-9e3e-bc81c9ea4447",
|
||||||
|
data={
|
||||||
|
CONF_HOST: "example.local",
|
||||||
|
CONF_PORT: 8090,
|
||||||
|
SERVER_URL: "http://example.local:8090/",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
if not skip_setup:
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return entry
|
|
@ -0,0 +1,90 @@
|
||||||
|
"""Tests for the Agent DVR config flow."""
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.agent_dvr import config_flow
|
||||||
|
from homeassistant.components.agent_dvr.const import SERVER_URL
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import init_integration
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that the user set up form is served."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
config_flow.DOMAIN, context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_device_exists_abort(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort flow if Agent device already configured."""
|
||||||
|
await init_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
config_flow.DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={CONF_HOST: "example.local", CONF_PORT: 8090},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connection_error(hass: HomeAssistant, aioclient_mock) -> None:
|
||||||
|
"""Test we show user form on Agent connection error."""
|
||||||
|
|
||||||
|
aioclient_mock.get("http://example.local:8090/command.cgi?cmd=getStatus", text="")
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
config_flow.DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={CONF_HOST: "example.local", CONF_PORT: 8090},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["errors"] == {"base": "device_unavailable"}
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_user_flow_implementation(
|
||||||
|
hass: HomeAssistant, aioclient_mock
|
||||||
|
) -> None:
|
||||||
|
"""Test the full manual user flow from start to finish."""
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://example.local:8090/command.cgi?cmd=getStatus",
|
||||||
|
text=load_fixture("agent_dvr/status.json"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://example.local:8090/command.cgi?cmd=getObjects",
|
||||||
|
text=load_fixture("agent_dvr/objects.json"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
config_flow.DOMAIN, context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["data"][CONF_HOST] == "example.local"
|
||||||
|
assert result["data"][CONF_PORT] == 8090
|
||||||
|
assert result["data"][SERVER_URL] == "http://example.local:8090/"
|
||||||
|
assert result["title"] == "DESKTOP"
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(config_flow.DOMAIN)
|
||||||
|
assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447"
|
|
@ -0,0 +1 @@
|
||||||
|
{"settings":{"canUpdate":true,"supportsPlugins":true,"isArmed":true,"background":"255,255,255"},"directories":[{"ID":0,"dir":"D:\\Projects\\agent-service\\AgentService\\Media\\WebServerRoot\\Media\\"}],"locations":[],"objectList": [],"profiles": [{"name":"Home","active":true,"id":0},{"name":"Away","active":false,"id":1},{"name":"Night","active":false,"id":2}],"views":[{"name":"0","mode":"column","objects":[],"maxWidth":1266,"maxHeight":1222,"backColor":"#222222","id":1,"typeID":2,"focused":false},{"name":"1","mode":"grid","objects":[]},{"name":"2","mode":"grid","objects":[]},{"name":"3","mode":"grid","objects":[]},{"name":"4","mode":"grid","objects":[]},{"name":"5","mode":"grid","objects":[]},{"name":"6","mode":"grid","objects":[]},{"name":"7","mode":"grid","objects":[]},{"name":"8","mode":"grid","objects":[]}],"rtmpStreaming":false}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"armed": false,
|
||||||
|
"devices": 4,
|
||||||
|
"active": 4,
|
||||||
|
"recording": 0,
|
||||||
|
"remoteAccess": true,
|
||||||
|
"unique": "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447",
|
||||||
|
"name": "DESKTOP",
|
||||||
|
"version": "2.6.1.0"
|
||||||
|
}
|
Loading…
Reference in New Issue