mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Refactored Arlo component and enhanced Arlo API queries and times (#14823)
* start arlo refactoring * Refactored Arlo Hub to avoid uncessary and duplicated GETs to Arlo API * Refactored Arlo camera component to avoid duplicate queries * Added debug and error messages when video is not found * Transformed Arlo Control Panel to Sync * Makes linter happy * Uses total_seconds() for scan_interval * Added callback and fixed scan_interval issue * Disable multiple tries and supported custom modes set in Arlo * Bump PyArlo version to 0.1.4 * Makes lint happy * Removed ArloHub object and added some tweaks * Fixed hub_refresh method * Makes lint happy * Ajusted async syntax and added callbacks decorators * Bump PyArlo version to 0.1.6 to include some enhacements * Refined code
This commit is contained in:
parent
be4776d039
commit
cdc5388dc9
@ -4,15 +4,17 @@ Support for Arlo Alarm Control Panels.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
|
||||
from homeassistant.components.arlo import (
|
||||
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
@ -36,21 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Arlo Alarm Control Panels."""
|
||||
data = hass.data[DATA_ARLO]
|
||||
arlo = hass.data[DATA_ARLO]
|
||||
|
||||
if not data.base_stations:
|
||||
if not arlo.base_stations:
|
||||
return
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in data.base_stations:
|
||||
for base_station in arlo.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
async_add_devices(base_stations, True)
|
||||
add_devices(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
@ -68,6 +69,16 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
"""Return icon."""
|
||||
return ICON
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
@ -75,30 +86,22 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
# PyArlo sometimes returns None for mode. So retry 3 times before
|
||||
# returning None.
|
||||
num_retries = 3
|
||||
i = 0
|
||||
while i < num_retries:
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
return
|
||||
i += 1
|
||||
self._state = None
|
||||
_LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name)
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._base_station.mode = DISARMED
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command. Uses custom mode."""
|
||||
self._base_station.mode = self._away_mode_name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command. Uses custom mode."""
|
||||
self._base_station.mode = self._home_mode_name
|
||||
|
||||
@ -125,4 +128,4 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
return mode
|
||||
|
@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.2']
|
||||
REQUIREMENTS = ['pyarlo==0.1.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -25,10 +29,16 @@ DOMAIN = 'arlo'
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SIGNAL_UPDATE_ARLO = "arlo_update"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -38,6 +48,7 @@ def setup(hass, config):
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
scan_interval = conf.get(CONF_SCAN_INTERVAL)
|
||||
|
||||
try:
|
||||
from pyarlo import PyArlo
|
||||
@ -45,7 +56,17 @@ def setup(hass, config):
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
|
||||
# assign refresh period to base station thread
|
||||
arlo_base_station = next((
|
||||
station for station in arlo.base_stations), None)
|
||||
|
||||
if arlo_base_station is None:
|
||||
return False
|
||||
|
||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
@ -55,4 +76,17 @@ def setup(hass, config):
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
def hub_refresh(event_time):
|
||||
"""Call ArloHub to refresh information."""
|
||||
_LOGGER.info("Updating Arlo Hub component")
|
||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||
update_base_station=True)
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||
|
||||
# register service
|
||||
hass.services.register(DOMAIN, 'update', hub_refresh)
|
||||
|
||||
# register scan interval for ArloHub
|
||||
track_time_interval(hass, hub_refresh, scan_interval)
|
||||
return True
|
||||
|
@ -4,23 +4,22 @@ Support for Netgear Arlo IP cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
|
||||
from homeassistant.components.arlo import (
|
||||
DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
@ -44,22 +43,19 @@ POWERSAVE_MODE_MAPPING = {
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
return False
|
||||
arlo = hass.data[DATA_ARLO]
|
||||
|
||||
cameras = []
|
||||
for camera in arlo.cameras:
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
add_devices(cameras, True)
|
||||
add_devices(cameras)
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
@ -74,31 +70,41 @@ class ArloCam(Camera):
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_refresh = None
|
||||
if self._camera.base_station:
|
||||
self._camera.base_station.refresh_rate = \
|
||||
SCAN_INTERVAL.total_seconds()
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
return self._camera.last_image
|
||||
return self._camera.last_image_from_cache
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
video = self._camera.last_video
|
||||
if not video:
|
||||
error_msg = \
|
||||
'Video not found for {0}. Is it older than {1} days?'.format(
|
||||
self.name, self._camera.min_days_vdo_cache)
|
||||
_LOGGER.error(error_msg)
|
||||
return
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
video.video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -132,11 +138,6 @@ class ArloCam(Camera):
|
||||
"""Return the camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
@ -164,7 +165,3 @@ class ArloCam(Camera):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
self.set_base_station_mode(ARLO_MODE_DISARMED)
|
||||
|
||||
def update(self):
|
||||
"""Add an attribute-update task to the executor pool."""
|
||||
self._camera.update()
|
||||
|
@ -4,17 +4,17 @@ This component provides HA sensor for Netgear Arlo IP cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.arlo import (
|
||||
CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO)
|
||||
CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
@ -22,8 +22,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['arlo']
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
# sensor_type [ description, unit, icon ]
|
||||
SENSOR_TYPES = {
|
||||
'last_capture': ['Last', None, 'run-fast'],
|
||||
@ -39,8 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP sensor."""
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
@ -50,24 +47,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
if sensor_type == 'total_cameras':
|
||||
sensors.append(ArloSensor(
|
||||
hass, SENSOR_TYPES[sensor_type][0], arlo, sensor_type))
|
||||
SENSOR_TYPES[sensor_type][0], arlo, sensor_type))
|
||||
else:
|
||||
for camera in arlo.cameras:
|
||||
name = '{0} {1}'.format(
|
||||
SENSOR_TYPES[sensor_type][0], camera.name)
|
||||
sensors.append(ArloSensor(hass, name, camera, sensor_type))
|
||||
sensors.append(ArloSensor(name, camera, sensor_type))
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class ArloSensor(Entity):
|
||||
"""An implementation of a Netgear Arlo IP sensor."""
|
||||
|
||||
def __init__(self, hass, name, device, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize an Arlo sensor."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._hass = hass
|
||||
self._data = device
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
@ -78,6 +73,16 @@ class ArloSensor(Entity):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
@ -98,18 +103,7 @@ class ArloSensor(Entity):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
try:
|
||||
base_station = self._data.base_station
|
||||
except (AttributeError, IndexError):
|
||||
return
|
||||
|
||||
if not base_station:
|
||||
return
|
||||
|
||||
base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
|
||||
|
||||
self._data.update()
|
||||
|
||||
_LOGGER.debug("Updating Arlo sensor %s", self.name)
|
||||
if self._sensor_type == 'total_cameras':
|
||||
self._state = len(self._data.cameras)
|
||||
|
||||
@ -118,9 +112,13 @@ class ArloSensor(Entity):
|
||||
|
||||
elif self._sensor_type == 'last_capture':
|
||||
try:
|
||||
video = self._data.videos()[0]
|
||||
video = self._data.last_video
|
||||
self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S")
|
||||
except (AttributeError, IndexError):
|
||||
error_msg = \
|
||||
'Video not found for {0}. Older than {1} days?'.format(
|
||||
self.name, self._data.min_days_vdo_cache)
|
||||
_LOGGER.debug(error_msg)
|
||||
self._state = None
|
||||
|
||||
elif self._sensor_type == 'battery_level':
|
||||
|
@ -734,7 +734,7 @@ pyairvisual==1.0.0
|
||||
pyalarmdotcom==0.3.2
|
||||
|
||||
# homeassistant.components.arlo
|
||||
pyarlo==0.1.2
|
||||
pyarlo==0.1.6
|
||||
|
||||
# homeassistant.components.notify.xmpp
|
||||
pyasn1-modules==0.1.5
|
||||
|
Loading…
x
Reference in New Issue
Block a user