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:
Marcelo Moreira de Mello 2018-06-12 02:01:26 -04:00 committed by Martin Hjelmare
parent be4776d039
commit cdc5388dc9
5 changed files with 116 additions and 84 deletions

View File

@ -4,15 +4,17 @@ Support for Arlo Alarm Control Panels.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.arlo/ https://home-assistant.io/components/alarm_control_panel.arlo/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv 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 ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA) 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 ( from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED) STATE_ALARM_DISARMED)
@ -36,21 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Arlo Alarm Control Panels.""" """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 return
home_mode_name = config.get(CONF_HOME_MODE_NAME) home_mode_name = config.get(CONF_HOME_MODE_NAME)
away_mode_name = config.get(CONF_AWAY_MODE_NAME) away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = [] 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, base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name)) away_mode_name))
async_add_devices(base_stations, True) add_devices(base_stations, True)
class ArloBaseStation(AlarmControlPanel): class ArloBaseStation(AlarmControlPanel):
@ -68,6 +69,16 @@ class ArloBaseStation(AlarmControlPanel):
"""Return icon.""" """Return icon."""
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 @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
@ -75,30 +86,22 @@ class ArloBaseStation(AlarmControlPanel):
def update(self): def update(self):
"""Update the state of the device.""" """Update the state of the device."""
# PyArlo sometimes returns None for mode. So retry 3 times before _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name)
# returning None. mode = self._base_station.mode
num_retries = 3 if mode:
i = 0 self._state = self._get_state_from_mode(mode)
while i < num_retries: else:
mode = self._base_station.mode self._state = None
if mode:
self._state = self._get_state_from_mode(mode)
return
i += 1
self._state = None
@asyncio.coroutine async def async_alarm_disarm(self, code=None):
def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
self._base_station.mode = DISARMED self._base_station.mode = DISARMED
@asyncio.coroutine async def async_alarm_arm_away(self, code=None):
def async_alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode.""" """Send arm away command. Uses custom mode."""
self._base_station.mode = self._away_mode_name self._base_station.mode = self._away_mode_name
@asyncio.coroutine async def async_alarm_arm_home(self, code=None):
def async_alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode.""" """Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name self._base_station.mode = self._home_mode_name
@ -125,4 +128,4 @@ class ArloBaseStation(AlarmControlPanel):
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif mode == self._away_mode_name: elif mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
return None return mode

View File

@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/arlo/ https://home-assistant.io/components/arlo/
""" """
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)
@ -25,10 +29,16 @@ DOMAIN = 'arlo'
NOTIFICATION_ID = 'arlo_notification' NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Component Setup' NOTIFICATION_TITLE = 'Arlo Component Setup'
SCAN_INTERVAL = timedelta(seconds=60)
SIGNAL_UPDATE_ARLO = "arlo_update"
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -38,6 +48,7 @@ def setup(hass, config):
conf = config[DOMAIN] conf = config[DOMAIN]
username = conf.get(CONF_USERNAME) username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD) password = conf.get(CONF_PASSWORD)
scan_interval = conf.get(CONF_SCAN_INTERVAL)
try: try:
from pyarlo import PyArlo from pyarlo import PyArlo
@ -45,7 +56,17 @@ def setup(hass, config):
arlo = PyArlo(username, password, preload=False) arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected: if not arlo.is_connected:
return False 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 hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex: except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
hass.components.persistent_notification.create( hass.components.persistent_notification.create(
@ -55,4 +76,17 @@ def setup(hass, config):
title=NOTIFICATION_TITLE, title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID) notification_id=NOTIFICATION_ID)
return False 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 return True

View File

@ -4,23 +4,22 @@ Support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.arlo/ https://home-assistant.io/components/camera.arlo/
""" """
import asyncio
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv 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.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=90)
ARLO_MODE_ARMED = 'armed' ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed' ARLO_MODE_DISARMED = 'disarmed'
@ -44,22 +43,19 @@ POWERSAVE_MODE_MAPPING = {
} }
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS): vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
cv.string,
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Arlo IP Camera.""" """Set up an Arlo IP Camera."""
arlo = hass.data.get(DATA_ARLO) arlo = hass.data[DATA_ARLO]
if not arlo:
return False
cameras = [] cameras = []
for camera in arlo.cameras: for camera in arlo.cameras:
cameras.append(ArloCam(hass, camera, config)) cameras.append(ArloCam(hass, camera, config))
add_devices(cameras, True) add_devices(cameras)
class ArloCam(Camera): class ArloCam(Camera):
@ -74,31 +70,41 @@ class ArloCam(Camera):
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_refresh = None self._last_refresh = None
if self._camera.base_station:
self._camera.base_station.refresh_rate = \
SCAN_INTERVAL.total_seconds()
self.attrs = {} self.attrs = {}
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
return self._camera.last_image return self._camera.last_image_from_cache
@asyncio.coroutine async def async_added_to_hass(self):
def handle_async_mjpeg_stream(self, request): """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.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpeg
video = self._camera.last_video video = self._camera.last_video
if not 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 return
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) 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) video.video_url, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream( await async_aiohttp_proxy_stream(
self.hass, request, stream, self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver') 'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close() await stream.close()
@property @property
def name(self): def name(self):
@ -132,11 +138,6 @@ class ArloCam(Camera):
"""Return the camera brand.""" """Return the camera brand."""
return DEFAULT_BRAND return DEFAULT_BRAND
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
@ -164,7 +165,3 @@ class ArloCam(Camera):
"""Disable the motion detection in base station (Disarm).""" """Disable the motion detection in base station (Disarm)."""
self._motion_status = False self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED) self.set_base_station_mode(ARLO_MODE_DISARMED)
def update(self):
"""Add an attribute-update task to the executor pool."""
self._camera.update()

View File

@ -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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.arlo/ https://home-assistant.io/components/sensor.arlo/
""" """
import asyncio
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.arlo import ( 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.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS) 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.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.icon import icon_for_battery_level
@ -22,8 +22,6 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['arlo'] DEPENDENCIES = ['arlo']
SCAN_INTERVAL = timedelta(seconds=90)
# sensor_type [ description, unit, icon ] # sensor_type [ description, unit, icon ]
SENSOR_TYPES = { SENSOR_TYPES = {
'last_capture': ['Last', None, 'run-fast'], 'last_capture': ['Last', None, 'run-fast'],
@ -39,8 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up an Arlo IP sensor.""" """Set up an Arlo IP sensor."""
arlo = hass.data.get(DATA_ARLO) arlo = hass.data.get(DATA_ARLO)
if not 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): for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type == 'total_cameras': if sensor_type == 'total_cameras':
sensors.append(ArloSensor( sensors.append(ArloSensor(
hass, SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) SENSOR_TYPES[sensor_type][0], arlo, sensor_type))
else: else:
for camera in arlo.cameras: for camera in arlo.cameras:
name = '{0} {1}'.format( name = '{0} {1}'.format(
SENSOR_TYPES[sensor_type][0], camera.name) 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): class ArloSensor(Entity):
"""An implementation of a Netgear Arlo IP sensor.""" """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.""" """Initialize an Arlo sensor."""
super().__init__()
self._name = name self._name = name
self._hass = hass
self._data = device self._data = device
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._state = None self._state = None
@ -78,6 +73,16 @@ class ArloSensor(Entity):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name 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 @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
@ -98,18 +103,7 @@ class ArloSensor(Entity):
def update(self): def update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""
try: _LOGGER.debug("Updating Arlo sensor %s", self.name)
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()
if self._sensor_type == 'total_cameras': if self._sensor_type == 'total_cameras':
self._state = len(self._data.cameras) self._state = len(self._data.cameras)
@ -118,9 +112,13 @@ class ArloSensor(Entity):
elif self._sensor_type == 'last_capture': elif self._sensor_type == 'last_capture':
try: try:
video = self._data.videos()[0] video = self._data.last_video
self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S")
except (AttributeError, IndexError): 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 self._state = None
elif self._sensor_type == 'battery_level': elif self._sensor_type == 'battery_level':

View File

@ -734,7 +734,7 @@ pyairvisual==1.0.0
pyalarmdotcom==0.3.2 pyalarmdotcom==0.3.2
# homeassistant.components.arlo # homeassistant.components.arlo
pyarlo==0.1.2 pyarlo==0.1.6
# homeassistant.components.notify.xmpp # homeassistant.components.notify.xmpp
pyasn1-modules==0.1.5 pyasn1-modules==0.1.5