From 23fc9f3de03a2174a215fe87d3a7043a2d8b3f26 Mon Sep 17 00:00:00 2001 From: gcobb321 Date: Mon, 29 Jul 2024 15:58:40 -0400 Subject: [PATCH] iCloud3 v3.0.5.6 --- custom_components/icloud3/ChangeLog.txt | 11 +- custom_components/icloud3/config_flow.py | 102 ++++++++++++++++-- custom_components/icloud3/const.py | 2 +- custom_components/icloud3/helpers/common.py | 71 +++++++++++- custom_components/icloud3/manifest.json | 2 +- custom_components/icloud3/sensor.py | 5 - .../icloud3/support/mobapp_data_handler.py | 4 + .../icloud3/support/pyicloud_ic3.py | 1 - .../icloud3/support/pyicloud_ic3_interface.py | 3 +- .../icloud3/support/start_ic3.py | 3 - 10 files changed, 179 insertions(+), 25 deletions(-) diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index a79ba26..2b442c7 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -3,10 +3,19 @@ **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.6 +....................... +### Change Log - v3.0.5.6 (7/15/2024) +1. ICLOUD3 BUG FIXES - Fixes the following errors: + ```AttributeError: 'NoneType' object has no attribute 'init_step_complete'``` + ```AttributeError: module 'custom_components.icloud3.sensor' has no attribute _setup_recorder_exclude_sensor_filter'``` +2. HA ERROR/WARNING MESSAGES - Fixed a problem where some I/O getting directory and filename list for the Update Devices configuration screen was being done outside of the HA Event Loop + + 3.0.5.5 ....................... ### Change Log - v3.0.5.5 (7/15/2024) -1. ICLOUD3 FAILED TO LOAD (Fix) - iCloud3 injected special code into the HA history recorder to exclude various sensor entities with large text fields from being added to the history recorder. This caused problems with HA in the 2024.7 release. The HA 2024.7.2 included special code that blocked iCloud3 from loading. A temporary patch was posted on the iCloud3 GitHub repository to disable the recorder injection. This update is a permant fix. +1. ICLOUD3 FAILED TO LOAD (Fix) - iCloud3 injected special code into the HA history recorder to exclude various sensor entities with large text fields from being added to the history recorder. This caused problems with HA in the 2024.7 release. The HA 2024.7.2 included special code that blocked iCloud3 from loading. A temporary patch was posted on the iCloud3 GitHub repository to disable the recorder injection. This update is a permant fix. All sensor attributes not related to the battery, distance and timer sensors are being added to the HA recorder history database. Text base sensor attributes are not being added (info sensors, Event Log sensor, badge, tracking update, zone, etc.).. diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 67e4ae2..58bb14e 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -68,6 +68,7 @@ from .const_sensor import (SENSOR_GROUPS ) from .helpers.common import (instr, isnumber, obscure_field, list_to_str, str_to_list, is_statzone, zone_dname, isbetween, list_del, list_add, + get_file_list, get_directory_list, sort_dict_by_values, ) from .helpers.messaging import (log_exception, log_debug_msg, log_info_msg, _traceha, _trace, @@ -1428,12 +1429,36 @@ def _dzf_set_example_zone_name_text(self, key, example_text, real_text): if key in DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT: DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT[key] = \ DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT[key].replace(example_text, real_text) - #------------------------------------------------------------------------------------------- async def async_step_tracking_parameters(self, user_input=None, errors=None): self.step_id = 'tracking_parameters' user_input, action_item = self._action_text_to_item(user_input) + if self.www_directory_list == []: + directory_list, start_dir, file_filter = [True, 'www', []] + self.www_directory_list = await Gb.hass.async_add_executor_job( + get_directory_list, + start_dir) + + if self.common_form_handler(user_input, action_item, errors): + if action_item == 'save': + Gb.picture_www_dirs = Gb.conf_profile[CONF_PICTURE_WWW_DIRS].copy() + self.picture_by_filename = {} + return await self.async_step_menu() + + + if self._any_errors(): + self.errors['action_items'] = 'update_aborted' + + return self.async_show_form(step_id=self.step_id, + data_schema=self.form_schema(self.step_id), + errors=self.errors) + +#------------------------------------------------------------------------------------------- + async def xasync_step_tracking_parameters(self, user_input=None, errors=None): + self.step_id = 'tracking_parameters' + user_input, action_item = self._action_text_to_item(user_input) + if self.www_directory_list == []: dir_filters = ['/.', 'deleted', '/x-'] path_config_base = f"{Gb.ha_config_directory}/" @@ -2283,7 +2308,7 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f or self.errors.get('base', '') == 'icloud_acct_logged_into') or self.errors.get('base', '') == 'icloud_acct_not_logged_into'): - await self._build_update_device_selection_lists() + # await self._build_update_device_selection_lists() self._prepare_device_selection_list() user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = self.endpoint_suffix @@ -2543,7 +2568,7 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r data_schema=self.form_schema(called_from_step_id), errors=self.errors) - await self._build_update_device_selection_lists() + # await self._build_update_device_selection_lists() self.obscure_username = obscure_field(self.username) or 'NoUsername' self.obscure_password = obscure_field(self.password) or 'NoPassword' @@ -2628,13 +2653,13 @@ async def async_step_device_list(self, user_input=None, errors=None): called_from_step_id='device_list') device_cnt = len(Gb.conf_devices) - if user_input is None: - await self._build_update_device_selection_lists() + # if user_input is None: + # await self._build_update_device_selection_lists() if user_input is not None: if (action_item in ['update_device', 'delete_device'] and CONF_DEVICES not in user_input): - await self._build_update_device_selection_lists() + # await self._build_update_device_selection_lists() action_item = '' if action_item == 'return': @@ -2892,6 +2917,7 @@ async def async_step_update_device(self, user_input=None, errors=None): self.step_id = 'update_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, @@ -3244,7 +3270,7 @@ def _update_changed_sensor_entities(self): async def _build_update_device_selection_lists(self): """ Setup the option lists used to select device parameters """ - self._build_picture_filename_list() + await self._build_picture_filename_list() self._build_mobapp_entity_list() self._build_zone_list() @@ -3506,8 +3532,63 @@ def _format_device_list_item(self, conf_device_data): return device_info + +#------------------------------------------------------------------------------------------- + async def _build_picture_filename_list(self): + + try: + if self.picture_by_filename != {}: + return + + directory_list, start_dir, file_filter = [False, 'www', ['png', 'jpg', 'jpeg']] + image_filenames = await Gb.hass.async_add_executor_job( + get_file_list, + start_dir, + file_filter) + # image_filenames = await Gb.hass.async_add_executor_job( + # self.get_file_or_directory_list, + # directory_list, + # start_dir, + # file_filter) + + sorted_image_filenames = [] + over_25_warning_msgs = [] + for image_filename in image_filenames: + if image_filename.startswith('⛔'): + over_25_warning_msgs.append(image_filename) + else: + sorted_image_filenames.append(f"{image_filename.rsplit('/', 1)[1]}:{image_filename}") + sorted_image_filenames.sort() + self.picture_by_filename = {} + self.picture_by_filename['www_dirs'] = "Source Directories:" + www_dir_idx = 0 + + if Gb.picture_www_dirs: + while www_dir_idx < len(Gb.picture_www_dirs): + self.picture_by_filename[f"www_dirs{www_dir_idx}"] = \ + f"{DOT}{list_to_str(Gb.picture_www_dirs[www_dir_idx:www_dir_idx+3])}" + www_dir_idx += 3 + else: + self.picture_by_filename["www_dirs0"] = f"{DOT}All `www/*` directories are scaned" + + for over_25_warning_msg in over_25_warning_msgs: + www_dir_idx += 1 + self.picture_by_filename[f"www_dirs{www_dir_idx}"] = over_25_warning_msg + + #self.picture_by_filename.extend(sorted_image_filenames) + self.picture_by_filename['www_dirs998'] = "Set filter on `Tracking and Other Parameters` screen" + self.picture_by_filename['www_dirs999'] = f"{'-'*85}" + self.picture_by_filename.update(self.picture_by_filename_base) + + for sorted_image_filename in sorted_image_filenames: + image_filename, image_filename_path = sorted_image_filename.split(':') + self.picture_by_filename[image_filename_path] = \ + f"{image_filename}{RARROW}{image_filename_path.replace(image_filename, '')}" + + except Exception as err: + log_exception(err) #------------------------------------------------------------------------------------------- - def _build_picture_filename_list(self): + def x_build_picture_filename_list(self): try: if self.picture_by_filename != {}: @@ -3863,7 +3944,6 @@ def _create_sensor_entity(self, devicename, conf_device, new_sensors_list): return Gb.async_add_entities_sensor(NewSensors, True) - ic3_sensor._setup_recorder_exclude_sensor_filter(NewSensors) #------------------------------------------------------------------------------------------- def _get_all_sensors_list(self): @@ -4507,8 +4587,8 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): #------------------------------------------------------------------------ elif step_id == 'update_device': - self._build_picture_filename_list() - self._build_devicename_by_famshr_fmf(self.conf_device_selected[CONF_IC3_DEVICENAME]) + # self._build_picture_filename_list() + # self._build_devicename_by_famshr_fmf(self.conf_device_selected[CONF_IC3_DEVICENAME]) error_key = '' self.errors = self.errors or {} diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index 63e7128..a352a4a 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -10,7 +10,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.5.5' +VERSION = '3.0.5.6' VERSION_BETA = '' #----------------------------------------- DOMAIN = 'icloud3' diff --git a/custom_components/icloud3/helpers/common.py b/custom_components/icloud3/helpers/common.py index e190a5c..a30c8b1 100644 --- a/custom_components/icloud3/helpers/common.py +++ b/custom_components/icloud3/helpers/common.py @@ -280,7 +280,11 @@ def format_list(arg_list): def format_cnt(desc, n): return f", {desc}(#{n})" if n > 1 else '' -#-------------------------------------------------------------------- +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# PYTHON OS. FILE I/O AND OTHER UTILITIES +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_load_json_file(filename): if os.path.exists(filename) is False: return {} @@ -413,6 +417,71 @@ def delete_file(file_desc, directory, filename, backup_extn=None, delete_old_sv_ 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 + #-------------------------------------------------------------------- def encode_password(password): ''' diff --git a/custom_components/icloud3/manifest.json b/custom_components/icloud3/manifest.json index ec7829d..03802c6 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.5" + "version": "3.0.5.6" } diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index 3a6e1d7..3345aa3 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -80,8 +80,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e NewSensors = [] Gb.EvLogSensor = Sensor_EventLog(SENSOR_EVENT_LOG_NAME) - # Gb.EvLogSensor = Sensor_EventLog_ExcludeFromRecorder(SENSOR_EVENT_LOG_NAME) - _traceha(f"EVLOG SENSOR {Gb.EvLogSensor=}") if Gb.EvLogSensor: NewSensors.append(Gb.EvLogSensor) else: @@ -1133,14 +1131,11 @@ def _format_devices_distance_extra_attrs(self): # ''' # try: # if isnumber(number) is False: - # _trace(f"zz {self.sensor} {number=} {um=}") # return number # um = um if um else Gb.um # precision = 5 if um in ['km', 'mi'] else 2 if um in ['m', 'ft'] else 4 - # _trace(f"aa {self.sensor} {number=} {um=} {precision=}") # number = round(float(number), precision) - # _trace(f"bb {self.sensor} {number=} {um=} {precision=}") # except Exception as err: # pass diff --git a/custom_components/icloud3/support/mobapp_data_handler.py b/custom_components/icloud3/support/mobapp_data_handler.py index 3d7ea4f..067202d 100644 --- a/custom_components/icloud3/support/mobapp_data_handler.py +++ b/custom_components/icloud3/support/mobapp_data_handler.py @@ -412,6 +412,10 @@ def get_mobapp_device_trkr_entity_attrs(Device): None - error or no data is available ''' try: + if (Device.mobapp_monitor_flag is False + or Gb.conf_data_source_MOBAPP is False): + return None + entity_id = Device.mobapp[DEVICE_TRACKER] device_trkr_attrs = {} device_trkr_attrs[DEVICE_TRACKER] = entity_io.get_state(entity_id) diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index a3b1dc9..ed89b4d 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -670,7 +670,6 @@ def _authenticate_with_token(self): self.data = req.json() if 'dsInfo' in self.data: - _traceha(f"{self.account_name=} {self.data['dsInfo']=}") if 'fullName' in self.data['dsInfo']: self.account_name = self.data['dsInfo']['fullName'] self.account_locked = self.data['dsInfo']['locked'] diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index a12fa1a..6aa08d0 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -117,7 +117,8 @@ def verify_pyicloud_setup_status(): create_PyiCloudService(Gb.PyiCloud, instance='startup') Gb.PyiCloud = Gb.PyiCloud or Gb.PyiCloudInit - if 'Authenticate' not in Gb.PyiCloud.init_step_complete: + if (Gb.PyiCloud is None + or 'Authenticate' not in Gb.PyiCloud.init_step_complete): create_PyiCloudService(Gb.PyiCloud, instance='startup') Gb.PyiCloud = Gb.PyiCloud or Gb.PyiCloudInit if Gb.PyiCloud is None: diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index 448a863..780cd24 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -1802,7 +1802,6 @@ def setup_tracked_devices_for_fmf(PyiCloud=None): return broadcast_info_msg(f"Stage 3 > Set up Find-my-Friends Devices") - _trace(f"Stage 3 > Set up Find-my-Friends Devices") # devices_desc = get_fmf_devices(PyiCloud) # device_id_by_fmf_email = devices_desc[0] @@ -1815,13 +1814,11 @@ def setup_tracked_devices_for_fmf(PyiCloud=None): return PyiCloud = Gb.PyiCloud - _trace(f"{PyiCloud.FindMyFriends=}") PyiCloud.create_FindMyFriends_object() _FmF = PyiCloud.FindMyFriends # _Fmf._set_service_available(True) Gb.conf_data_source_FMF = True _FmF.refresh_client() - _trace(f"{PyiCloud.FindMyFriends=}") event_msg = "Find-My-Friends Devices > " # if Gb.conf_data_source_FMF is False: