From b5a1d1759ad42b654a066922eee40bd63f31a900 Mon Sep 17 00:00:00 2001 From: gcobb321 Date: Wed, 31 Jul 2024 11:54:10 -0400 Subject: [PATCH] iCloud3 v3.0.5.7 (7/31/2024) --- custom_components/icloud3/ChangeLog.txt | 9 + custom_components/icloud3/config_flow.py | 20 +- custom_components/icloud3/const.py | 2 +- custom_components/icloud3/helpers/file_io.py | 328 ++++++++++++++++++ custom_components/icloud3/manifest.json | 2 +- .../icloud3/support/start_ic3.py | 5 +- 6 files changed, 352 insertions(+), 14 deletions(-) create mode 100644 custom_components/icloud3/helpers/file_io.py diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index 2b442c7..4c4b128 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -3,6 +3,15 @@ **Installing for the first time_** - See [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.2-installing-and-configuring) for instructions on installing as a New Installation **iCloud3 v3 Documentation** - iCloud3 User Guide can be found [here](https://gcobb321.github.io/icloud3_v3_docs/#/) + +3.0.5.7 +....................... +### Change Log - v3.0.5.6 (7/15/2024) +1. ICLOUD3 PROBLEMS WITH HA 2024.7.4 - Fixed +2. ADD/UPDATE DEVICE CONFIGURATION (Fixed) - This was probably caused by HA 2024.7.4 Loading issues. +3. MOBILE APP NOTIFY MESSAGE (Fixed) - A warning message about not being able to send a notification to a device was displayed in the Event Log when the device was not using the Mobile App. + + 3.0.5.6 ....................... ### Change Log - v3.0.5.6 (7/15/2024) diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 58bb14e..46c74c6 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -1520,7 +1520,8 @@ async def async_step_waze_main(self, user_input=None, errors=None): #------------------------------------------------------------------------------------------- async def async_step_special_zones(self, user_input=None, errors=None): self.step_id = 'special_zones' - user_input, action_item = self._action_text_to_item(user_input)\ + user_input, action_item = self._action_text_to_item(user_input) + await self._build_zone_list() if self.common_form_handler(user_input, action_item, errors): return await self.async_step_menu() @@ -2865,6 +2866,7 @@ async def async_step_add_device(self, user_input=None, errors=None): self.step_id = 'add_device' self.errors = errors or {} self.errors_user_input = {} + await self._build_update_device_selection_lists() if user_input is None: return self.async_show_form(step_id=self.step_id, @@ -3271,12 +3273,12 @@ async def _build_update_device_selection_lists(self): """ Setup the option lists used to select device parameters """ await self._build_picture_filename_list() - self._build_mobapp_entity_list() - self._build_zone_list() + await self._build_mobapp_entity_list() + await self._build_zone_list() await self._build_famshr_devices_list() # self._build_fmf_devices_list() - self._build_devicename_by_famshr_fmf() + await self._build_devicename_by_famshr_fmf() #---------------------------------------------------------------------- async def _build_famshr_devices_list(self): @@ -3378,7 +3380,7 @@ def _build_fmf_devices_list(self): self.fmf_list_text_by_email = self.fmf_list_text_by_email_base.copy() #---------------------------------------------------------------------- - def _build_devicename_by_famshr_fmf(self, current_devicename=None): + async def _build_devicename_by_famshr_fmf(self, current_devicename=None): ''' Cycle thru the configured devices and build a devicename by the famshr fname and fmf email values. This is used to validate these @@ -3419,7 +3421,7 @@ def _build_devicename_by_famshr_fmf(self, current_devicename=None): # self.fmf_list_text_by_email[fmf_email] = f"{fmf_text}{devicename_msg}" #---------------------------------------------------------------------- - def _build_mobapp_entity_list(self): + async def _build_mobapp_entity_list(self): ''' Cycle through the /config/.storage/core.entity_registry file and return the entities for platform ('mobile_app', etc) @@ -3652,11 +3654,12 @@ def x_build_picture_filename_list(self): log_exception(err) #------------------------------------------------------------------------------------------- - def _build_zone_list(self): + async def _build_zone_list(self): if self.zone_name_key_text != {}: return + fname_zones = [] for zone, Zone in Gb.HAZones_by_zone.items(): if is_statzone(zone): @@ -5051,9 +5054,6 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): #------------------------------------------------------------------------ elif step_id == 'special_zones': try: - if self.zone_name_key_text == {}: - self._build_zone_list() - pass_thru_zone_used = (Gb.conf_general[CONF_PASSTHRU_ZONE_TIME] > 0) stat_zone_used = (Gb.conf_general[CONF_STAT_ZONE_STILL_TIME] > 0) track_from_base_zone_used = Gb.conf_general[CONF_TRACK_FROM_BASE_ZONE_USED] diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index a352a4a..1fc940c 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -10,7 +10,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.5.6' +VERSION = '3.0.5.7' VERSION_BETA = '' #----------------------------------------- DOMAIN = 'icloud3' diff --git a/custom_components/icloud3/helpers/file_io.py b/custom_components/icloud3/helpers/file_io.py new file mode 100644 index 0000000..c7f59dd --- /dev/null +++ b/custom_components/icloud3/helpers/file_io.py @@ -0,0 +1,328 @@ + +from ..global_variables import GlobalVariables as Gb +from ..const import (CRLF_DOT, ) +from .common import (instr, ) +from .messaging import (log_exception, _trace, _traceha, ) + +from collections import OrderedDict +from homeassistant.util import json as json_util +from homeassistant.helpers import json as json_helpers +import os +import asyncio +import aiofiles.ospath +import logging +_LOGGER = logging.getLogger(__name__) +#_LOGGER = logging.getLogger(f"icloud3") + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# JSON FILE UTILITIES +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +async def async_load_json_file(filename): + + if file_exists(filename) is False: + return {} + + try: + data = await Gb.hass.async_add_executor_job( + json_util.load_json, + filename) + return data + + except Exception as err: + #_LOGGER.exception(err) + pass + + return {} + +#-------------------------------------------------------------------- +def load_json_file(filename): + + if file_exists(filename) is False: + return {} + + try: + if Gb.initial_icloud3_loading_flag: + data = json_util.load_json(filename) + else: + data = Gb.hass.async_add_executor_job( + json_util.load_json, + filename) + return data + + except RuntimeError as err: + if str(err) == 'no running event loop': + data = json_util.load_json(filename) + return data + + except Exception as err: + _LOGGER.exception(err) + pass + + return {} + +#-------------------------------------------------------------------- +async def async_save_json_file(filename, data): + + try: + await Gb.hass.async_add_executor_job( + json_helpers.save_json, + filename, + data) + return True + + except Exception as err: + _LOGGER.exception(err) + pass + + return False + +#-------------------------------------------------------------------- +def save_json_file(filename, data): + + try: + # The HA event loop has not been set up yet during initialization + if Gb.initial_icloud3_loading_flag: + json_helpers.save_json(filename, data) + else: + + Gb.hass.async_add_executor_job( + json_helpers.save_json, + filename, + data) + return True + + except RuntimeError as err: + if err == 'no running event loop': + json_helpers.save_json(filename, data) + return True + + except Exception as err: + _LOGGER.exception(err) + pass + + return False + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# PYTHON OS. FILE I/O AND OTHER UTILITIES +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +def file_exists(filename): + return _os(os.path.isfile, filename) + # return _os(aiofiles.ospath.isfile, filename) +def remove_file(filename): + return _os(os.remove, filename) + # return _os(aiofiles.ospath.remove, filename) + +def directory_exists(dir_name): + return _os(os.path.exists, dir_name) + # return _os(aiofiles.ospath.exists, dir_name) +def make_directory(dir_name): + if directory_exists(dir_name): + return True + return _os(os.makedirs, dir_name) + # return _os(aiofiles.ospath.makedirs, dir_name) + +def extract_filename(directory_filename): + return _os(os.path.basename, directory_filename) + # return _os(aiofiles.ospath.basename, directory_filename) + +#-------------------------------------------------------------------- +def _os(os_module, filename, on_error=None): + try: + # if Gb.ha_started: + if True is False: + results = asyncio.run_coroutine_threadsafe( + async_os(os_module, filename), Gb.hass.loop).result() + else: + results = os_module(filename) + + return results + + except RuntimeError as err: + if err == 'no running event loop': + return os_module(filename) + + except Exception as err: + log_exception(err) + pass + + return on_error or False +#................................................................... +async def async_os(os_module, filename): + return await Gb.hass.async_add_executor_job(os_module, filename) + +#-------------------------------------------------------------------- +def rename_file(from_filename, to_filename): + try: + if file_exists(from_filename) is False: + return False + if file_exists(to_filename): + remove_file(to_filename) + + if Gb.initial_icloud3_loading_flag: + os.rename(from_filename, to_filename) + return True + else: + asyncio.run_coroutine_threadsafe( + async_rename_file(from_filename, to_filename), Gb.hass.loop) + return True + + except RuntimeError as err: + if err == 'no running event loop': + os.rename(from_filename, to_filename) + return True + + except Exception as err: + log_exception(err) + pass + + return False +#................................................................... +async def async_rename_file(from_filename, to_filename): + return await Gb.hass.async_add_executor_job(os.rename, from_filename, to_filename) + +#-------------------------------------------------------------------- +# def x_extract_filename(directory_filename): +# try: +# if file_exists(directory_filename) is False: +# return '???.???' +# elif Gb.initial_icloud3_loading_flag: +# filename = os.path.basename(directory_filename) +# else: +# filename = asyncio.run_coroutine_threadsafe( +# async_extract_filename(directory_filename), Gb.hass.loop).result() +# return filename + +# except RuntimeError as err: +# if err == 'no running event loop': +# filename = os.path.basename(directory_filename) +# return filename + +# except Exception as err: +# log_exception(err) +# pass + +# return '???.???' +# #................................................................... +# async def async_extract_filename(directory_filename): +# return await Gb.hass.async_add_executor_job(os.path.basename, directory_filename) + +#-------------------------------------------------------------------- +def delete_file(file_desc, directory, filename, backup_extn=None, delete_old_sv_file=False): + ''' + Delete a file. + Parameters: + directory - directory containing the file to be deleted + filename - file to be deleted + backup_extn - rename the filename to this extension before deleting + delete_old_sv_file - Some files were previously renamed to .sv before deleting + They should be deleted if they exist. + ''' + try: + file_msg = "" + directory_filename = (f"{directory}/{filename}") + + if backup_extn: + filename_bu = f"{filename.split('.')[0]}.{backup_extn}" + directory_filename_bu = (f"{directory}/{filename_bu}") + + if file_exists(directory_filename_bu): + remove_file(directory_filename_bu) + file_msg += (f"{CRLF_DOT}Deleted backup file (...{filename_bu})") + + rename_file(directory_filename, directory_filename_bu) + file_msg += (f"{CRLF_DOT}Rename current file to ...{filename}.{backup_extn})") + + if file_exists(directory_filename): + remove_file(directory_filename) + file_msg += (f"{CRLF_DOT}Deleted file (...{filename})") + + if delete_old_sv_file: + filename = f"{filename.split('.')[0]}.sv" + directory_filename = f"{directory_filename.split('.')[0]}.sv" + if file_exists(directory_filename): + remove_file(directory_filename) + file_msg += (f"{CRLF_DOT}Deleted file (...{filename})") + + if file_msg != "": + if instr(directory, 'config'): + directory = f"config{directory.split('config')[1]}" + file_msg = f"{file_desc} file > ({directory}) {file_msg}" + + return file_msg + + except Exception as err: + Gb.HALogger.exception(err) + return "Delete error" + + +#-------------------------------------------------------------------- +def get_file_list(start_dir=None, file_extn_filter=[]): + return get_file_or_directory_list( directory_list=False, + start_dir=start_dir, + file_extn_filter=file_extn_filter) + +def get_directory_list(start_dir=None): + return get_file_or_directory_list( directory_list=True, + start_dir=start_dir, + file_extn_filter=[]) + +#-------------------------------------------------------------------- +def get_file_or_directory_list(directory_list=False, start_dir=None, file_extn_filter=[]): + ''' + Return a list of directories or files in a given path + + Parameters: + - directory_list = True (List of directories), False (List of files) + - start_dir = Top level directory to start searching from ('www') + -file_extn_filter = List of files witn extensions to include (['png' 'jpg'], []) + + Can call from executor function: + directory_list, start_dir, file_filter = [False, 'www', ['png', 'jpg', 'jpeg']] + image_filenames = await Gb.hass.async_add_executor_job( + self.get_file_or_directory_list, + directory_list, + start_dir, + file_filter) + ''' + + directory_filter = ['/.', 'deleted', '/x-'] + filename_or_directory_list = [] + path_config_base = f"{Gb.ha_config_directory}/" + back_slash = '\\' + if start_dir is None: start_dir = '' + + for path, dirs, files in os.walk(f"{path_config_base}{start_dir}"): + www_sub_directory = path.replace(path_config_base, '') + in_filter_cnt = len([filter for filter in directory_filter if instr(www_sub_directory, filter)]) + if in_filter_cnt > 0 or www_sub_directory.count('/') > 4 or www_sub_directory.count(back_slash): + continue + + if directory_list: + filename_or_directory_list.append(www_sub_directory) + continue + + # Filter unwanted directories - std dirs are www/icloud3, www/cummunity, www/images + if Gb.picture_www_dirs: + valid_dir = [dir for dir in Gb.picture_www_dirs if www_sub_directory.startswith(dir)] + if valid_dir == []: + continue + + dir_filenames = [f"{www_sub_directory}/{file}" + for file in files + if (file_extn_filter + and file.rsplit('.', 1)[-1] in file_extn_filter)] + + filename_or_directory_list.extend(dir_filenames[:25]) + if len(dir_filenames) > 25: + filename_or_directory_list.append( + f"⛔ {www_sub_directory} > The first 25 files out of " + f"{len(dir_filenames)} are listed") + + return filename_or_directory_list \ No newline at end of file diff --git a/custom_components/icloud3/manifest.json b/custom_components/icloud3/manifest.json index 03802c6..ced1e07 100644 --- a/custom_components/icloud3/manifest.json +++ b/custom_components/icloud3/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "https://github.com/gcobb321/icloud3_v3/issues", "loggers": ["icloud3"], "requirements": [], - "version": "3.0.5.6" + "version": "3.0.5.7" } diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index 780cd24..c262ba7 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -2423,8 +2423,9 @@ def setup_notify_service_name_for_mobapp_devices(post_event_msg=False): mobapp_devicename = mobile_app_notify_devicename.replace('mobile_app_', '') for devicename, Device in Gb.Devices_by_devicename.items(): if instr(mobapp_devicename, devicename) or instr(devicename, mobapp_devicename): - Device.mobapp_monitor_flag = True - if Device.mobapp[NOTIFY] == '': + if (Device.conf_mobapp_fname != 'None' + and Device.mobapp_monitor_flag + and Device.mobapp[NOTIFY] == ''): Device.mobapp[NOTIFY] = mobile_app_notify_devicename setup_msg += (f"{CRLF_DOT}{Device.devicename_fname}{RARROW}{mobile_app_notify_devicename}") break