-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add agent_dvr integration #32711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add agent_dvr integration #32711
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
ca25dfc
initial
ispysoftware 9d92b30
add missing fixture
ispysoftware 25c01d9
fix mocks
ispysoftware a58510d
fix mocks 2
ispysoftware e3e82e5
update coverage
ispysoftware 1d1b364
fix broken sync between agent and integration
ispysoftware 79fd81c
Update homeassistant/components/agent_dvr/camera.py
ispysoftware 0cad5c8
Update homeassistant/components/agent_dvr/camera.py
ispysoftware 4e73c18
Update homeassistant/components/agent_dvr/camera.py
ispysoftware fb76a64
Update homeassistant/components/agent_dvr/camera.py
ispysoftware 2860005
updates for review
ispysoftware 7dbaecc
add back in should poll again
ispysoftware 675655b
revert motion detection enabled flag in state attributes
ispysoftware b338409
Update homeassistant/components/agent_dvr/camera.py
ispysoftware f6328c4
Update homeassistant/components/agent_dvr/camera.py
ispysoftware 1224e7d
Update homeassistant/components/agent_dvr/camera.py
ispysoftware d75d4ab
Update homeassistant/components/agent_dvr/camera.py
ispysoftware ed7d7e4
Update homeassistant/components/agent_dvr/__init__.py
ispysoftware e08c7ff
Update homeassistant/components/agent_dvr/camera.py
ispysoftware 51552d1
Update homeassistant/components/agent_dvr/camera.py
ispysoftware e7d873c
Update homeassistant/components/agent_dvr/camera.py
ispysoftware 50f995e
add is_streaming
ispysoftware 4b73d4c
Merge branch 'agent' of https://github.com/ispysoftware/home-assistan…
ispysoftware 8fba4c7
fix is_streaming bug, remove mp4 stream
ispysoftware 04cc7d0
cleanup
ispysoftware File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
|
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) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.