From 590f110fbb5dac536e8c42dedd304d6ab47ff821 Mon Sep 17 00:00:00 2001 From: Jeff Culverhouse Date: Fri, 14 Mar 2025 13:27:50 -0400 Subject: [PATCH] add retries in a couple places; cleanup; new defaults --- VERSION | 2 +- amcrest_api.py | 93 ++++++++++++++++++++++++++++++------------------- amcrest_mqtt.py | 23 ++++++++---- app.py | 10 ++++-- 4 files changed, 81 insertions(+), 47 deletions(-) diff --git a/VERSION b/VERSION index 2306249..9aa7d10 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.99.25 +0.99.26 diff --git a/amcrest_api.py b/amcrest_api.py index 6d82b2d..08e1e4b 100644 --- a/amcrest_api.py +++ b/amcrest_api.py @@ -138,23 +138,24 @@ class AmcrestAPI(object): privacy = device["camera"].privacy_config().split() privacy_mode = True if privacy[0].split('=')[1] == 'true' else False device['privacy_mode'] = privacy_mode - - return privacy_mode except CommError as err: self.logger.error(f'Failed to communicate with device ({device_id}) to get privacy mode') except LoginError as err: self.logger.error(f'Failed to authenticate with device ({device_id}) to get privacy mode') + return privacy_mode + def set_privacy_mode(self, device_id, switch): device = self.devices[device_id] try: - return device["camera"].set_privacy(switch).strip() + response = device["camera"].set_privacy(switch).strip() except CommError as err: self.logger.error(f'Failed to communicate with device ({device_id}) to set privacy mode') except LoginError as err: self.logger.error(f'Failed to authenticate with device ({device_id}) to set privacy mode') + return response # Motion detection config --------------------------------------------------------------------- @@ -163,23 +164,25 @@ class AmcrestAPI(object): try: motion_detection = device["camera"].is_motion_detector_on() - - return motion_detection except CommError as err: self.logger.error(f'Failed to communicate with device ({device_id}) to get motion detection') except LoginError as err: self.logger.error(f'Failed to authenticate with device ({device_id}) to get motion detection') + return motion_detection + def set_motion_detection(self, device_id, switch): device = self.devices[device_id] try: - return device["camera"].set_motion_detection(switch) + response = device["camera"].set_motion_detection(switch) except CommError as err: self.logger.error(f'Failed to communicate with device ({device_id}) to set motion detections') except LoginError as err: self.logger.error(f'Failed to authenticate with device ({device_id}) to set motion detections') + return response + # Snapshots ----------------------------------------------------------------------------------- async def collect_all_device_snapshots(self): @@ -189,17 +192,24 @@ class AmcrestAPI(object): async def get_snapshot_from_device(self, device_id): device = self.devices[device_id] - try: - if 'privacy_mode' not in device or device['privacy_mode'] == False: - image = await device["camera"].async_snapshot() - device['snapshot'] = base64.b64encode(image) - self.logger.debug(f'Processed snapshot from ({device_id}) {len(image)} bytes raw, and {len(device['snapshot'])} bytes base64') - else: - self.logger.info(f'Skipped snapshot from ({device_id}) because "privacy mode" is ON') - except CommError as err: + tries = 0 + while tries < 3: + try: + if 'privacy_mode' not in device or device['privacy_mode'] == False: + image = await device["camera"].async_snapshot() + device['snapshot'] = base64.b64encode(image) + self.logger.debug(f'Processed snapshot from ({device_id}) {len(image)} bytes raw, and {len(device['snapshot'])} bytes base64') + break + else: + self.logger.info(f'Skipped snapshot from ({device_id}) because "privacy mode" is ON') + break + except CommError as err: + tries += 1 + except LoginError as err: + tries += 1 + + if tries == 3: self.logger.error(f'Failed to communicate with device ({device_id}) to get snapshot') - except LoginError as err: - self.logger.error(f'Failed to authenticate with device ({device_id}) to get snapshot') def get_snapshot(self, device_id): return self.devices[device_id]['snapshot'] if 'snapshot' in self.devices[device_id] else None @@ -208,20 +218,26 @@ class AmcrestAPI(object): def get_recorded_file(self, device_id, file): device = self.devices[device_id] - try: - data_raw = device["camera"].download_file(file) - if data_raw: - data_base64 = base64.b64encode(data_raw) - self.logger.info(f'Processed recording from ({device_id}) {len(data_raw)} bytes raw, and {len(data_base64)} bytes base64') - if len(data_base64) < 100 * 1024 * 1024 * 1024: - return data_base64 - else: - self.logger.error(f'Processed recording is too large') - return - except CommError as err: - self.logger.error(f'Failed to download recorded file for device ({device_id})') - except LoginError as err: - self.logger.error(f'Failed to authenticate for recorded file for device ({device_id})') + tries = 0 + while tries < 3: + try: + data_raw = device["camera"].download_file(file) + if data_raw: + data_base64 = base64.b64encode(data_raw) + self.logger.info(f'Processed recording from ({device_id}) {len(data_raw)} bytes raw, and {len(data_base64)} bytes base64') + if len(data_base64) < 100 * 1024 * 1024 * 1024: + return data_base64 + else: + self.logger.error(f'Processed recording is too large') + return + except CommError as err: + tries += 1 + except LoginError as err: + tries += 1 + + if tries == 3: + self.logger.error(f'Failed to communicate with device ({device_id}) to get recorded file') + # Events -------------------------------------------------------------------------------------- @@ -232,13 +248,18 @@ class AmcrestAPI(object): async def get_events_from_device(self, device_id): device = self.devices[device_id] - try: - async for code, payload in device["camera"].async_event_actions("All"): - await self.process_device_event(device_id, code, payload) - except CommError as err: + tries = 0 + while tries < 3: + try: + async for code, payload in device["camera"].async_event_actions("All"): + await self.process_device_event(device_id, code, payload) + except CommError as err: + tries += 1 + except LoginError as err: + tries += 1 + + if tries == 3: self.logger.error(f'Failed to communicate for events for device ({device_id})') - except LoginError as err: - self.logger.error(f'Failed to authenticate for events for device ({device_id})') async def process_device_event(self, device_id, code, payload): try: diff --git a/amcrest_mqtt.py b/amcrest_mqtt.py index 088acd0..168a2da 100644 --- a/amcrest_mqtt.py +++ b/amcrest_mqtt.py @@ -22,6 +22,7 @@ from zoneinfo import ZoneInfo class AmcrestMqtt(object): def __init__(self, config): self.running = False + self.paused = False self.logger = logging.getLogger(__name__) self.mqttc = None @@ -64,20 +65,28 @@ class AmcrestMqtt(object): def mqtt_on_connect(self, client, userdata, flags, rc, properties): if rc != 0: - self.logger.error(f'MQTT CONNECTION ISSUE ({rc})') + self.logger.error(f'MQTT connection issue ({rc})') exit() + self.logger.info(f'MQTT connected as {self.client_id}') client.subscribe(self.get_device_sub_topic()) client.subscribe(self.get_attribute_sub_topic()) def mqtt_on_disconnect(self, client, userdata, flags, rc, properties): self.logger.info('MQTT connection closed') + self.mqttc.loop_stop() if self.running and time.time() > self.mqtt_connect_time + 10: + self.paused = True + self.logger.info('Sleeping for 30 seconds to give MQTT time to relax') + time.sleep(30) + # lets use a new client_id for a reconnect self.client_id = self.get_new_client_id() self.mqttc_create() + self.paused = False else: + self.running = False exit() def mqtt_on_log(self, client, userdata, paho_log_level, msg): @@ -154,6 +163,7 @@ class AmcrestMqtt(object): callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=self.client_id, clean_session=False, + reconnect_on_failure=False, ) if self.mqtt_config.get('tls_enabled'): @@ -499,6 +509,7 @@ class AmcrestMqtt(object): 'payload_off': 'off', 'device_class': 'motion', 'state_topic': self.get_discovery_topic(device_id, 'motion'), + 'json_attributes_topic': self.get_discovery_topic(device_id, 'motion'), 'value_template': '{{ value_json.state }}', 'unique_id': self.get_slug(device_id, 'motion'), } @@ -671,8 +682,6 @@ class AmcrestMqtt(object): self.logger.info(f'Failed to get recorded file from {self.get_device_name(device_id)}') return - self.logger.info(f'Got back base64 image of {len(image)} bytes') - # only store and send to MQTT if the image has changed if device_states['camera'][type] is None or device_states['camera'][type] != image: device_states['camera'][type] = image @@ -781,23 +790,23 @@ class AmcrestMqtt(object): async def collect_storage_info(self): while self.running == True: - self.refresh_storage_all_devices() + if not self.paused: self.refresh_storage_all_devices() if self.running: await asyncio.sleep(self.storage_update_interval) async def collect_events(self): while self.running == True: - await self.collect_all_device_events() + if not self.paused: await self.collect_all_device_events() if self.running: await asyncio.sleep(1) async def check_event_queue(self): while self.running == True: - self.check_for_events() + if not self.paused: self.check_for_events() if self.running: await asyncio.sleep(1) async def collect_snapshots(self): while self.running == True: await self.amcrestc.collect_all_device_snapshots() - self.refresh_snapshot_all_devices() + if not self.paused: self.refresh_snapshot_all_devices() if self.running: await asyncio.sleep(self.snapshot_update_interval) # main loop diff --git a/app.py b/app.py index 544d2f8..59d94d7 100644 --- a/app.py +++ b/app.py @@ -77,12 +77,16 @@ except: } config['version'] = version config['configpath'] = os.path.dirname(configpath) + +# defaults if 'username' not in config['mqtt']: config['mqtt']['username'] = '' if 'password' not in config['mqtt']: config['mqtt']['password'] = '' -if 'qos' not in config['mqtt']: config['mqtt']['qos'] = 0 -if 'timezone' not in config: config['timezone'] = 'UTC' -if 'debug' not in config: config['debug'] = os.getenv('DEBUG') or False +if 'qos' not in config['mqtt']: config['mqtt']['qos'] = 0 +if 'timezone' not in config: config['timezone'] = 'UTC' +if 'debug' not in config: config['debug'] = os.getenv('DEBUG') or False +if 'hide_ts' not in config: config['hide_ts'] = os.getenv('HIDE_TS') or False +# init logging, based on config settings logging.basicConfig( format = '%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s' if config['hide_ts'] == False else '[%(levelname)s] %(name)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S',