-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Refactored Arlo component and enhanced Arlo API queries and times #14823
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
Changes from 13 commits
d6ac61a
ec1442f
3f3387a
e0a252f
0cc5079
e437a0c
1cbd2f8
63ae201
66a791d
cae9616
da74c93
377a9b7
b0c3d03
a83658c
6a0a9c8
db2d2ee
1f2f03b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,12 @@ | |
| 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 +39,20 @@ | |
| }) | ||
|
|
||
|
|
||
| @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,24 +70,29 @@ def icon(self): | |
| """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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass self.async_schedule_update_ha_state(True)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
|
|
||
| @property | ||
| def state(self): | ||
| """Return the state of the device.""" | ||
| return self._state | ||
|
|
||
| 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): | ||
|
|
@@ -125,4 +132,4 @@ def _get_state_from_mode(self, mode): | |
| return STATE_ALARM_ARMED_HOME | ||
| elif mode == self._away_mode_name: | ||
| return STATE_ALARM_ARMED_AWAY | ||
| return None | ||
| return mode | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,14 +5,18 @@ | |
| 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.4'] | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -25,10 +29,16 @@ | |
| 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,14 +48,25 @@ 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 | ||
|
|
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. continuation line over-indented for visual indent |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,21 +6,20 @@ | |
| """ | ||
| import asyncio | ||
| import logging | ||
| from datetime import timedelta | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| 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,14 +43,13 @@ | |
| } | ||
|
|
||
| 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) | ||
| arlo = hass.data[DATA_ARLO] | ||
| if not arlo: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can't happen.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this can happen? |
||
| return False | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only |
||
|
|
||
|
|
@@ -74,21 +72,32 @@ def __init__(self, hass, camera, device_info): | |
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update all coroutines to use new syntax. |
||
| def async_added_to_hass(self): | ||
| """Register callbacks.""" | ||
| async_dispatcher_connect( | ||
| self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) | ||
|
|
||
| def _update_callback(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update this like my previous comment. I haven't commented on all the places that needs the same changes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, my bad. |
||
| """Call update method.""" | ||
| self.schedule_update_ha_state(True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. |
||
|
|
||
| @asyncio.coroutine | ||
| 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) | ||
|
|
@@ -132,11 +141,6 @@ def brand(self): | |
| """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 +168,3 @@ def disable_motion_detection(self): | |
| """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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,24 +6,22 @@ | |
| """ | ||
| import asyncio | ||
| import logging | ||
| from datetime import timedelta | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| 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 | ||
|
|
||
| _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 @@ | |
| }) | ||
|
|
||
|
|
||
| @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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. |
||
|
|
@@ -50,24 +47,23 @@ 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) | ||
| return True | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this. |
||
|
|
||
|
|
||
| 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 +74,16 @@ def name(self): | |
| """Return the name of this camera.""" | ||
| return self._name | ||
|
|
||
| @asyncio.coroutine | ||
| def async_added_to_hass(self): | ||
| """Register callbacks.""" | ||
| async_dispatcher_connect( | ||
| self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) | ||
|
|
||
| def _update_callback(self): | ||
| """Call update method.""" | ||
| self.schedule_update_ha_state(True) | ||
|
|
||
| @property | ||
| def state(self): | ||
| """Return the state of the sensor.""" | ||
|
|
@@ -98,18 +104,7 @@ def unit_of_measurement(self): | |
|
|
||
| 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 +113,13 @@ def update(self): | |
|
|
||
| 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': | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decorate this with
@callbackimported from core.py.