diff --git a/.gitignore b/.gitignore index 894a44cc..50164225 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,7 @@ venv.bak/ # mypy .mypy_cache/ + +# backups +*~ +.*.sw? diff --git a/aarlo/__init__.py b/aarlo/__init__.py index d0023967..5b3d6e66 100644 --- a/aarlo/__init__.py +++ b/aarlo/__init__.py @@ -26,13 +26,26 @@ NOTIFICATION_ID = 'aarlo_notification' NOTIFICATION_TITLE = 'aarlo Component Setup' -SCAN_INTERVAL = timedelta(seconds=60) +CONF_PACKET_DUMP = 'packet_dump' +CONF_CACHE_VIDEOS = 'cache_videos' +CONF_DB_MOTION_TIME = 'db_motion_time' +CONF_DB_DING_TIME = 'db_ding_time' + +SCAN_INTERVAL = timedelta(seconds=60) +PACKET_DUMP = False +CACHE_VIDEOS = False +DB_MOTION_TIME = timedelta(seconds=30) +DB_DING_TIME = timedelta(seconds=10) 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, + vol.Optional(CONF_PACKET_DUMP, default=PACKET_DUMP): cv.boolean, + vol.Optional(CONF_CACHE_VIDEOS, default=CACHE_VIDEOS): cv.boolean, + vol.Optional(CONF_DB_MOTION_TIME, default=DB_MOTION_TIME): cv.time_period, + vol.Optional(CONF_DB_DING_TIME, default=DB_DING_TIME): cv.time_period, }), }, extra=vol.ALLOW_EXTRA) @@ -40,13 +53,19 @@ def setup(hass, config): """Set up an Arlo component.""" conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + packet_dump = conf.get(CONF_PACKET_DUMP) + cache_videos = conf.get(CONF_CACHE_VIDEOS) + motion_time = conf.get(CONF_DB_MOTION_TIME).total_seconds() + ding_time = conf.get(CONF_DB_DING_TIME).total_seconds() try: - from custom_components.aarlo.pyarlo import PyArlo + from custom_components.aarlo.pyaarlo import PyArlo - arlo = PyArlo( username,password ) + arlo = PyArlo( username,password, + dump=packet_dump, + db_motion_time=motion_time,db_ding_time=ding_time ) if not arlo.is_connected: return False diff --git a/aarlo/pyarlo/__init__.py b/aarlo/pyaarlo/__init__.py similarity index 81% rename from aarlo/pyarlo/__init__.py rename to aarlo/pyaarlo/__init__.py index efadb947..eeffa0bc 100644 --- a/aarlo/pyarlo/__init__.py +++ b/aarlo/pyaarlo/__init__.py @@ -1,4 +1,5 @@ +import os import logging import time import datetime @@ -6,30 +7,38 @@ import pprint import threading -from custom_components.aarlo.pyarlo.background import ArloBackground -from custom_components.aarlo.pyarlo.storage import ArloStorage -from custom_components.aarlo.pyarlo.backend import ArloBackEnd -from custom_components.aarlo.pyarlo.media import ArloMediaLibrary -from custom_components.aarlo.pyarlo.base import ArloBase -from custom_components.aarlo.pyarlo.camera import ArloCamera -from custom_components.aarlo.pyarlo.doorbell import ArloDoorBell +from custom_components.aarlo.pyaarlo.background import ArloBackground +from custom_components.aarlo.pyaarlo.storage import ArloStorage +from custom_components.aarlo.pyaarlo.backend import ArloBackEnd +from custom_components.aarlo.pyaarlo.media import ArloMediaLibrary +from custom_components.aarlo.pyaarlo.base import ArloBase +from custom_components.aarlo.pyaarlo.camera import ArloCamera +from custom_components.aarlo.pyaarlo.doorbell import ArloDoorBell -from custom_components.aarlo.pyarlo.constant import ( BLANK_IMAGE, +from custom_components.aarlo.pyaarlo.constant import ( BLANK_IMAGE, DEVICE_KEYS, DEVICES_URL, TOTAL_BELLS_KEY, TOTAL_CAMERAS_KEY ) -_LOGGER = logging.getLogger('cc.aarlo.pyarlo') +_LOGGER = logging.getLogger('pyaarlo') class PyArlo(object): - def __init__( self,username,password,name='arlo',store='/config/state-arlo',preload=False ): + def __init__( self,username,password,name='aarlo', + storage_dir='/config/.aarlo',dump=False,max_days=365, + db_motion_time=30,db_ding_time=10 ): + + try: + os.mkdir( storage_dir ) + except: + pass + self._name = name self._bg = ArloBackground( self ) - self._st = ArloStorage( self,store ) - self._be = ArloBackEnd( self,username,password ) - self._ml = ArloMediaLibrary( self ) + self._st = ArloStorage( self,name=name,storage_dir=storage_dir ) + self._be = ArloBackEnd( self,username,password,dump=dump,storage_dir=storage_dir ) + self._ml = ArloMediaLibrary( self,max_days=max_days ) self._lock = threading.Lock() self._bases = [] self._cameras = [] @@ -53,10 +62,11 @@ def __init__( self,username,password,name='arlo',store='/config/state-arlo',prel self.info('skipping ' + dname + ': state unknown') elif dtype == 'basestation': self._bases.append( ArloBase( dname,self,device ) ) - elif dtype == 'camera': + elif dtype == 'camera' or dtype == 'arloq' or dtype == 'arloqs': self._cameras.append( ArloCamera( dname,self,device ) ) elif dtype == 'doorbell': - self._doorbells.append( ArloDoorBell( dname,self,device ) ) + self._doorbells.append( ArloDoorBell( dname,self,device, + motion_time=db_motion_time,ding_time=db_ding_time ) ) # save out unchanging stats! self._st.set( ['ARLO',TOTAL_CAMERAS_KEY],len(self._cameras) ) diff --git a/aarlo/pyarlo/backend.py b/aarlo/pyaarlo/backend.py similarity index 95% rename from aarlo/pyarlo/backend.py rename to aarlo/pyaarlo/backend.py index b36558d5..2baf9089 100644 --- a/aarlo/pyarlo/backend.py +++ b/aarlo/pyaarlo/backend.py @@ -6,8 +6,8 @@ import requests import pprint -from custom_components.aarlo.pyarlo.sseclient import ( SSEClient ) -from custom_components.aarlo.pyarlo.constant import ( LOGOUT_URL,NOTIFY_URL, +from custom_components.aarlo.pyaarlo.sseclient import ( SSEClient ) +from custom_components.aarlo.pyaarlo.constant import ( LOGOUT_URL,NOTIFY_URL, SUBSCRIBE_URL, UNSUBSCRIBE_URL, TRANSID_PREFIX ) @@ -15,13 +15,16 @@ # include token and session details class ArloBackEnd(object): - def __init__( self,arlo,username,password ): + def __init__( self,arlo,username,password,dump,storage_dir ): self._arlo = arlo self.session = requests.Session() self.request_lock_ = threading.Lock() self.lock_ = threading.Condition() + self._dump = dump + self._dump_file = storage_dir + '/' + 'packets.dump' + self.requests_ = {} self.subscriptions_ = {} self.callbacks_ = {} @@ -126,8 +129,9 @@ def _ev_loop( self,stream ): break response = json.loads( event.data ) - with open('/config/packets.dump','a') as dump: - dump.write( pprint.pformat( response,indent=2 ) + '\n' ) + if self._dump: + with open( self._dump_file,'a' ) as dump: + dump.write( pprint.pformat( response,indent=2 ) + '\n' ) # logged out? signal exited if response.get('action') == 'logout': diff --git a/aarlo/pyarlo/background.py b/aarlo/pyaarlo/background.py similarity index 86% rename from aarlo/pyarlo/background.py rename to aarlo/pyaarlo/background.py index 776dfb0c..a4bd8ff5 100644 --- a/aarlo/pyarlo/background.py +++ b/aarlo/pyaarlo/background.py @@ -12,7 +12,7 @@ def __init__( self ): def _next_id( self ): self._id += 1 - return str(self._id) + ':' + str(time.monotonic) + return str(self._id) + ':' + str(time.monotonic()) def _run_next( self ): @@ -72,6 +72,17 @@ def queue_job( self,run_at,prio,job ): self._lock.notify() return job_id + def stop_job( self,to_delete ): + with self._lock: + for prio in self._queue.keys(): + for run_at,job_id in self._queue[prio].keys(): + if job_id == to_delete: + #print( 'cancelling ' + str(job_id) ) + del self._queue[prio][ (run_at,job_id) ] + return True + return False + + class ArloBackground(threading.Thread): def __init__( self,arlo ): @@ -115,3 +126,7 @@ def run_every( self,cb,seconds,**kwargs): def run_low_every( self,cb,seconds,**kwargs): return self._run_every( cb,99,seconds,**kwargs) + def cancel( self,to_delete ): + if to_delete is not None: + self._worker.stop_job( to_delete ) + diff --git a/aarlo/pyarlo/base.py b/aarlo/pyaarlo/base.py similarity index 96% rename from aarlo/pyarlo/base.py rename to aarlo/pyaarlo/base.py index 5d828862..9ccba120 100644 --- a/aarlo/pyarlo/base.py +++ b/aarlo/pyaarlo/base.py @@ -1,8 +1,8 @@ import pprint -from custom_components.aarlo.pyarlo.device import ArloDevice +from custom_components.aarlo.pyaarlo.device import ArloDevice -from custom_components.aarlo.pyarlo.constant import ( DEFAULT_MODES, +from custom_components.aarlo.pyaarlo.constant import ( DEFAULT_MODES, MODES_KEY, MODE_ID_TO_NAME_KEY, MODE_KEY, diff --git a/aarlo/pyarlo/camera.py b/aarlo/pyaarlo/camera.py similarity index 94% rename from aarlo/pyarlo/camera.py rename to aarlo/pyaarlo/camera.py index 7a5bfa1e..f7b0abc6 100644 --- a/aarlo/pyarlo/camera.py +++ b/aarlo/pyaarlo/camera.py @@ -2,9 +2,9 @@ import threading import pprint -from custom_components.aarlo.pyarlo.device import ArloChildDevice -from custom_components.aarlo.pyarlo.util import ( http_get ) -from custom_components.aarlo.pyarlo.constant import( BRIGHTNESS_KEY, +from custom_components.aarlo.pyaarlo.device import ArloChildDevice +from custom_components.aarlo.pyaarlo.util import ( http_get ) +from custom_components.aarlo.pyaarlo.constant import( BRIGHTNESS_KEY, CAPTURED_TODAY_KEY, FLIP_KEY, LAST_CAPTURE_KEY, @@ -154,7 +154,7 @@ def update_last_image( self ): def has_capability( self,cap ): if cap in ( 'last_capture','captured_today','battery_level','signal_strength' ): return True - if cap in ( 'audioDetected' ) and self.model_id.startswith('VMC4030'): + if cap in ( 'audio','audioDetected','sound' ) and self.model_id.startswith('VMC4030'): return True return super().has_capability( cap ) diff --git a/aarlo/pyarlo/constant.py b/aarlo/pyaarlo/constant.py similarity index 89% rename from aarlo/pyarlo/constant.py rename to aarlo/pyaarlo/constant.py index f89074c1..94bc1c36 100644 --- a/aarlo/pyarlo/constant.py +++ b/aarlo/pyaarlo/constant.py @@ -25,9 +25,10 @@ SIGNAL_STR_KEY = 'signalStrength' # we can get these from the resource; doorbell is subset -RESOURCE_KEYS = [ BATTERY_KEY, BRIGHTNESS_KEY, CONNECTION_KEY, - FLIP_KEY, MIRROR_KEY, MOTION_ENABLED_KEY, MOTION_SENS_KEY, - POWER_SAVE_KEY, SIGNAL_STR_KEY ] +RESOURCE_KEYS = [ ACTIVITY_STATE, AUDIO_DETECTED_KEY, BATTERY_KEY, + BRIGHTNESS_KEY, CONNECTION_KEY, FLIP_KEY, + MIRROR_KEY, MOTION_DETECTED_KEY, MOTION_ENABLED_KEY, + MOTION_SENS_KEY, POWER_SAVE_KEY, SIGNAL_STR_KEY ] RESOURCE_UPDATE_KEYS = [ ACTIVITY_STATE, AUDIO_DETECTED_KEY, BATTERY_KEY, MOTION_DETECTED_KEY, SIGNAL_STR_KEY ] diff --git a/aarlo/pyarlo/device.py b/aarlo/pyaarlo/device.py similarity index 97% rename from aarlo/pyarlo/device.py rename to aarlo/pyaarlo/device.py index 436b7813..e6a23b24 100644 --- a/aarlo/pyarlo/device.py +++ b/aarlo/pyaarlo/device.py @@ -1,9 +1,9 @@ import pprint import threading -import custom_components.aarlo.pyarlo.storage +import custom_components.aarlo.pyaarlo.storage -from custom_components.aarlo.pyarlo.constant import( BATTERY_KEY, +from custom_components.aarlo.pyaarlo.constant import( BATTERY_KEY, PARENT_ID_KEY, RESOURCE_KEYS, RESOURCE_UPDATE_KEYS, diff --git a/aarlo/pyarlo/doorbell.py b/aarlo/pyaarlo/doorbell.py similarity index 64% rename from aarlo/pyarlo/doorbell.py rename to aarlo/pyaarlo/doorbell.py index 2e6f474d..9fd5ebc0 100644 --- a/aarlo/pyarlo/doorbell.py +++ b/aarlo/pyaarlo/doorbell.py @@ -1,17 +1,25 @@ import pprint -from custom_components.aarlo.pyarlo.device import ArloChildDevice +from custom_components.aarlo.pyaarlo.device import ArloChildDevice class ArloDoorBell(ArloChildDevice): - def __init__( self,name,arlo,attrs ): + def __init__( self,name,arlo,attrs,motion_time,ding_time ): super().__init__( name,arlo,attrs ) + self._mt = motion_time + self._mt_job = None + self._dt = ding_time + self._dt_job = None def _motion_stopped( self ): self._save_and_do_callbacks( 'motionDetected',False ) + with self._lock: + self._mt_job = None def _button_unpressed( self ): self._save_and_do_callbacks( 'buttonPressed',False ) + with self._lock: + self._dt_job = None def _event_handler( self,resource,event ): self._arlo.info( self.name + ' DOORBELL got one ' + resource ) @@ -23,10 +31,14 @@ def _event_handler( self,resource,event ): #acts = event.get('properties',{}).get('activityState',False) if cons and cons == 'available': self._save_and_do_callbacks( 'motionDetected',True ) - self._arlo._bg.run_in( self._motion_stopped,15 ) + with self._lock: + self._arlo._bg.cancel( self._mt_job ) + self._mt_job = self._arlo._bg.run_in( self._motion_stopped,self._mt ) if butp: self._save_and_do_callbacks( 'buttonPressed',True ) - self._arlo._bg.run_in( self._button_unpressed,5 ) + with self._lock: + self._arlo._bg.cancel( self._dt_job ) + self._dt_job = self._arlo._bg.run_in( self._button_unpressed,self._dt ) # if acts and acts == 'idle': # self._save_and_do_callbacks( 'motionDetected',False ) # self._save_and_do_callbacks( 'buttonPressed',False ) diff --git a/aarlo/pyarlo/media.py b/aarlo/pyaarlo/media.py similarity index 96% rename from aarlo/pyarlo/media.py rename to aarlo/pyaarlo/media.py index 41bbbd5a..3814e4ec 100644 --- a/aarlo/pyarlo/media.py +++ b/aarlo/pyaarlo/media.py @@ -3,9 +3,9 @@ import threading from datetime import datetime from datetime import timedelta -from custom_components.aarlo.pyarlo.constant import ( LIBRARY_URL, +from custom_components.aarlo.pyaarlo.constant import ( LIBRARY_URL, PRELOAD_DAYS ) -from custom_components.aarlo.pyarlo.util import ( arlotime_strftime, +from custom_components.aarlo.pyaarlo.util import ( arlotime_strftime, arlotime_to_datetime, http_stream, http_get ) @@ -13,7 +13,7 @@ class ArloMediaLibrary(object): """Arlo Library Media module implementation.""" - def __init__( self,arlo ): + def __init__( self,arlo,max_days ): self._arlo = arlo self._lock = threading.Lock() self._load_cbs_ = [] diff --git a/aarlo/pyarlo/sseclient.py b/aarlo/pyaarlo/sseclient.py similarity index 100% rename from aarlo/pyarlo/sseclient.py rename to aarlo/pyaarlo/sseclient.py diff --git a/aarlo/pyarlo/storage.py b/aarlo/pyaarlo/storage.py similarity index 95% rename from aarlo/pyarlo/storage.py rename to aarlo/pyaarlo/storage.py index 786ecc04..0f39770e 100644 --- a/aarlo/pyarlo/storage.py +++ b/aarlo/pyaarlo/storage.py @@ -6,9 +6,9 @@ class ArloStorage(object): - def __init__( self,arlo,file_name ): + def __init__( self,arlo,name,storage_dir ): self._arlo = arlo - self.file = file_name + '.pickle' + self.file = storage_dir + '/' + name + '.pickle' self.db = {} self.lock = threading.Lock() self.load() diff --git a/aarlo/pyarlo/util.py b/aarlo/pyaarlo/util.py similarity index 100% rename from aarlo/pyarlo/util.py rename to aarlo/pyaarlo/util.py diff --git a/camera/aarlo.py b/camera/aarlo.py index cd79c77a..1f3b7279 100644 --- a/camera/aarlo.py +++ b/camera/aarlo.py @@ -71,7 +71,6 @@ def __init__(self, hass, camera, device_info): self._unique_id = self._name.lower().replace(' ','_') self._camera = camera self._state = None - self._on = True self._motion_status = False self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) @@ -86,11 +85,16 @@ async def async_added_to_hass(self): def update_state( device,attr,value ): _LOGGER.info( 'callback:' + self._name + ':' + attr + ':' + str(value)[:80]) - # set state - if attr == 'activityState': - self._state = value - if attr == 'connectionState': - self._on = value != 'thermalShutdownCold' + # set state + if attr == 'activityState' or attr == 'connectionState': + if value == 'thermalShutdownCold': + self._state = 'Offline, Too Cold' + elif value == 'userStreamActive': + self._state = STATE_STREAMING + elif value == 'alertStreamActive': + self._state = STATE_RECORDING + else: + self._state = STATE_IDLE self.async_schedule_update_ha_state() @@ -128,20 +132,16 @@ def unique_id(self): @property def is_recording(self): - return self._state == 'alertStreamActive' + return self._state == STATE_RECORDING @property def is_on(self): - return self._on + return True @property def state(self): """Return the camera state.""" - if self.is_recording: - return STATE_RECORDING - if self._state == 'userStreamActive': - return STATE_STREAMING - return STATE_IDLE + return self._state @property def device_state_attributes(self): diff --git a/fix_imports b/fix_imports new file mode 100755 index 00000000..9e1bb48f --- /dev/null +++ b/fix_imports @@ -0,0 +1,6 @@ +#!/bin/bash +# +# small script to fix pyaarlo imports while testing hass-aarlo + +sed -i.old -e 's@from pyaarlo@from custom_components.aarlo.pyaarlo@' *py +sed -i.old -e 's@import pyaarlo@import custom_components.aarlo.pyaarlo@' *py diff --git a/install b/install new file mode 100755 index 00000000..669cd64f --- /dev/null +++ b/install @@ -0,0 +1,30 @@ +#!/bin/bash +# + +if [[ "${1}" == "go" ]]; then + ECHO= + shift +else + ECHO=echo +fi + +DEST="${1}" +if [[ -z "${DEST}" ]]; then + echo "*** please supply the custom_components directory" + exit 1 +fi +if [[ ! -d "${DEST}" ]]; then + echo "*** please make sure the destination directory exists" + exit 1 +fi + +if [[ -n "${ECHO}" ]]; then + echo "**** would run the following commands, use './install go $1' to do the work" +fi + +${ECHO} cp -afv aarlo "${DEST}" +${ECHO} cp -afv alarm_control_panel "${DEST}" +${ECHO} cp -afv binary_sensor "${DEST}" +${ECHO} cp -afv camera "${DEST}" +${ECHO} cp -afv sensor "${DEST}" +